summaryrefslogtreecommitdiff
path: root/src/md/visitor
diff options
context:
space:
mode:
Diffstat (limited to 'src/md/visitor')
-rw-r--r--src/md/visitor/block_class.rs25
-rw-r--r--src/md/visitor/embedded.rs35
-rw-r--r--src/md/visitor/image.rs25
-rw-r--r--src/md/visitor/linting.rs40
-rw-r--r--src/md/visitor/mod.rs17
-rw-r--r--src/md/visitor/structure.rs100
-rw-r--r--src/md/visitor/typesetting.rs85
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);
+ }
+ }
+ }
+ }
+}