diff options
Diffstat (limited to 'src/md/visitor')
-rw-r--r-- | src/md/visitor/block_class.rs | 25 | ||||
-rw-r--r-- | src/md/visitor/embedded.rs | 35 | ||||
-rw-r--r-- | src/md/visitor/image.rs | 25 | ||||
-rw-r--r-- | src/md/visitor/linting.rs | 40 | ||||
-rw-r--r-- | src/md/visitor/mod.rs | 17 | ||||
-rw-r--r-- | src/md/visitor/structure.rs | 100 | ||||
-rw-r--r-- | src/md/visitor/typesetting.rs | 85 |
7 files changed, 327 insertions, 0 deletions
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<String>, +} + +impl MutVisitor for BlockClassVisitor { + fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) { + 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<Block>) { + 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<PathBuf>, +} + +impl ImageVisitor { + pub fn new() -> Self { + ImageVisitor { images: vec![] } + } + + pub fn images(&self) -> Vec<PathBuf> { + 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<SubplotError>, +} + +impl MutVisitor for LintingVisitor { + fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) { + 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<Element>, +} + +impl StructureVisitor { + pub fn new() -> Self { + Self { elements: vec![] } + } +} + +impl MutVisitor for StructureVisitor { + fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) { + 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<Block>) { + 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<Inline>) { + 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); + } + } + } + } +} |