diff options
author | Lars Wirzenius <liw@liw.fi> | 2020-08-08 18:43:57 +0300 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2020-08-08 20:47:22 +0300 |
commit | d1651321ec1cd1e02fb93fe6e31ab4de115356c9 (patch) | |
tree | f5966966f0cb910659a62afa63c7f089913e9937 | |
parent | c3e8c88f3294338e8ea4678fb5493c96150a4e3c (diff) | |
download | subplot-d1651321ec1cd1e02fb93fe6e31ab4de115356c9.tar.gz |
refactor: split stuff from src/ast.rs into smaller modules
This only moves things around, to avoid huge source code modules. It
doesn't rename functions, add unit tests, or similar.
* src/datafiles.rs: DataFile, DataFiles
* src/metata.rs: Metadata
* src/panhelper.rs: functions for querying Pandoc Attrs
* src/policy.rs: the get_basedir_from function; place for later policy
functions
* src/typeset.rs: functions to produce Pandoc AST nodes
* srv/visitor/*: various MutVisitor implementations for traversing
ASTs, and their helper functions
-rw-r--r-- | src/ast.rs | 749 | ||||
-rw-r--r-- | src/datafiles.rs | 51 | ||||
-rw-r--r-- | src/lib.rs | 18 | ||||
-rw-r--r-- | src/metadata.rs | 277 | ||||
-rw-r--r-- | src/panhelper.rs | 26 | ||||
-rw-r--r-- | src/policy.rs | 15 | ||||
-rw-r--r-- | src/typeset.rs | 181 | ||||
-rw-r--r-- | src/visitor/block_class.rs | 25 | ||||
-rw-r--r-- | src/visitor/datafiles.rs | 35 | ||||
-rw-r--r-- | src/visitor/image.rs | 25 | ||||
-rw-r--r-- | src/visitor/linting.rs | 40 | ||||
-rw-r--r-- | src/visitor/mod.rs | 17 | ||||
-rw-r--r-- | src/visitor/structure.rs | 82 | ||||
-rw-r--r-- | src/visitor/typesetting.rs | 49 |
14 files changed, 855 insertions, 735 deletions
@@ -1,19 +1,19 @@ use crate::parser::parse_scenario_snippet; -use crate::Bindings; +use crate::visitor; +use crate::DataFile; +use crate::DataFiles; +use crate::LintingVisitor; use crate::MatchedScenario; -use crate::PartialStep; +use crate::Metadata; use crate::Scenario; use crate::ScenarioStep; -use crate::StepKind; -use crate::{DotMarkup, GraphMarkup, PlantumlMarkup}; use crate::{Result, SubplotError}; use std::collections::HashSet; use std::ops::Deref; use std::path::{Path, PathBuf}; -use pandoc_ast::{Attr, Block, Inline, Map, MetaValue, MutVisitor, Pandoc}; -use serde::{Deserialize, Serialize}; +use pandoc_ast::{MutVisitor, Pandoc}; /// The set of known (special) classes which subplot will always recognise /// as being valid. @@ -165,7 +165,7 @@ impl<'a> Document { names.push(x.to_path_buf()); } - let mut visitor = ImageVisitor::new(); + let mut visitor = visitor::ImageVisitor::new(); visitor.walk_pandoc(&mut self.ast); for x in visitor.images().iter() { names.push(x.to_path_buf()); @@ -210,7 +210,7 @@ impl<'a> Document { /// Check that all the block classes in the document are known fn check_block_classes(&self) -> Result<()> { - let mut visitor = BlockClassVisitor::default(); + 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 @@ -242,13 +242,13 @@ impl<'a> Document { /// Typeset a Subplot document. pub fn typeset(&mut self) { - let mut visitor = TypesettingVisitor::new(&self.meta.bindings); + let mut visitor = visitor::TypesettingVisitor::new(&self.meta.bindings()); visitor.walk_pandoc(&mut self.ast); } /// Return all scenarios in a document. pub fn scenarios(&mut self) -> Result<Vec<Scenario>> { - let mut visitor = StructureVisitor::new(); + let mut visitor = visitor::StructureVisitor::new(); visitor.walk_pandoc(&mut self.ast); let mut scenarios: Vec<Scenario> = vec![]; @@ -276,20 +276,20 @@ impl<'a> Document { } } -fn extract_scenario(e: &[Element]) -> Result<(Option<Scenario>, usize)> { +fn extract_scenario(e: &[visitor::Element]) -> Result<(Option<Scenario>, usize)> { if e.is_empty() { // If we get here, it's a programming error. panic!("didn't expect empty list of elements"); } match &e[0] { - Element::Snippet(_) => Err(SubplotError::ScenarioBeforeHeading), - Element::Heading(title, level) => { + visitor::Element::Snippet(_) => Err(SubplotError::ScenarioBeforeHeading), + visitor::Element::Heading(title, level) => { let mut scen = Scenario::new(&title); let mut prevkind = None; for (i, item) in e.iter().enumerate().skip(1) { match item { - Element::Heading(_, level2) => { + visitor::Element::Heading(_, level2) => { let is_subsection = *level2 > *level; if is_subsection { if scen.has_steps() { @@ -302,7 +302,7 @@ fn extract_scenario(e: &[Element]) -> Result<(Option<Scenario>, usize)> { return Ok((None, i)); } } - Element::Snippet(text) => { + visitor::Element::Snippet(text) => { for line in parse_scenario_snippet(&text) { let step = ScenarioStep::new_from_str(line, prevkind)?; scen.add(&step); @@ -323,7 +323,7 @@ fn extract_scenario(e: &[Element]) -> Result<(Option<Scenario>, usize)> { #[cfg(test)] mod test_extract { use super::extract_scenario; - use super::Element; + use super::visitor::Element; use crate::Result; use crate::Scenario; use crate::SubplotError; @@ -426,720 +426,3 @@ mod test_extract { } } } - -/// Metadata of a document, as needed by Subplot. -#[derive(Debug)] -pub struct Metadata { - title: String, - date: Option<String>, - bindings_filenames: Vec<PathBuf>, - bindings: Bindings, - functions_filenames: Vec<PathBuf>, - template: Option<String>, - bibliographies: Vec<PathBuf>, - /// Extra class names which should be considered 'correct' for this document - classes: Vec<String>, -} - -impl Metadata { - /// Construct a Metadata from a Document, if possible. - pub fn new<P>(basedir: P, doc: &Pandoc) -> Result<Metadata> - where - P: AsRef<Path>, - { - let title = get_title(&doc.meta)?; - let date = get_date(&doc.meta); - let bindings_filenames = get_bindings_filenames(basedir.as_ref(), &doc.meta); - let functions_filenames = get_functions_filenames(basedir.as_ref(), &doc.meta); - let template = get_template_name(&doc.meta)?; - let mut bindings = Bindings::new(); - - let bibliographies = get_bibliographies(basedir.as_ref(), &doc.meta); - let classes = get_classes(&doc.meta); - - get_bindings(&bindings_filenames, &mut bindings)?; - Ok(Metadata { - title, - date, - bindings_filenames, - bindings, - functions_filenames, - template, - bibliographies, - classes, - }) - } - - /// Return title of document. - pub fn title(&self) -> &str { - &self.title - } - - /// Return date of document, if any. - pub fn date(&self) -> Option<&str> { - if let Some(date) = &self.date { - Some(&date) - } else { - None - } - } - - /// Return filename where bindings are specified. - pub fn bindings_filenames(&self) -> Vec<&Path> { - self.bindings_filenames.iter().map(|f| f.as_ref()).collect() - } - - /// Return filename where functions are specified. - pub fn functions_filenames(&self) -> Vec<&Path> { - self.functions_filenames - .iter() - .map(|f| f.as_ref()) - .collect() - } - - /// Return the name of the code template, if specified. - pub fn template_name(&self) -> Option<&str> { - match &self.template { - Some(x) => Some(&x), - None => None, - } - } - - /// Return the bindings. - pub fn bindings(&self) -> &Bindings { - &self.bindings - } - - /// Return the bibliographies. - pub fn bibliographies(&self) -> Vec<&Path> { - self.bibliographies.iter().map(|x| x.as_path()).collect() - } - - /// The classes which this document also claims are valid - pub fn classes(&self) -> impl Iterator<Item = &str> { - self.classes.iter().map(Deref::deref) - } -} - -type Mapp = Map<String, MetaValue>; - -fn get_title(map: &Mapp) -> Result<String> { - if let Some(s) = get_string(map, "title") { - Ok(s) - } else { - Ok("".to_string()) - } -} - -fn get_date(map: &Mapp) -> Option<String> { - if let Some(s) = get_string(map, "date") { - Some(s) - } else { - None - } -} - -fn get_bindings_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf> -where - P: AsRef<Path>, -{ - get_paths(basedir, map, "bindings") -} - -fn get_functions_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf> -where - P: AsRef<Path>, -{ - get_paths(basedir, map, "functions") -} - -fn get_template_name(map: &Mapp) -> Result<Option<String>> { - match get_string(map, "template") { - Some(s) => Ok(Some(s)), - None => Ok(None), - } -} - -fn get_paths<P>(basedir: P, map: &Mapp, field: &str) -> Vec<PathBuf> -where - P: AsRef<Path>, -{ - match map.get(field) { - None => vec![], - Some(v) => pathbufs(basedir, v), - } -} - -fn get_string(map: &Mapp, field: &str) -> Option<String> { - let v = match map.get(field) { - None => return None, - Some(s) => s, - }; - let v = match v { - pandoc_ast::MetaValue::MetaString(s) => s.to_string(), - pandoc_ast::MetaValue::MetaInlines(vec) => join(&vec), - _ => panic!("don't know how to handle: {:?}", v), - }; - Some(v) -} - -fn get_bibliographies<P>(basedir: P, map: &Mapp) -> Vec<PathBuf> -where - P: AsRef<Path>, -{ - let v = match map.get("bibliography") { - None => return vec![], - Some(s) => s, - }; - pathbufs(basedir, v) -} - -fn pathbufs<P>(basedir: P, v: &MetaValue) -> Vec<PathBuf> -where - P: AsRef<Path>, -{ - let mut bufs = vec![]; - push_pathbufs(basedir, v, &mut bufs); - bufs -} - -fn get_classes(map: &Mapp) -> Vec<String> { - let mut ret = Vec::new(); - if let Some(classes) = map.get("classes") { - push_strings(classes, &mut ret); - } - ret -} - -fn push_strings(v: &MetaValue, strings: &mut Vec<String>) { - match v { - MetaValue::MetaString(s) => strings.push(s.to_string()), - MetaValue::MetaInlines(vec) => strings.push(join(&vec)), - MetaValue::MetaList(values) => { - for value in values { - push_strings(value, strings); - } - } - _ => panic!("don't know how to handle: {:?}", v), - }; -} - -fn push_pathbufs<P>(basedir: P, v: &MetaValue, bufs: &mut Vec<PathBuf>) -where - P: AsRef<Path>, -{ - match v { - MetaValue::MetaString(s) => bufs.push(basedir.as_ref().join(Path::new(s))), - MetaValue::MetaInlines(vec) => bufs.push(basedir.as_ref().join(Path::new(&join(&vec)))), - MetaValue::MetaList(values) => { - for value in values { - push_pathbufs(basedir.as_ref(), value, bufs); - } - } - _ => panic!("don't know how to handle: {:?}", v), - }; -} - -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 { - pandoc_ast::Inline::Str(s) => buf.push_str(&s), - pandoc_ast::Inline::Emph(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::Strong(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::Strikeout(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::Superscript(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::Subscript(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::SmallCaps(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::Space => buf.push_str(" "), - pandoc_ast::Inline::SoftBreak => buf.push_str(" "), - pandoc_ast::Inline::LineBreak => buf.push_str(" "), - _ => panic!("unknown pandoc_ast::Inline component {:?}", item), - } - } -} - -#[cfg(test)] -mod test_join { - use super::join; - use pandoc_ast::Inline; - - #[test] - fn join_all_kinds() { - let v = vec![ - Inline::Str("a".to_string()), - Inline::Emph(vec![Inline::Str("b".to_string())]), - Inline::Strong(vec![Inline::Str("c".to_string())]), - Inline::Strikeout(vec![Inline::Str("d".to_string())]), - Inline::Superscript(vec![Inline::Str("e".to_string())]), - Inline::Subscript(vec![Inline::Str("f".to_string())]), - Inline::SmallCaps(vec![Inline::Str("g".to_string())]), - Inline::Space, - Inline::SoftBreak, - Inline::LineBreak, - ]; - assert_eq!(join(&v), "abcdefg "); - } -} - -fn get_bindings<P>(filenames: &[P], bindings: &mut Bindings) -> Result<()> -where - P: AsRef<Path>, -{ - for filename in filenames { - bindings.add_from_file(filename)?; - } - Ok(()) -} - -/// Visitor for the pandoc AST. -/// -/// This includes rendering stuff which we find as we go -struct TypesettingVisitor<'a> { - bindings: &'a Bindings, -} - -impl<'a> TypesettingVisitor<'a> { - fn new(bindings: &'a Bindings) -> Self { - TypesettingVisitor { bindings } - } -} - -// 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 graph markup -// with the rendered SVG image. -impl<'a> MutVisitor for TypesettingVisitor<'a> { - fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) { - for block in vec_block { - match block { - Block::CodeBlock(attr, s) => { - if is_class(attr, "scenario") { - *block = scenario_snippet(&self.bindings, s) - } else if is_class(attr, "file") { - *block = file_block(attr, s) - } else if is_class(attr, "dot") { - *block = dot_to_block(s) - } else if is_class(attr, "plantuml") { - *block = plantuml_to_block(s) - } else if is_class(attr, "roadmap") { - *block = roadmap_to_block(s) - } - } - _ => { - self.visit_block(block); - } - } - } - } -} - -// Is a code block marked as being of a given type? -fn is_class(attr: &Attr, class: &str) -> bool { - let (_id, classes, _kvpairs) = attr; - classes.iter().any(|s| s == class) -} - -/// Typeset an error as a Pandoc AST Block element. -pub fn error(err: SubplotError) -> Block { - let msg = format!("ERROR: {}", err.to_string()); - Block::Para(error_msg(&msg)) -} - -// Typeset an error message a vector of inlines. -pub fn error_msg(msg: &str) -> Vec<Inline> { - vec![Inline::Strong(vec![inlinestr(msg)])] -} - -/// 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 codeblock = Block::CodeBlock(attr.clone(), 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) -> Block { - let lines = parse_scenario_snippet(snippet); - let mut steps = vec![]; - let mut prevkind: Option<StepKind> = None; - - for line in lines { - let (this, thiskind) = step(bindings, line, prevkind); - 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, - defkind: Option<StepKind>, -) -> (Vec<Inline>, Option<StepKind>) { - let step = ScenarioStep::new_from_str(text, defkind); - if step.is_err() { - return ( - error_msg(&format!("Could not parse step: {}", text)), - defkind, - ); - } - let step = step.unwrap(); - - let m = match bindings.find(&step) { - Ok(m) => m, - Err(e) => { - eprintln!("Could not select binding: {:?}", e); - return ( - error_msg(&format!("Could not select binding for: {}", text)), - defkind, - ); - } - }; - - let mut inlines = Vec::new(); - - inlines.push(keyword(&step)); - inlines.push(space()); - - for part in m.parts() { - #[allow(unused_variables)] - match part { - PartialStep::UncapturedText(s) => inlines.push(uncaptured(s.text())), - PartialStep::CapturedText { name, 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) -> Inline { - let word = inlinestr(step.keyword()); - Inline::Emph(vec![word]) -} - -fn inlinestr(s: &str) -> Inline { - Inline::Str(String::from(s)) -} - -// 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)]) -} - -// Take a dot graph, render it as SVG, and return an AST Block -// element. The Block will contain the SVG data. This allows the graph -// to be rendered without referending external entities. -fn dot_to_block(dot: &str) -> Block { - match DotMarkup::new(dot).as_svg() { - Ok(svg) => typeset_svg(svg), - Err(err) => { - eprintln!("dot failed: {}", err); - error(err) - } - } -} - -// Take a PlantUML graph, render it as SVG, and return an AST Block -// element. The Block will contain the SVG data. This allows the graph -// to be rendered without referending external entities. -fn plantuml_to_block(markup: &str) -> Block { - match PlantumlMarkup::new(markup).as_svg() { - Ok(svg) => typeset_svg(svg), - Err(err) => { - eprintln!("plantuml failed: {}", err); - error(err) - } - } -} - -/// Typeset a project roadmap expressed as textual YAML, and render it -/// as an SVG image. -fn roadmap_to_block(yaml: &str) -> 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), - 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: Vec<u8>) -> 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: Vec<u8>) -> String { - let svg = base64::encode(&svg); - format!("data:image/svg+xml;base64,{}", svg) -} - -// A structure element in the document: a heading or a scenario snippet. -#[derive(Debug)] -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. -struct StructureVisitor { - 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>) { - 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); - } - } - } - } -} - -/// A data file embedded in the document. -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] -pub struct DataFile { - filename: String, - contents: String, -} - -impl DataFile { - fn new(filename: String, contents: String) -> DataFile { - DataFile { filename, contents } - } - - /// Return name of embedded file. - pub fn filename(&self) -> &str { - &self.filename - } - - /// Return contents of embedded file. - pub fn contents(&self) -> &str { - &self.contents - } -} - -/// A collection of data files embedded in document. -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] -struct DataFiles { - files: Vec<DataFile>, -} - -impl DataFiles { - fn new(ast: &mut Pandoc) -> DataFiles { - let mut files = DataFiles { files: vec![] }; - files.walk_pandoc(ast); - files - } - - fn files(&self) -> &[DataFile] { - &self.files - } -} - -impl MutVisitor for DataFiles { - fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) { - for block in vec_block { - match block { - Block::CodeBlock(attr, contents) => { - if is_class(attr, "file") { - let add_newline = match 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.files.push(DataFile::new(get_filename(attr), contents)); - } - } - _ => { - self.visit_block(block); - } - } - } - } -} - -fn get_filename(attr: &Attr) -> String { - attr.0.to_string() -} - -struct ImageVisitor { - images: Vec<PathBuf>, -} - -impl ImageVisitor { - fn new() -> Self { - ImageVisitor { images: vec![] } - } - - 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)); - } - } -} - -#[derive(Default)] -struct BlockClassVisitor { - 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); - } - } - } - } -} - -#[derive(Default)] -struct LintingVisitor { - 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 is_class(attr, "file") { - let newlines: Vec<_> = 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( - get_filename(&attr), - newlines[0].to_owned(), - )), - }, - _ => self.issues.push(SubplotError::RepeatedAddNewlineAttribute( - get_filename(&attr), - )), - } - } - } - _ => { - self.visit_block(block); - } - } - } - } -} - -/// Get the base directory given the name of the markdown file. -/// -/// All relative filename, such as bindings files, are resolved -/// against the base directory. -pub fn get_basedir_from(filename: &Path) -> Result<PathBuf> { - let dirname = match filename.parent() { - None => return Err(SubplotError::BasedirError(filename.to_path_buf())), - Some(x) => x.to_path_buf(), - }; - Ok(dirname) -} - -/// Utility function to find key/value pairs from an attribute -fn find_attr_kv<'a>(attr: &'a Attr, key: &'static str) -> impl Iterator<Item = &'a str> { - attr.2.iter().flat_map(move |(key_, value)| { - if key == key_ { - Some(value.as_ref()) - } else { - None - } - }) -} diff --git a/src/datafiles.rs b/src/datafiles.rs new file mode 100644 index 0000000..83e90d9 --- /dev/null +++ b/src/datafiles.rs @@ -0,0 +1,51 @@ +use pandoc_ast::{MutVisitor, Pandoc}; +use serde::{Deserialize, Serialize}; + +/// A data file embedded in the document. +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub struct DataFile { + filename: String, + contents: String, +} + +impl DataFile { + /// Create a new data file, with a name and contents. + pub fn new(filename: String, contents: String) -> DataFile { + DataFile { filename, contents } + } + + /// Return name of embedded file. + pub fn filename(&self) -> &str { + &self.filename + } + + /// Return contents of embedded file. + pub fn contents(&self) -> &str { + &self.contents + } +} + +/// A collection of data files embedded in document. +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub struct DataFiles { + files: Vec<DataFile>, +} + +impl DataFiles { + /// Create new set of data files. + pub fn new(ast: &mut Pandoc) -> DataFiles { + let mut files = DataFiles { files: vec![] }; + files.walk_pandoc(ast); + files + } + + /// Return slice of all data files. + pub fn files(&self) -> &[DataFile] { + &self.files + } + + /// Append a new data file. + pub fn push(&mut self, file: DataFile) { + self.files.push(file); + } +} @@ -12,9 +12,23 @@ pub use error::SubplotError; mod graphmarkup; pub use graphmarkup::{DotMarkup, GraphMarkup, PlantumlMarkup}; +mod datafiles; +pub use datafiles::DataFile; +pub use datafiles::DataFiles; + +mod panhelper; +mod typeset; + +mod visitor; +use visitor::LintingVisitor; + +mod policy; +pub use policy::get_basedir_from; + +mod metadata; +pub use metadata::Metadata; + mod ast; -pub use ast::get_basedir_from; -pub use ast::DataFile; pub use ast::Document; mod scenarios; diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 0000000..05f686b --- /dev/null +++ b/src/metadata.rs @@ -0,0 +1,277 @@ +use crate::Bindings; +use crate::Result; + +use std::ops::Deref; +use std::path::{Path, PathBuf}; + +use pandoc_ast::{Inline, Map, MetaValue, Pandoc}; + +/// Metadata of a document, as needed by Subplot. +#[derive(Debug)] +pub struct Metadata { + title: String, + date: Option<String>, + bindings_filenames: Vec<PathBuf>, + bindings: Bindings, + functions_filenames: Vec<PathBuf>, + template: Option<String>, + bibliographies: Vec<PathBuf>, + /// Extra class names which should be considered 'correct' for this document + classes: Vec<String>, +} + +impl Metadata { + /// Construct a Metadata from a Document, if possible. + pub fn new<P>(basedir: P, doc: &Pandoc) -> Result<Metadata> + where + P: AsRef<Path>, + { + let title = get_title(&doc.meta)?; + let date = get_date(&doc.meta); + let bindings_filenames = get_bindings_filenames(basedir.as_ref(), &doc.meta); + let functions_filenames = get_functions_filenames(basedir.as_ref(), &doc.meta); + let template = get_template_name(&doc.meta)?; + let mut bindings = Bindings::new(); + + let bibliographies = get_bibliographies(basedir.as_ref(), &doc.meta); + let classes = get_classes(&doc.meta); + + get_bindings(&bindings_filenames, &mut bindings)?; + Ok(Metadata { + title, + date, + bindings_filenames, + bindings, + functions_filenames, + template, + bibliographies, + classes, + }) + } + + /// Return title of document. + pub fn title(&self) -> &str { + &self.title + } + + /// Return date of document, if any. + pub fn date(&self) -> Option<&str> { + if let Some(date) = &self.date { + Some(&date) + } else { + None + } + } + + /// Return filename where bindings are specified. + pub fn bindings_filenames(&self) -> Vec<&Path> { + self.bindings_filenames.iter().map(|f| f.as_ref()).collect() + } + + /// Return filename where functions are specified. + pub fn functions_filenames(&self) -> Vec<&Path> { + self.functions_filenames + .iter() + .map(|f| f.as_ref()) + .collect() + } + + /// Return the name of the code template, if specified. + pub fn template_name(&self) -> Option<&str> { + match &self.template { + Some(x) => Some(&x), + None => None, + } + } + + /// Return the bindings. + pub fn bindings(&self) -> &Bindings { + &self.bindings + } + + /// Return the bibliographies. + pub fn bibliographies(&self) -> Vec<&Path> { + self.bibliographies.iter().map(|x| x.as_path()).collect() + } + + /// The classes which this document also claims are valid + pub fn classes(&self) -> impl Iterator<Item = &str> { + self.classes.iter().map(Deref::deref) + } +} + +type Mapp = Map<String, MetaValue>; + +fn get_title(map: &Mapp) -> Result<String> { + if let Some(s) = get_string(map, "title") { + Ok(s) + } else { + Ok("".to_string()) + } +} + +fn get_date(map: &Mapp) -> Option<String> { + if let Some(s) = get_string(map, "date") { + Some(s) + } else { + None + } +} + +fn get_bindings_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf> +where + P: AsRef<Path>, +{ + get_paths(basedir, map, "bindings") +} + +fn get_functions_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf> +where + P: AsRef<Path>, +{ + get_paths(basedir, map, "functions") +} + +fn get_template_name(map: &Mapp) -> Result<Option<String>> { + match get_string(map, "template") { + Some(s) => Ok(Some(s)), + None => Ok(None), + } +} + +fn get_paths<P>(basedir: P, map: &Mapp, field: &str) -> Vec<PathBuf> +where + P: AsRef<Path>, +{ + match map.get(field) { + None => vec![], + Some(v) => pathbufs(basedir, v), + } +} + +fn get_string(map: &Mapp, field: &str) -> Option<String> { + let v = match map.get(field) { + None => return None, + Some(s) => s, + }; + let v = match v { + pandoc_ast::MetaValue::MetaString(s) => s.to_string(), + pandoc_ast::MetaValue::MetaInlines(vec) => join(&vec), + _ => panic!("don't know how to handle: {:?}", v), + }; + Some(v) +} + +fn get_bibliographies<P>(basedir: P, map: &Mapp) -> Vec<PathBuf> +where + P: AsRef<Path>, +{ + let v = match map.get("bibliography") { + None => return vec![], + Some(s) => s, + }; + pathbufs(basedir, v) +} + +fn pathbufs<P>(basedir: P, v: &MetaValue) -> Vec<PathBuf> +where + P: AsRef<Path>, +{ + let mut bufs = vec![]; + push_pathbufs(basedir, v, &mut bufs); + bufs +} + +fn get_classes(map: &Mapp) -> Vec<String> { + let mut ret = Vec::new(); + if let Some(classes) = map.get("classes") { + push_strings(classes, &mut ret); + } + ret +} + +fn push_strings(v: &MetaValue, strings: &mut Vec<String>) { + match v { + MetaValue::MetaString(s) => strings.push(s.to_string()), + MetaValue::MetaInlines(vec) => strings.push(join(&vec)), + MetaValue::MetaList(values) => { + for value in values { + push_strings(value, strings); + } + } + _ => panic!("don't know how to handle: {:?}", v), + }; +} + +fn push_pathbufs<P>(basedir: P, v: &MetaValue, bufs: &mut Vec<PathBuf>) +where + P: AsRef<Path>, +{ + match v { + MetaValue::MetaString(s) => bufs.push(basedir.as_ref().join(Path::new(s))), + MetaValue::MetaInlines(vec) => bufs.push(basedir.as_ref().join(Path::new(&join(&vec)))), + MetaValue::MetaList(values) => { + for value in values { + push_pathbufs(basedir.as_ref(), value, bufs); + } + } + _ => panic!("don't know how to handle: {:?}", v), + }; +} + +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 { + pandoc_ast::Inline::Str(s) => buf.push_str(&s), + pandoc_ast::Inline::Emph(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::Strong(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::Strikeout(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::Superscript(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::Subscript(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::SmallCaps(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::Space => buf.push_str(" "), + pandoc_ast::Inline::SoftBreak => buf.push_str(" "), + pandoc_ast::Inline::LineBreak => buf.push_str(" "), + _ => panic!("unknown pandoc_ast::Inline component {:?}", item), + } + } +} + +#[cfg(test)] +mod test_join { + use super::join; + use pandoc_ast::Inline; + + #[test] + fn join_all_kinds() { + let v = vec![ + Inline::Str("a".to_string()), + Inline::Emph(vec![Inline::Str("b".to_string())]), + Inline::Strong(vec![Inline::Str("c".to_string())]), + Inline::Strikeout(vec![Inline::Str("d".to_string())]), + Inline::Superscript(vec![Inline::Str("e".to_string())]), + Inline::Subscript(vec![Inline::Str("f".to_string())]), + Inline::SmallCaps(vec![Inline::Str("g".to_string())]), + Inline::Space, + Inline::SoftBreak, + Inline::LineBreak, + ]; + assert_eq!(join(&v), "abcdefg "); + } +} + +fn get_bindings<P>(filenames: &[P], bindings: &mut Bindings) -> Result<()> +where + P: AsRef<Path>, +{ + for filename in filenames { + bindings.add_from_file(filename)?; + } + Ok(()) +} diff --git a/src/panhelper.rs b/src/panhelper.rs new file mode 100644 index 0000000..f7ab801 --- /dev/null +++ b/src/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<Item = &'a str> { + 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/policy.rs b/src/policy.rs new file mode 100644 index 0000000..2d5bda6 --- /dev/null +++ b/src/policy.rs @@ -0,0 +1,15 @@ +use crate::{Result, SubplotError}; + +use std::path::{Path, PathBuf}; + +/// Get the base directory given the name of the markdown file. +/// +/// All relative filename, such as bindings files, are resolved +/// against the base directory. +pub fn get_basedir_from(filename: &Path) -> Result<PathBuf> { + let dirname = match filename.parent() { + None => return Err(SubplotError::BasedirError(filename.to_path_buf())), + Some(x) => x.to_path_buf(), + }; + Ok(dirname) +} diff --git a/src/typeset.rs b/src/typeset.rs new file mode 100644 index 0000000..32956c6 --- /dev/null +++ b/src/typeset.rs @@ -0,0 +1,181 @@ +use crate::parser::parse_scenario_snippet; +use crate::Bindings; +use crate::PartialStep; +use crate::ScenarioStep; +use crate::StepKind; +use crate::SubplotError; +use crate::{DotMarkup, GraphMarkup, PlantumlMarkup}; + +use pandoc_ast::Attr; +use pandoc_ast::Block; +use pandoc_ast::Inline; + +/// Typeset an error as a Pandoc AST Block element. +pub fn error(err: SubplotError) -> Block { + let msg = format!("ERROR: {}", err.to_string()); + Block::Para(error_msg(&msg)) +} + +/// Typeset an error message a vector of inlines. +pub fn error_msg(msg: &str) -> Vec<Inline> { + 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 codeblock = Block::CodeBlock(attr.clone(), 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) -> Block { + let lines = parse_scenario_snippet(snippet); + let mut steps = vec![]; + let mut prevkind: Option<StepKind> = None; + + for line in lines { + let (this, thiskind) = step(bindings, line, prevkind); + 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, + defkind: Option<StepKind>, +) -> (Vec<Inline>, Option<StepKind>) { + let step = ScenarioStep::new_from_str(text, defkind); + if step.is_err() { + return ( + error_msg(&format!("Could not parse step: {}", text)), + defkind, + ); + } + let step = step.unwrap(); + + let m = match bindings.find(&step) { + Ok(m) => m, + Err(e) => { + eprintln!("Could not select binding: {:?}", e); + return ( + error_msg(&format!("Could not select binding for: {}", text)), + defkind, + ); + } + }; + + let mut inlines = Vec::new(); + + inlines.push(keyword(&step)); + inlines.push(space()); + + for part in m.parts() { + #[allow(unused_variables)] + match part { + PartialStep::UncapturedText(s) => inlines.push(uncaptured(s.text())), + PartialStep::CapturedText { name, 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) -> Inline { + let word = inlinestr(step.keyword()); + Inline::Emph(vec![word]) +} + +// 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)]) +} + +// Take a dot graph, render it as SVG, and return an AST Block +// element. The Block will contain the SVG data. This allows the graph +// to be rendered without referending external entities. +pub fn dot_to_block(dot: &str) -> Block { + match DotMarkup::new(dot).as_svg() { + Ok(svg) => typeset_svg(svg), + Err(err) => { + eprintln!("dot failed: {}", err); + error(err) + } + } +} + +// Take a PlantUML graph, render it as SVG, and return an AST Block +// element. The Block will contain the SVG data. This allows the graph +// to be rendered without referending external entities. +pub fn plantuml_to_block(markup: &str) -> Block { + match PlantumlMarkup::new(markup).as_svg() { + Ok(svg) => typeset_svg(svg), + Err(err) => { + eprintln!("plantuml failed: {}", 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) -> 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), + 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: Vec<u8>) -> 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: Vec<u8>) -> String { + let svg = base64::encode(&svg); + format!("data:image/svg+xml;base64,{}", svg) +} diff --git a/src/visitor/block_class.rs b/src/visitor/block_class.rs new file mode 100644 index 0000000..303616b --- /dev/null +++ b/src/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/visitor/datafiles.rs b/src/visitor/datafiles.rs new file mode 100644 index 0000000..bc6961d --- /dev/null +++ b/src/visitor/datafiles.rs @@ -0,0 +1,35 @@ +use crate::panhelper; +use crate::DataFile; +use crate::DataFiles; + +use pandoc_ast::{Block, MutVisitor}; + +impl MutVisitor for DataFiles { + 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(DataFile::new(panhelper::get_filename(attr), contents)); + } + } + _ => { + self.visit_block(block); + } + } + } + } +} diff --git a/src/visitor/image.rs b/src/visitor/image.rs new file mode 100644 index 0000000..be49d66 --- /dev/null +++ b/src/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/visitor/linting.rs b/src/visitor/linting.rs new file mode 100644 index 0000000..a5171f9 --- /dev/null +++ b/src/visitor/linting.rs @@ -0,0 +1,40 @@ +use crate::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") { + 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 new file mode 100644 index 0000000..95bf2b1 --- /dev/null +++ b/src/visitor/mod.rs @@ -0,0 +1,17 @@ +mod block_class; +pub use block_class::BlockClassVisitor; + +mod datafiles; + +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 new file mode 100644 index 0000000..56e61a7 --- /dev/null +++ b/src/visitor/structure.rs @@ -0,0 +1,82 @@ +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<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 { + pandoc_ast::Inline::Str(s) => buf.push_str(&s), + pandoc_ast::Inline::Emph(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::Strong(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::Strikeout(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::Superscript(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::Subscript(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::SmallCaps(v) => join_into_buffer(v, buf), + pandoc_ast::Inline::Space => buf.push_str(" "), + pandoc_ast::Inline::SoftBreak => buf.push_str(" "), + pandoc_ast::Inline::LineBreak => buf.push_str(" "), + _ => panic!("unknown pandoc_ast::Inline component {:?}", item), + } + } +} diff --git a/src/visitor/typesetting.rs b/src/visitor/typesetting.rs new file mode 100644 index 0000000..af6ea01 --- /dev/null +++ b/src/visitor/typesetting.rs @@ -0,0 +1,49 @@ +use crate::panhelper; +use crate::typeset; +use crate::Bindings; + +use pandoc_ast::{Block, MutVisitor}; + +/// Visitor for the pandoc AST. +/// +/// This includes rendering stuff which we find as we go +pub struct TypesettingVisitor<'a> { + bindings: &'a Bindings, +} + +impl<'a> TypesettingVisitor<'a> { + pub fn new(bindings: &'a Bindings) -> Self { + TypesettingVisitor { bindings } + } +} + +// 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 graph 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) + } else if is_class(attr, "file") { + *block = typeset::file_block(attr, s) + } else if is_class(attr, "dot") { + *block = typeset::dot_to_block(s) + } else if is_class(attr, "plantuml") { + *block = typeset::plantuml_to_block(s) + } else if is_class(attr, "roadmap") { + *block = typeset::roadmap_to_block(s) + } + } + _ => { + self.visit_block(block); + } + } + } + } +} |