From e7861cabfc610291d534ae078765a71dc7007c7e Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 28 Jan 2023 19:43:10 +0200 Subject: refactor: move visitor, panhelper, typeset modules into md This commit just moves files around and fixes references to moved modules. Sponsored-by: author --- src/lib.rs | 6 -- src/md.rs | 10 +- src/md/panhelper.rs | 26 +++++ src/md/typeset.rs | 229 ++++++++++++++++++++++++++++++++++++++++++ src/md/visitor/block_class.rs | 25 +++++ src/md/visitor/embedded.rs | 35 +++++++ src/md/visitor/image.rs | 25 +++++ src/md/visitor/linting.rs | 40 ++++++++ src/md/visitor/mod.rs | 17 ++++ src/md/visitor/structure.rs | 100 ++++++++++++++++++ src/md/visitor/typesetting.rs | 85 ++++++++++++++++ src/panhelper.rs | 26 ----- src/typeset.rs | 229 ------------------------------------------ src/visitor/block_class.rs | 25 ----- src/visitor/embedded.rs | 35 ------- src/visitor/image.rs | 25 ----- src/visitor/linting.rs | 40 -------- src/visitor/mod.rs | 17 ---- src/visitor/structure.rs | 100 ------------------ src/visitor/typesetting.rs | 85 ---------------- 20 files changed, 590 insertions(+), 590 deletions(-) create mode 100644 src/md/panhelper.rs create mode 100644 src/md/typeset.rs create mode 100644 src/md/visitor/block_class.rs create mode 100644 src/md/visitor/embedded.rs create mode 100644 src/md/visitor/image.rs create mode 100644 src/md/visitor/linting.rs create mode 100644 src/md/visitor/mod.rs create mode 100644 src/md/visitor/structure.rs create mode 100644 src/md/visitor/typesetting.rs delete mode 100644 src/panhelper.rs delete mode 100644 src/typeset.rs delete mode 100644 src/visitor/block_class.rs delete mode 100644 src/visitor/embedded.rs delete mode 100644 src/visitor/image.rs delete mode 100644 src/visitor/linting.rs delete mode 100644 src/visitor/mod.rs delete mode 100644 src/visitor/structure.rs delete mode 100644 src/visitor/typesetting.rs diff --git a/src/lib.rs b/src/lib.rs index 1ae64d4..90c183f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,12 +27,6 @@ mod embedded; pub use embedded::EmbeddedFile; pub use embedded::EmbeddedFiles; -mod panhelper; -mod typeset; - -mod visitor; -use visitor::LintingVisitor; - mod policy; pub use policy::get_basedir_from; diff --git a/src/md.rs b/src/md.rs index 15fe57a..6f00ab7 100644 --- a/src/md.rs +++ b/src/md.rs @@ -1,8 +1,8 @@ //! A parsed Markdown document. use crate::{ - parse_scenario_snippet, visitor, Bindings, EmbeddedFiles, LintingVisitor, Scenario, - ScenarioStep, Style, SubplotError, Warning, YamlMetadata, + parse_scenario_snippet, Bindings, EmbeddedFiles, Scenario, ScenarioStep, Style, SubplotError, + Warning, YamlMetadata, }; use log::trace; use pandoc_ast::{Map, MetaValue, MutVisitor, Pandoc}; @@ -11,6 +11,12 @@ use std::collections::HashSet; use std::convert::TryFrom; use std::path::{Path, PathBuf}; +mod panhelper; +mod typeset; + +mod visitor; +use visitor::LintingVisitor; + /// A parsed Markdown document. #[derive(Debug)] pub struct Markdown { diff --git a/src/md/panhelper.rs b/src/md/panhelper.rs new file mode 100644 index 0000000..f7ab801 --- /dev/null +++ b/src/md/panhelper.rs @@ -0,0 +1,26 @@ +use pandoc_ast::Attr; + +/// Is a code block marked as being of a given type? +pub fn is_class(attr: &Attr, class: &str) -> bool { + let (_id, classes, _kvpairs) = attr; + classes.iter().any(|s| s == class) +} + +/// Utility function to find key/value pairs from an attribute +pub fn find_attr_kv<'a>(attr: &'a Attr, key: &'static str) -> impl Iterator { + attr.2.iter().flat_map(move |(key_, value)| { + if key == key_ { + Some(value.as_ref()) + } else { + None + } + }) +} + +/// Get the filename for a fenced code block tagged .file. +/// +/// The filename is the first (and presumably only) identifier for the +/// block. +pub fn get_filename(attr: &Attr) -> String { + attr.0.to_string() +} diff --git a/src/md/typeset.rs b/src/md/typeset.rs new file mode 100644 index 0000000..f63206a --- /dev/null +++ b/src/md/typeset.rs @@ -0,0 +1,229 @@ +use crate::parser::parse_scenario_snippet; +use crate::Bindings; +use crate::PartialStep; +use crate::ScenarioStep; +use crate::StepKind; +use crate::SubplotError; +use crate::{DiagramMarkup, DotMarkup, PikchrMarkup, PlantumlMarkup, Svg}; +use crate::{Warning, Warnings}; + +use pandoc_ast::Attr; +use pandoc_ast::Block; +use pandoc_ast::Inline; +use pandoc_ast::Target; + +/// Typeset an error as a Pandoc AST Block element. +pub fn error(err: SubplotError) -> Block { + let msg = format!("ERROR: {}", err); + Block::Para(error_msg(&msg)) +} + +/// Typeset an error message a vector of inlines. +pub fn error_msg(msg: &str) -> Vec { + vec![Inline::Strong(vec![inlinestr(msg)])] +} + +/// Typeset a string as an inline element. +pub fn inlinestr(s: &str) -> Inline { + Inline::Str(String::from(s)) +} + +/// Typeset a code block tagged as a file. +pub fn file_block(attr: &Attr, text: &str) -> Block { + let filename = inlinestr(&attr.0); + let filename = Inline::Strong(vec![filename]); + let intro = Block::Para(vec![inlinestr("File:"), space(), filename]); + let mut cbattrs = attr.clone(); + if cbattrs.1.iter().any(|s| s == "noNumberLines") { + // If the block says "noNumberLines" we remove that class + cbattrs.1.retain(|s| s != "noNumberLines"); + } else if cbattrs.1.iter().all(|s| s != "numberLines") { + // Otherwise if it doesn't say numberLines we add that in. + cbattrs.1.push("numberLines".to_string()); + } + // If this was an `example`, convert that class to `file` + if cbattrs.1.iter().any(|s| s == "example") { + cbattrs.1.retain(|s| s != "example"); + cbattrs.1.push("file".into()); + } + let codeblock = Block::CodeBlock(cbattrs, text.to_string()); + let noattr = ("".to_string(), vec![], vec![]); + Block::Div(noattr, vec![intro, codeblock]) +} + +/// Typeset a scenario snippet as a Pandoc AST Block. +/// +/// Typesetting here means producing the Pandoc abstract syntax tree +/// nodes that result in the desired output, when Pandoc processes +/// them. +/// +/// The snippet is given as a text string, which is parsed. It need +/// not be a complete scenario, but it should consist of complete steps. +pub fn scenario_snippet(bindings: &Bindings, snippet: &str, warnings: &mut Warnings) -> Block { + let lines = parse_scenario_snippet(snippet); + let mut steps = vec![]; + let mut prevkind: Option = None; + + for line in lines { + let (this, thiskind) = step(bindings, line, prevkind, warnings); + steps.push(this); + prevkind = thiskind; + } + Block::LineBlock(steps) +} + +// Typeset a single scenario step as a sequence of Pandoc AST Inlines. +fn step( + bindings: &Bindings, + text: &str, + prevkind: Option, + warnings: &mut Warnings, +) -> (Vec, Option) { + let step = ScenarioStep::new_from_str(text, prevkind); + if step.is_err() { + return ( + error_msg(&format!("Could not parse step: {}", text)), + prevkind, + ); + } + let step = step.unwrap(); + + let m = match bindings.find("", &step) { + Ok(m) => m, + Err(e) => { + let w = Warning::UnknownBinding(format!("{}", e)); + warnings.push(w.clone()); + return (error_msg(&format!("{}", w)), prevkind); + } + }; + + let mut inlines = vec![keyword(&step, prevkind), space()]; + + for part in m.parts() { + match part { + PartialStep::UncapturedText(s) => inlines.push(uncaptured(s.text())), + PartialStep::CapturedText { text, .. } => inlines.push(captured(text)), + } + } + + (inlines, Some(step.kind())) +} + +// Typeset first word, which is assumed to be a keyword, of a scenario +// step. +fn keyword(step: &ScenarioStep, prevkind: Option) -> Inline { + let actual = inlinestr(&format!("{}", step.kind())); + let and = inlinestr("and"); + let keyword = if let Some(prevkind) = prevkind { + if prevkind == step.kind() { + and + } else { + actual + } + } else { + actual + }; + Inline::Emph(vec![keyword]) +} + +// Typeset a space between words. +fn space() -> Inline { + Inline::Space +} + +// Typeset an uncaptured part of a step. +fn uncaptured(s: &str) -> Inline { + inlinestr(s) +} + +// Typeset a captured part of a step. +fn captured(s: &str) -> Inline { + Inline::Strong(vec![inlinestr(s)]) +} + +/// Typeset a link as a note. +pub fn link_as_note(attr: Attr, text: Vec, target: Target) -> Inline { + let (url, _) = target.clone(); + let url = Inline::Code(attr.clone(), url); + let link = Inline::Link(attr.clone(), vec![url], target); + let note = Inline::Note(vec![Block::Para(vec![link])]); + let mut text = text; + text.push(note); + Inline::Span(attr, text) +} + +/// Take a pikchr diagram, render it as SVG, and return an AST block element. +/// +/// The `Block` will contain the SVG data. This allows the diagram to +/// be rendered without referencing external entities. +/// +/// If the code block which contained the pikchr contains other classes, they +/// can be added to the SVG for use in later typesetting etc. +pub fn pikchr_to_block(pikchr: &str, class: Option<&str>, warnings: &mut Warnings) -> Block { + match PikchrMarkup::new(pikchr, class).as_svg() { + Ok(svg) => typeset_svg(svg), + Err(err) => { + warnings.push(Warning::Pikchr(format!("{}", err))); + error(err) + } + } +} + +// Take a dot diagram, render it as SVG, and return an AST Block +// element. The Block will contain the SVG data. This allows the +// diagram to be rendered without referending external entities. +pub fn dot_to_block(dot: &str, warnings: &mut Warnings) -> Block { + match DotMarkup::new(dot).as_svg() { + Ok(svg) => typeset_svg(svg), + Err(err) => { + warnings.push(Warning::Dot(format!("{}", err))); + error(err) + } + } +} + +// Take a PlantUML diagram, render it as SVG, and return an AST Block +// element. The Block will contain the SVG data. This allows the +// diagram to be rendered without referending external entities. +pub fn plantuml_to_block(markup: &str, warnings: &mut Warnings) -> Block { + match PlantumlMarkup::new(markup).as_svg() { + Ok(svg) => typeset_svg(svg), + Err(err) => { + warnings.push(Warning::Plantuml(format!("{}", err))); + error(err) + } + } +} + +/// Typeset a project roadmap expressed as textual YAML, and render it +/// as an SVG image. +pub fn roadmap_to_block(yaml: &str, warnings: &mut Warnings) -> Block { + match roadmap::from_yaml(yaml) { + Ok(ref mut roadmap) => { + roadmap.set_missing_statuses(); + let width = 50; + match roadmap.format_as_dot(width) { + Ok(dot) => dot_to_block(&dot, warnings), + Err(e) => Block::Para(vec![inlinestr(&e.to_string())]), + } + } + Err(e) => Block::Para(vec![inlinestr(&e.to_string())]), + } +} + +// Typeset an SVG, represented as a byte vector, as a Pandoc AST Block +// element. +fn typeset_svg(svg: Svg) -> Block { + let url = svg_as_data_url(svg); + let attr = ("".to_string(), vec![], vec![]); + let img = Inline::Image(attr, vec![], (url, "".to_string())); + Block::Para(vec![img]) +} + +// Convert an SVG, represented as a byte vector, into a data: URL, +// which can be inlined so the image can be rendered without +// referencing external files. +fn svg_as_data_url(svg: Svg) -> String { + let svg = base64::encode(svg.data()); + format!("data:image/svg+xml;base64,{}", svg) +} diff --git a/src/md/visitor/block_class.rs b/src/md/visitor/block_class.rs new file mode 100644 index 0000000..303616b --- /dev/null +++ b/src/md/visitor/block_class.rs @@ -0,0 +1,25 @@ +use std::collections::HashSet; + +use pandoc_ast::{Block, MutVisitor}; + +#[derive(Default)] +pub struct BlockClassVisitor { + pub classes: HashSet, +} + +impl MutVisitor for BlockClassVisitor { + fn visit_vec_block(&mut self, vec_block: &mut Vec) { + for block in vec_block { + match block { + Block::CodeBlock(attr, _) => { + for class in &attr.1 { + self.classes.insert(class.to_string()); + } + } + _ => { + self.visit_block(block); + } + } + } + } +} diff --git a/src/md/visitor/embedded.rs b/src/md/visitor/embedded.rs new file mode 100644 index 0000000..840d9ed --- /dev/null +++ b/src/md/visitor/embedded.rs @@ -0,0 +1,35 @@ +use crate::md::panhelper; +use crate::EmbeddedFile; +use crate::EmbeddedFiles; + +use pandoc_ast::{Block, MutVisitor}; + +impl MutVisitor for EmbeddedFiles { + fn visit_vec_block(&mut self, vec_block: &mut Vec) { + use panhelper::is_class; + for block in vec_block { + match block { + Block::CodeBlock(attr, contents) => { + if is_class(attr, "file") { + let add_newline = match panhelper::find_attr_kv(attr, "add-newline").next() + { + None | Some("auto") => !contents.ends_with('\n'), + Some("yes") => true, + Some("no") => false, + _ => unreachable!(), + }; + let contents = if add_newline { + format!("{}\n", contents) + } else { + contents.clone() + }; + self.push(EmbeddedFile::new(panhelper::get_filename(attr), contents)); + } + } + _ => { + self.visit_block(block); + } + } + } + } +} diff --git a/src/md/visitor/image.rs b/src/md/visitor/image.rs new file mode 100644 index 0000000..be49d66 --- /dev/null +++ b/src/md/visitor/image.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; + +use pandoc_ast::{Inline, MutVisitor}; + +pub struct ImageVisitor { + images: Vec, +} + +impl ImageVisitor { + pub fn new() -> Self { + ImageVisitor { images: vec![] } + } + + pub fn images(&self) -> Vec { + self.images.clone() + } +} + +impl MutVisitor for ImageVisitor { + fn visit_inline(&mut self, inline: &mut Inline) { + if let Inline::Image(_attr, _inlines, target) = inline { + self.images.push(PathBuf::from(&target.0)); + } + } +} diff --git a/src/md/visitor/linting.rs b/src/md/visitor/linting.rs new file mode 100644 index 0000000..d64b03e --- /dev/null +++ b/src/md/visitor/linting.rs @@ -0,0 +1,40 @@ +use crate::md::panhelper; +use crate::SubplotError; + +use pandoc_ast::{Block, MutVisitor}; + +#[derive(Default)] +pub struct LintingVisitor { + pub issues: Vec, +} + +impl MutVisitor for LintingVisitor { + fn visit_vec_block(&mut self, vec_block: &mut Vec) { + for block in vec_block { + match block { + Block::CodeBlock(attr, _) => { + if panhelper::is_class(attr, "file") || panhelper::is_class(attr, "example") { + let newlines: Vec<_> = + panhelper::find_attr_kv(attr, "add-newline").collect(); + match newlines.len() { + 0 => {} + 1 => match newlines[0].to_ascii_lowercase().as_ref() { + "auto" | "yes" | "no" => {} + _ => self.issues.push(SubplotError::UnrecognisedAddNewline( + panhelper::get_filename(attr), + newlines[0].to_owned(), + )), + }, + _ => self.issues.push(SubplotError::RepeatedAddNewlineAttribute( + panhelper::get_filename(attr), + )), + } + } + } + _ => { + self.visit_block(block); + } + } + } + } +} diff --git a/src/md/visitor/mod.rs b/src/md/visitor/mod.rs new file mode 100644 index 0000000..1c095ac --- /dev/null +++ b/src/md/visitor/mod.rs @@ -0,0 +1,17 @@ +mod block_class; +pub use block_class::BlockClassVisitor; + +mod embedded; + +mod image; +pub use image::ImageVisitor; + +mod linting; +pub use linting::LintingVisitor; + +mod structure; +pub use structure::Element; +pub use structure::StructureVisitor; + +mod typesetting; +pub use typesetting::TypesettingVisitor; diff --git a/src/md/visitor/structure.rs b/src/md/visitor/structure.rs new file mode 100644 index 0000000..d8faef6 --- /dev/null +++ b/src/md/visitor/structure.rs @@ -0,0 +1,100 @@ +use crate::md::panhelper; + +use pandoc_ast::{Block, Inline, MutVisitor}; + +// A structure element in the document: a heading or a scenario snippet. +#[derive(Debug)] +pub enum Element { + // Headings consist of the text and the level of the heading. + Heading(String, i64), + + // Scenario snippets consist just of the unparsed text. + Snippet(String), +} + +impl Element { + pub fn heading(text: &str, level: i64) -> Element { + Element::Heading(text.to_string(), level) + } + + pub fn snippet(text: &str) -> Element { + Element::Snippet(text.to_string()) + } +} + +// A MutVisitor for extracting document structure. +pub struct StructureVisitor { + pub elements: Vec, +} + +impl StructureVisitor { + pub fn new() -> Self { + Self { elements: vec![] } + } +} + +impl MutVisitor for StructureVisitor { + fn visit_vec_block(&mut self, vec_block: &mut Vec) { + use panhelper::is_class; + for block in vec_block { + match block { + Block::Header(level, _attr, inlines) => { + let text = join(inlines); + let heading = Element::heading(&text, *level); + self.elements.push(heading); + } + Block::CodeBlock(attr, s) => { + if is_class(attr, "scenario") { + let snippet = Element::snippet(s); + self.elements.push(snippet); + } + } + _ => { + self.visit_block(block); + } + } + } + } +} + +fn join(vec: &[Inline]) -> String { + let mut buf = String::new(); + join_into_buffer(vec, &mut buf); + buf +} + +fn join_into_buffer(vec: &[Inline], buf: &mut String) { + for item in vec { + match item { + Inline::Str(s) => buf.push_str(s), + Inline::Emph(v) => join_into_buffer(v, buf), + Inline::Strong(v) => join_into_buffer(v, buf), + Inline::Strikeout(v) => join_into_buffer(v, buf), + Inline::Superscript(v) => join_into_buffer(v, buf), + Inline::Subscript(v) => join_into_buffer(v, buf), + Inline::SmallCaps(v) => join_into_buffer(v, buf), + Inline::Quoted(qt, v) => { + let q = match qt { + pandoc_ast::QuoteType::SingleQuote => "'", + pandoc_ast::QuoteType::DoubleQuote => "\"", + }; + buf.push_str(q); + join_into_buffer(v, buf); + buf.push_str(q); + } + Inline::Cite(_, v) => join_into_buffer(v, buf), + Inline::Code(_attr, s) => buf.push_str(s), + Inline::Space => buf.push(' '), + Inline::SoftBreak => buf.push(' '), + Inline::LineBreak => buf.push(' '), + Inline::Math(_, s) => buf.push_str(s), + Inline::RawInline(_, s) => buf.push_str(s), + Inline::Link(_, v, _) => join_into_buffer(v, buf), + Inline::Image(_, v, _) => join_into_buffer(v, buf), + Inline::Note(_) => buf.push_str(""), + Inline::Span(_attr, v) => join_into_buffer(v, buf), + #[cfg(feature = "pandoc_ast_08")] + Inline::Underline(v) => join_into_buffer(v, buf), + } + } +} diff --git a/src/md/visitor/typesetting.rs b/src/md/visitor/typesetting.rs new file mode 100644 index 0000000..2405c03 --- /dev/null +++ b/src/md/visitor/typesetting.rs @@ -0,0 +1,85 @@ +use crate::md::panhelper; +use crate::md::typeset; +use crate::{Bindings, Style, Warnings}; + +use pandoc_ast::{Block, Inline, MutVisitor}; + +/// Visitor for the pandoc AST. +/// +/// This includes rendering stuff which we find as we go +pub struct TypesettingVisitor<'a> { + style: Style, + bindings: &'a Bindings, + warnings: Warnings, +} + +impl<'a> TypesettingVisitor<'a> { + pub fn new(style: Style, bindings: &'a Bindings) -> Self { + TypesettingVisitor { + style, + bindings, + warnings: Warnings::default(), + } + } + + pub fn warnings(self) -> Warnings { + self.warnings + } +} + +// Visit interesting parts of the Pandoc abstract syntax tree. The +// document top level is a vector of blocks and we visit that and +// replace any fenced code block with the scenario tag with a typeset +// paragraph. Also, replace fenced code blocks with known diagram +// markup with the rendered SVG image. +impl<'a> MutVisitor for TypesettingVisitor<'a> { + fn visit_vec_block(&mut self, vec_block: &mut Vec) { + use panhelper::is_class; + for block in vec_block { + match block { + Block::CodeBlock(attr, s) => { + if is_class(attr, "scenario") { + *block = typeset::scenario_snippet(self.bindings, s, &mut self.warnings) + } else if is_class(attr, "file") || is_class(attr, "example") { + *block = typeset::file_block(attr, s) + } else if is_class(attr, "dot") { + *block = typeset::dot_to_block(s, &mut self.warnings) + } else if is_class(attr, "plantuml") { + *block = typeset::plantuml_to_block(s, &mut self.warnings) + } else if is_class(attr, "roadmap") { + *block = typeset::roadmap_to_block(s, &mut self.warnings) + } else if is_class(attr, "pikchr") { + let other_classes: Vec<_> = attr + .1 + .iter() + .map(String::as_str) + .filter(|s| *s != "pikchr") + .collect(); + let class = if other_classes.is_empty() { + None + } else { + Some(other_classes.join(" ")) + }; + let class = class.as_deref(); + *block = typeset::pikchr_to_block(s, class, &mut self.warnings) + } + } + _ => { + self.visit_block(block); + } + } + } + } + fn visit_vec_inline(&mut self, vec_inline: &mut Vec) { + for inline in vec_inline { + match inline { + Inline::Link(attr, vec, target) if self.style.links_as_notes() => { + *inline = typeset::link_as_note(attr.clone(), vec.to_vec(), target.clone()); + } + _ => { + self.visit_inline(inline); + } + } + } + } +} diff --git a/src/panhelper.rs b/src/panhelper.rs deleted file mode 100644 index f7ab801..0000000 --- a/src/panhelper.rs +++ /dev/null @@ -1,26 +0,0 @@ -use pandoc_ast::Attr; - -/// Is a code block marked as being of a given type? -pub fn is_class(attr: &Attr, class: &str) -> bool { - let (_id, classes, _kvpairs) = attr; - classes.iter().any(|s| s == class) -} - -/// Utility function to find key/value pairs from an attribute -pub fn find_attr_kv<'a>(attr: &'a Attr, key: &'static str) -> impl Iterator { - attr.2.iter().flat_map(move |(key_, value)| { - if key == key_ { - Some(value.as_ref()) - } else { - None - } - }) -} - -/// Get the filename for a fenced code block tagged .file. -/// -/// The filename is the first (and presumably only) identifier for the -/// block. -pub fn get_filename(attr: &Attr) -> String { - attr.0.to_string() -} diff --git a/src/typeset.rs b/src/typeset.rs deleted file mode 100644 index f63206a..0000000 --- a/src/typeset.rs +++ /dev/null @@ -1,229 +0,0 @@ -use crate::parser::parse_scenario_snippet; -use crate::Bindings; -use crate::PartialStep; -use crate::ScenarioStep; -use crate::StepKind; -use crate::SubplotError; -use crate::{DiagramMarkup, DotMarkup, PikchrMarkup, PlantumlMarkup, Svg}; -use crate::{Warning, Warnings}; - -use pandoc_ast::Attr; -use pandoc_ast::Block; -use pandoc_ast::Inline; -use pandoc_ast::Target; - -/// Typeset an error as a Pandoc AST Block element. -pub fn error(err: SubplotError) -> Block { - let msg = format!("ERROR: {}", err); - Block::Para(error_msg(&msg)) -} - -/// Typeset an error message a vector of inlines. -pub fn error_msg(msg: &str) -> Vec { - vec![Inline::Strong(vec![inlinestr(msg)])] -} - -/// Typeset a string as an inline element. -pub fn inlinestr(s: &str) -> Inline { - Inline::Str(String::from(s)) -} - -/// Typeset a code block tagged as a file. -pub fn file_block(attr: &Attr, text: &str) -> Block { - let filename = inlinestr(&attr.0); - let filename = Inline::Strong(vec![filename]); - let intro = Block::Para(vec![inlinestr("File:"), space(), filename]); - let mut cbattrs = attr.clone(); - if cbattrs.1.iter().any(|s| s == "noNumberLines") { - // If the block says "noNumberLines" we remove that class - cbattrs.1.retain(|s| s != "noNumberLines"); - } else if cbattrs.1.iter().all(|s| s != "numberLines") { - // Otherwise if it doesn't say numberLines we add that in. - cbattrs.1.push("numberLines".to_string()); - } - // If this was an `example`, convert that class to `file` - if cbattrs.1.iter().any(|s| s == "example") { - cbattrs.1.retain(|s| s != "example"); - cbattrs.1.push("file".into()); - } - let codeblock = Block::CodeBlock(cbattrs, text.to_string()); - let noattr = ("".to_string(), vec![], vec![]); - Block::Div(noattr, vec![intro, codeblock]) -} - -/// Typeset a scenario snippet as a Pandoc AST Block. -/// -/// Typesetting here means producing the Pandoc abstract syntax tree -/// nodes that result in the desired output, when Pandoc processes -/// them. -/// -/// The snippet is given as a text string, which is parsed. It need -/// not be a complete scenario, but it should consist of complete steps. -pub fn scenario_snippet(bindings: &Bindings, snippet: &str, warnings: &mut Warnings) -> Block { - let lines = parse_scenario_snippet(snippet); - let mut steps = vec![]; - let mut prevkind: Option = None; - - for line in lines { - let (this, thiskind) = step(bindings, line, prevkind, warnings); - steps.push(this); - prevkind = thiskind; - } - Block::LineBlock(steps) -} - -// Typeset a single scenario step as a sequence of Pandoc AST Inlines. -fn step( - bindings: &Bindings, - text: &str, - prevkind: Option, - warnings: &mut Warnings, -) -> (Vec, Option) { - let step = ScenarioStep::new_from_str(text, prevkind); - if step.is_err() { - return ( - error_msg(&format!("Could not parse step: {}", text)), - prevkind, - ); - } - let step = step.unwrap(); - - let m = match bindings.find("", &step) { - Ok(m) => m, - Err(e) => { - let w = Warning::UnknownBinding(format!("{}", e)); - warnings.push(w.clone()); - return (error_msg(&format!("{}", w)), prevkind); - } - }; - - let mut inlines = vec![keyword(&step, prevkind), space()]; - - for part in m.parts() { - match part { - PartialStep::UncapturedText(s) => inlines.push(uncaptured(s.text())), - PartialStep::CapturedText { text, .. } => inlines.push(captured(text)), - } - } - - (inlines, Some(step.kind())) -} - -// Typeset first word, which is assumed to be a keyword, of a scenario -// step. -fn keyword(step: &ScenarioStep, prevkind: Option) -> Inline { - let actual = inlinestr(&format!("{}", step.kind())); - let and = inlinestr("and"); - let keyword = if let Some(prevkind) = prevkind { - if prevkind == step.kind() { - and - } else { - actual - } - } else { - actual - }; - Inline::Emph(vec![keyword]) -} - -// Typeset a space between words. -fn space() -> Inline { - Inline::Space -} - -// Typeset an uncaptured part of a step. -fn uncaptured(s: &str) -> Inline { - inlinestr(s) -} - -// Typeset a captured part of a step. -fn captured(s: &str) -> Inline { - Inline::Strong(vec![inlinestr(s)]) -} - -/// Typeset a link as a note. -pub fn link_as_note(attr: Attr, text: Vec, target: Target) -> Inline { - let (url, _) = target.clone(); - let url = Inline::Code(attr.clone(), url); - let link = Inline::Link(attr.clone(), vec![url], target); - let note = Inline::Note(vec![Block::Para(vec![link])]); - let mut text = text; - text.push(note); - Inline::Span(attr, text) -} - -/// Take a pikchr diagram, render it as SVG, and return an AST block element. -/// -/// The `Block` will contain the SVG data. This allows the diagram to -/// be rendered without referencing external entities. -/// -/// If the code block which contained the pikchr contains other classes, they -/// can be added to the SVG for use in later typesetting etc. -pub fn pikchr_to_block(pikchr: &str, class: Option<&str>, warnings: &mut Warnings) -> Block { - match PikchrMarkup::new(pikchr, class).as_svg() { - Ok(svg) => typeset_svg(svg), - Err(err) => { - warnings.push(Warning::Pikchr(format!("{}", err))); - error(err) - } - } -} - -// Take a dot diagram, render it as SVG, and return an AST Block -// element. The Block will contain the SVG data. This allows the -// diagram to be rendered without referending external entities. -pub fn dot_to_block(dot: &str, warnings: &mut Warnings) -> Block { - match DotMarkup::new(dot).as_svg() { - Ok(svg) => typeset_svg(svg), - Err(err) => { - warnings.push(Warning::Dot(format!("{}", err))); - error(err) - } - } -} - -// Take a PlantUML diagram, render it as SVG, and return an AST Block -// element. The Block will contain the SVG data. This allows the -// diagram to be rendered without referending external entities. -pub fn plantuml_to_block(markup: &str, warnings: &mut Warnings) -> Block { - match PlantumlMarkup::new(markup).as_svg() { - Ok(svg) => typeset_svg(svg), - Err(err) => { - warnings.push(Warning::Plantuml(format!("{}", err))); - error(err) - } - } -} - -/// Typeset a project roadmap expressed as textual YAML, and render it -/// as an SVG image. -pub fn roadmap_to_block(yaml: &str, warnings: &mut Warnings) -> Block { - match roadmap::from_yaml(yaml) { - Ok(ref mut roadmap) => { - roadmap.set_missing_statuses(); - let width = 50; - match roadmap.format_as_dot(width) { - Ok(dot) => dot_to_block(&dot, warnings), - Err(e) => Block::Para(vec![inlinestr(&e.to_string())]), - } - } - Err(e) => Block::Para(vec![inlinestr(&e.to_string())]), - } -} - -// Typeset an SVG, represented as a byte vector, as a Pandoc AST Block -// element. -fn typeset_svg(svg: Svg) -> Block { - let url = svg_as_data_url(svg); - let attr = ("".to_string(), vec![], vec![]); - let img = Inline::Image(attr, vec![], (url, "".to_string())); - Block::Para(vec![img]) -} - -// Convert an SVG, represented as a byte vector, into a data: URL, -// which can be inlined so the image can be rendered without -// referencing external files. -fn svg_as_data_url(svg: Svg) -> String { - let svg = base64::encode(svg.data()); - format!("data:image/svg+xml;base64,{}", svg) -} diff --git a/src/visitor/block_class.rs b/src/visitor/block_class.rs deleted file mode 100644 index 303616b..0000000 --- a/src/visitor/block_class.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::collections::HashSet; - -use pandoc_ast::{Block, MutVisitor}; - -#[derive(Default)] -pub struct BlockClassVisitor { - pub classes: HashSet, -} - -impl MutVisitor for BlockClassVisitor { - fn visit_vec_block(&mut self, vec_block: &mut Vec) { - for block in vec_block { - match block { - Block::CodeBlock(attr, _) => { - for class in &attr.1 { - self.classes.insert(class.to_string()); - } - } - _ => { - self.visit_block(block); - } - } - } - } -} diff --git a/src/visitor/embedded.rs b/src/visitor/embedded.rs deleted file mode 100644 index 891240b..0000000 --- a/src/visitor/embedded.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::panhelper; -use crate::EmbeddedFile; -use crate::EmbeddedFiles; - -use pandoc_ast::{Block, MutVisitor}; - -impl MutVisitor for EmbeddedFiles { - fn visit_vec_block(&mut self, vec_block: &mut Vec) { - use panhelper::is_class; - for block in vec_block { - match block { - Block::CodeBlock(attr, contents) => { - if is_class(attr, "file") { - let add_newline = match panhelper::find_attr_kv(attr, "add-newline").next() - { - None | Some("auto") => !contents.ends_with('\n'), - Some("yes") => true, - Some("no") => false, - _ => unreachable!(), - }; - let contents = if add_newline { - format!("{}\n", contents) - } else { - contents.clone() - }; - self.push(EmbeddedFile::new(panhelper::get_filename(attr), contents)); - } - } - _ => { - self.visit_block(block); - } - } - } - } -} diff --git a/src/visitor/image.rs b/src/visitor/image.rs deleted file mode 100644 index be49d66..0000000 --- a/src/visitor/image.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::path::PathBuf; - -use pandoc_ast::{Inline, MutVisitor}; - -pub struct ImageVisitor { - images: Vec, -} - -impl ImageVisitor { - pub fn new() -> Self { - ImageVisitor { images: vec![] } - } - - pub fn images(&self) -> Vec { - self.images.clone() - } -} - -impl MutVisitor for ImageVisitor { - fn visit_inline(&mut self, inline: &mut Inline) { - if let Inline::Image(_attr, _inlines, target) = inline { - self.images.push(PathBuf::from(&target.0)); - } - } -} diff --git a/src/visitor/linting.rs b/src/visitor/linting.rs deleted file mode 100644 index 6266516..0000000 --- a/src/visitor/linting.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::panhelper; -use crate::SubplotError; - -use pandoc_ast::{Block, MutVisitor}; - -#[derive(Default)] -pub struct LintingVisitor { - pub issues: Vec, -} - -impl MutVisitor for LintingVisitor { - fn visit_vec_block(&mut self, vec_block: &mut Vec) { - for block in vec_block { - match block { - Block::CodeBlock(attr, _) => { - if panhelper::is_class(attr, "file") || panhelper::is_class(attr, "example") { - let newlines: Vec<_> = - panhelper::find_attr_kv(attr, "add-newline").collect(); - match newlines.len() { - 0 => {} - 1 => match newlines[0].to_ascii_lowercase().as_ref() { - "auto" | "yes" | "no" => {} - _ => self.issues.push(SubplotError::UnrecognisedAddNewline( - panhelper::get_filename(attr), - newlines[0].to_owned(), - )), - }, - _ => self.issues.push(SubplotError::RepeatedAddNewlineAttribute( - panhelper::get_filename(attr), - )), - } - } - } - _ => { - self.visit_block(block); - } - } - } - } -} diff --git a/src/visitor/mod.rs b/src/visitor/mod.rs deleted file mode 100644 index 1c095ac..0000000 --- a/src/visitor/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod block_class; -pub use block_class::BlockClassVisitor; - -mod embedded; - -mod image; -pub use image::ImageVisitor; - -mod linting; -pub use linting::LintingVisitor; - -mod structure; -pub use structure::Element; -pub use structure::StructureVisitor; - -mod typesetting; -pub use typesetting::TypesettingVisitor; diff --git a/src/visitor/structure.rs b/src/visitor/structure.rs deleted file mode 100644 index f5693a6..0000000 --- a/src/visitor/structure.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::panhelper; - -use pandoc_ast::{Block, Inline, MutVisitor}; - -// A structure element in the document: a heading or a scenario snippet. -#[derive(Debug)] -pub enum Element { - // Headings consist of the text and the level of the heading. - Heading(String, i64), - - // Scenario snippets consist just of the unparsed text. - Snippet(String), -} - -impl Element { - pub fn heading(text: &str, level: i64) -> Element { - Element::Heading(text.to_string(), level) - } - - pub fn snippet(text: &str) -> Element { - Element::Snippet(text.to_string()) - } -} - -// A MutVisitor for extracting document structure. -pub struct StructureVisitor { - pub elements: Vec, -} - -impl StructureVisitor { - pub fn new() -> Self { - Self { elements: vec![] } - } -} - -impl MutVisitor for StructureVisitor { - fn visit_vec_block(&mut self, vec_block: &mut Vec) { - use panhelper::is_class; - for block in vec_block { - match block { - Block::Header(level, _attr, inlines) => { - let text = join(inlines); - let heading = Element::heading(&text, *level); - self.elements.push(heading); - } - Block::CodeBlock(attr, s) => { - if is_class(attr, "scenario") { - let snippet = Element::snippet(s); - self.elements.push(snippet); - } - } - _ => { - self.visit_block(block); - } - } - } - } -} - -fn join(vec: &[Inline]) -> String { - let mut buf = String::new(); - join_into_buffer(vec, &mut buf); - buf -} - -fn join_into_buffer(vec: &[Inline], buf: &mut String) { - for item in vec { - match item { - Inline::Str(s) => buf.push_str(s), - Inline::Emph(v) => join_into_buffer(v, buf), - Inline::Strong(v) => join_into_buffer(v, buf), - Inline::Strikeout(v) => join_into_buffer(v, buf), - Inline::Superscript(v) => join_into_buffer(v, buf), - Inline::Subscript(v) => join_into_buffer(v, buf), - Inline::SmallCaps(v) => join_into_buffer(v, buf), - Inline::Quoted(qt, v) => { - let q = match qt { - pandoc_ast::QuoteType::SingleQuote => "'", - pandoc_ast::QuoteType::DoubleQuote => "\"", - }; - buf.push_str(q); - join_into_buffer(v, buf); - buf.push_str(q); - } - Inline::Cite(_, v) => join_into_buffer(v, buf), - Inline::Code(_attr, s) => buf.push_str(s), - Inline::Space => buf.push(' '), - Inline::SoftBreak => buf.push(' '), - Inline::LineBreak => buf.push(' '), - Inline::Math(_, s) => buf.push_str(s), - Inline::RawInline(_, s) => buf.push_str(s), - Inline::Link(_, v, _) => join_into_buffer(v, buf), - Inline::Image(_, v, _) => join_into_buffer(v, buf), - Inline::Note(_) => buf.push_str(""), - Inline::Span(_attr, v) => join_into_buffer(v, buf), - #[cfg(feature = "pandoc_ast_08")] - Inline::Underline(v) => join_into_buffer(v, buf), - } - } -} diff --git a/src/visitor/typesetting.rs b/src/visitor/typesetting.rs deleted file mode 100644 index da9c362..0000000 --- a/src/visitor/typesetting.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::panhelper; -use crate::typeset; -use crate::{Bindings, Style, Warnings}; - -use pandoc_ast::{Block, Inline, MutVisitor}; - -/// Visitor for the pandoc AST. -/// -/// This includes rendering stuff which we find as we go -pub struct TypesettingVisitor<'a> { - style: Style, - bindings: &'a Bindings, - warnings: Warnings, -} - -impl<'a> TypesettingVisitor<'a> { - pub fn new(style: Style, bindings: &'a Bindings) -> Self { - TypesettingVisitor { - style, - bindings, - warnings: Warnings::default(), - } - } - - pub fn warnings(self) -> Warnings { - self.warnings - } -} - -// Visit interesting parts of the Pandoc abstract syntax tree. The -// document top level is a vector of blocks and we visit that and -// replace any fenced code block with the scenario tag with a typeset -// paragraph. Also, replace fenced code blocks with known diagram -// markup with the rendered SVG image. -impl<'a> MutVisitor for TypesettingVisitor<'a> { - fn visit_vec_block(&mut self, vec_block: &mut Vec) { - use panhelper::is_class; - for block in vec_block { - match block { - Block::CodeBlock(attr, s) => { - if is_class(attr, "scenario") { - *block = typeset::scenario_snippet(self.bindings, s, &mut self.warnings) - } else if is_class(attr, "file") || is_class(attr, "example") { - *block = typeset::file_block(attr, s) - } else if is_class(attr, "dot") { - *block = typeset::dot_to_block(s, &mut self.warnings) - } else if is_class(attr, "plantuml") { - *block = typeset::plantuml_to_block(s, &mut self.warnings) - } else if is_class(attr, "roadmap") { - *block = typeset::roadmap_to_block(s, &mut self.warnings) - } else if is_class(attr, "pikchr") { - let other_classes: Vec<_> = attr - .1 - .iter() - .map(String::as_str) - .filter(|s| *s != "pikchr") - .collect(); - let class = if other_classes.is_empty() { - None - } else { - Some(other_classes.join(" ")) - }; - let class = class.as_deref(); - *block = typeset::pikchr_to_block(s, class, &mut self.warnings) - } - } - _ => { - self.visit_block(block); - } - } - } - } - fn visit_vec_inline(&mut self, vec_inline: &mut Vec) { - for inline in vec_inline { - match inline { - Inline::Link(attr, vec, target) if self.style.links_as_notes() => { - *inline = typeset::link_as_note(attr.clone(), vec.to_vec(), target.clone()); - } - _ => { - self.visit_inline(inline); - } - } - } - } -} -- cgit v1.2.1 From 4892ab97637c3727834248aba19b0407439c1f5f Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 28 Jan 2023 19:56:11 +0200 Subject: refactor: only mutating methods on Markdown require mutable self Use RefCell's interior mutability to work around the fact that pandoc_ast's MutVisitor requires a mutable reference to self. Sponsored-by: author --- src/doc.rs | 2 +- src/md.rs | 38 +++++++++++++++++--------------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/doc.rs b/src/doc.rs index a39ab99..e860726 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -105,7 +105,7 @@ impl Document { subplot: PathBuf, markdowns: Vec, yamlmeta: &YamlMetadata, - mut md: Markdown, + md: Markdown, style: Style, template: Option<&str>, ) -> Result diff --git a/src/md.rs b/src/md.rs index 6f00ab7..59fdc1e 100644 --- a/src/md.rs +++ b/src/md.rs @@ -7,6 +7,7 @@ use crate::{ use log::trace; use pandoc_ast::{Map, MetaValue, MutVisitor, Pandoc}; use serde_yaml::{Mapping, Value}; +use std::cell::RefCell; use std::collections::HashSet; use std::convert::TryFrom; use std::path::{Path, PathBuf}; @@ -20,21 +21,19 @@ use visitor::LintingVisitor; /// A parsed Markdown document. #[derive(Debug)] pub struct Markdown { - pandoc: Pandoc, + pandoc: RefCell, } impl Markdown { fn new(pandoc: Pandoc) -> Self { - Self { pandoc } - } - - fn pandoc(&mut self) -> &mut Pandoc { - &mut self.pandoc + Self { + pandoc: RefCell::new(pandoc), + } } /// Set document metadata from subplot. pub fn set_metadata(&mut self, meta: &YamlMetadata) { - self.pandoc.meta = to_pandoc_meta(meta); + self.pandoc.borrow_mut().meta = to_pandoc_meta(meta); } /// JSON representation of Pandoc AST. @@ -44,17 +43,17 @@ impl Markdown { } /// Find problems. - pub fn lint(&mut self) -> Vec { + pub fn lint(&self) -> Vec { let mut linter = LintingVisitor::default(); - linter.walk_pandoc(self.pandoc()); + linter.walk_pandoc(&mut self.pandoc.borrow_mut()); linter.issues } /// Find included images. - pub fn images(&mut self) -> Vec { + pub fn images(&self) -> Vec { let mut names = vec![]; let mut visitor = visitor::ImageVisitor::new(); - visitor.walk_pandoc(self.pandoc()); + visitor.walk_pandoc(&mut self.pandoc.borrow_mut()); for x in visitor.images().iter() { names.push(x.to_path_buf()); } @@ -62,31 +61,28 @@ impl Markdown { } /// Find classes used for fenced blocks. - pub fn block_classes(&mut self) -> HashSet { + pub fn block_classes(&self) -> HashSet { let mut visitor = visitor::BlockClassVisitor::default(); - // Irritatingly we can't immutably visit the AST for some reason - // This clone() is expensive and unwanted, but I'm not sure how - // to get around it for now - visitor.walk_pandoc(self.pandoc()); + visitor.walk_pandoc(&mut self.pandoc.borrow_mut()); visitor.classes } /// Typeset. pub fn typeset(&mut self, style: Style, bindings: &Bindings) -> Vec { let mut visitor = visitor::TypesettingVisitor::new(style, bindings); - visitor.walk_pandoc(self.pandoc()); + visitor.walk_pandoc(&mut self.pandoc.borrow_mut()); visitor.warnings().warnings().to_vec() } /// Find scenarios. - pub fn scenarios(&mut self) -> Result, SubplotError> { + pub fn scenarios(&self) -> Result, SubplotError> { trace!( "Metadata::scenarios: looking for scenarios: {:#?}", self.pandoc ); let mut visitor = visitor::StructureVisitor::new(); - visitor.walk_pandoc(self.pandoc()); + visitor.walk_pandoc(&mut self.pandoc.borrow_mut()); trace!( "Metadata::scenarios: visitor found {} elements: {:#?}", visitor.elements.len(), @@ -108,9 +104,9 @@ impl Markdown { } /// Find embedded files. - pub fn embedded_files(&mut self) -> EmbeddedFiles { + pub fn embedded_files(&self) -> EmbeddedFiles { let mut files = EmbeddedFiles::default(); - files.walk_pandoc(self.pandoc()); + files.walk_pandoc(&mut self.pandoc.borrow_mut()); files } } -- cgit v1.2.1 From b160ad5b9f0e38859b0a6d3c3262afbb39067584 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 28 Jan 2023 20:01:53 +0200 Subject: refactor: turn loading Markdown from a file into a constructor Previously, we used the TryFrom<&Path> trait, but it's not clear that converting a pathname into a Markdown value means reading it. An associated function that reads a file is clearer. Sponsored-by: author --- src/doc.rs | 3 +-- src/md.rs | 52 ++++++++++++++++++++++++---------------------------- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/doc.rs b/src/doc.rs index e860726..faa3a84 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -13,7 +13,6 @@ use crate::{Metadata, YamlMetadata}; use crate::{Warning, Warnings}; use std::collections::HashSet; -use std::convert::TryFrom; use std::default::Default; use std::fmt::Debug; use std::fs::read; @@ -147,7 +146,7 @@ impl Document { let mdfile = meta.markdown(); let mdfile = basedir.join(mdfile); - let mut md = Markdown::try_from(mdfile.as_path())?; + let mut md = Markdown::load_file(mdfile.as_path())?; md.set_metadata(&meta); let markdowns = vec![mdfile]; diff --git a/src/md.rs b/src/md.rs index 59fdc1e..f43b462 100644 --- a/src/md.rs +++ b/src/md.rs @@ -9,7 +9,6 @@ use pandoc_ast::{Map, MetaValue, MutVisitor, Pandoc}; use serde_yaml::{Mapping, Value}; use std::cell::RefCell; use std::collections::HashSet; -use std::convert::TryFrom; use std::path::{Path, PathBuf}; mod panhelper; @@ -25,6 +24,30 @@ pub struct Markdown { } impl Markdown { + /// Load a Markdown file. + pub fn load_file(filename: &Path) -> Result { + trace!("parsing file as markdown: {}", filename.display()); + let mut pandoc = pandoc::new(); + pandoc.add_input(&filename); + pandoc.set_input_format( + pandoc::InputFormat::Markdown, + vec![pandoc::MarkdownExtension::Citations], + ); + pandoc.set_output_format(pandoc::OutputFormat::Json, vec![]); + pandoc.set_output(pandoc::OutputKind::Pipe); + + // Add external Pandoc filters. + crate::policy::add_citeproc(&mut pandoc); + + let json = match pandoc.execute().map_err(SubplotError::Pandoc)? { + pandoc::PandocOutput::ToBuffer(o) => o, + _ => return Err(SubplotError::NotJson), + }; + + let ast: Pandoc = serde_json::from_str(&json).map_err(SubplotError::AstJson)?; + Ok(Self::new(ast)) + } + fn new(pandoc: Pandoc) -> Self { Self { pandoc: RefCell::new(pandoc), @@ -211,33 +234,6 @@ fn meta_path_bufs(v: &[PathBuf]) -> MetaValue { MetaValue::MetaList(v.iter().map(|p| meta_path_buf(p)).collect()) } -impl TryFrom<&Path> for Markdown { - type Error = SubplotError; - - fn try_from(filename: &Path) -> Result { - trace!("parsing file as markdown: {}", filename.display()); - let mut pandoc = pandoc::new(); - pandoc.add_input(&filename); - pandoc.set_input_format( - pandoc::InputFormat::Markdown, - vec![pandoc::MarkdownExtension::Citations], - ); - pandoc.set_output_format(pandoc::OutputFormat::Json, vec![]); - pandoc.set_output(pandoc::OutputKind::Pipe); - - // Add external Pandoc filters. - crate::policy::add_citeproc(&mut pandoc); - - let json = match pandoc.execute().map_err(SubplotError::Pandoc)? { - pandoc::PandocOutput::ToBuffer(o) => o, - _ => return Err(SubplotError::NotJson), - }; - - let ast: Pandoc = serde_json::from_str(&json).map_err(SubplotError::AstJson)?; - Ok(Self::new(ast)) - } -} - fn extract_scenario(e: &[visitor::Element]) -> Result<(Option, usize), SubplotError> { if e.is_empty() { // If we get here, it's a programming error. -- cgit v1.2.1