use crate::parser::parse_scenario_snippet; use crate::Bindings; use crate::MatchedScenario; use crate::PartialStep; 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}; /// The set of known (special) classes which subplot will always recognise /// as being valid. static SPECIAL_CLASSES: &[&str] = &["scenario", "file", "dot", "plantuml", "roadmap"]; /// The set of known (file-type) classes which subplot will always recognise /// as being valid. static KNOWN_FILE_CLASSES: &[&str] = &["rust", "yaml", "python", "sh", "shell", "markdown", "bash"]; /// The set of known (special-to-pandoc) classes which subplot will always recognise /// as being valid. static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines"]; /// A parsed Subplot document. /// /// Pandoc works by parsing its various input files and constructing /// an abstract syntax tree or AST. When Pandoc generates output, it /// works based on the AST. This way, the input parsing and output /// generation are cleanly separated. /// /// A Pandoc filter can modify the AST before output generation /// starts working. This allows the filter to make changes to what /// gets output, without having to understand the input documents at /// all. /// /// This function is a Pandoc filter, to be use with /// pandoc_ast::filter, for typesetting Subplot documents. /// /// # Example /// /// fix this example; /// ~~~~ /// let markdown = "\ /// --- /// title: Test Title /// ... /// This is a test document. /// "; /// /// use std::io::Write; /// use tempfile::NamedTempFile; /// let mut f = NamedTempFile::new().unwrap(); /// f.write_all(markdown.as_bytes()).unwrap(); /// let filename = f.path(); /// /// use subplot; /// let basedir = std::path::Path::new("."); /// let doc = subplot::Document::from_file(&basedir, filename).unwrap(); /// assert_eq!(doc.files(), &[]); /// ~~~~ #[derive(Debug)] pub struct Document { markdowns: Vec, ast: Pandoc, meta: Metadata, files: DataFiles, } impl<'a> Document { fn new(markdowns: Vec, ast: Pandoc, meta: Metadata, files: DataFiles) -> Document { Document { markdowns, ast, meta, files, } } /// Construct a Document from a JSON AST pub fn from_json

(basedir: P, markdowns: Vec, json: &str) -> Result where P: AsRef, { let mut ast: Pandoc = serde_json::from_str(json)?; let meta = Metadata::new(basedir, &ast)?; let files = DataFiles::new(&mut ast); Ok(Document::new(markdowns, ast, meta, files)) } /// Construct a Document from a named file. /// /// The file can be in any format Pandoc understands. This runs /// Pandoc to parse the file into an AST, so it can be a little /// slow. pub fn from_file(basedir: &Path, filename: &Path) -> Result { let markdowns = vec![filename.to_path_buf()]; 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. let citeproc = std::path::Path::new("pandoc-citeproc"); pandoc.add_option(pandoc::PandocOption::Filter(citeproc.to_path_buf())); let output = match pandoc.execute()? { pandoc::PandocOutput::ToFile(_) => return Err(SubplotError::NotJson), pandoc::PandocOutput::ToBuffer(o) => o, }; let doc = Document::from_json(basedir, markdowns, &output)?; Ok(doc) } /// Return the AST of a Document, serialized as JSON. /// /// This is useful in a Pandoc filter, so that the filter can give /// it back to Pandoc for typesetting. pub fn ast(&self) -> Result { let json = serde_json::to_string(&self.ast)?; Ok(json) } /// Return the document's metadata. pub fn meta(&self) -> &Metadata { &self.meta } /// Return all source filenames for the document. /// /// The sources are any files that affect the output so that if /// the source file changes, the output needs to be re-generated. pub fn sources(&mut self) -> Vec { let mut names = vec![]; for x in self.meta().bindings_filenames() { names.push(PathBuf::from(x)) } for x in self.meta().functions_filenames() { names.push(PathBuf::from(x)) } for x in self.meta().bibliographies().iter() { names.push(PathBuf::from(x)) } for x in self.markdowns.iter() { names.push(x.to_path_buf()); } let mut visitor = ImageVisitor::new(); visitor.walk_pandoc(&mut self.ast); for x in visitor.images().iter() { names.push(x.to_path_buf()); } names } /// Return list of files embeddedin the document. pub fn files(&self) -> &[DataFile] { self.files.files() } /// Check the document for common problems. pub fn lint(&self) -> Result<()> { self.check_doc_has_title()?; self.check_filenames_are_unique()?; self.check_block_classes()?; Ok(()) } // Check that all filenames for embedded files are unique. fn check_filenames_are_unique(&self) -> Result<()> { let mut known = HashSet::new(); for filename in self.files().iter().map(|f| f.filename().to_lowercase()) { if known.contains(&filename) { return Err(SubplotError::DuplicateEmbeddedFilename(filename)); } known.insert(filename); } Ok(()) } // Check that document has a title in its metadata. fn check_doc_has_title(&self) -> Result<()> { if self.meta().title().is_empty() { Err(SubplotError::NoTitle) } else { Ok(()) } } /// Check that all the block classes in the document are known fn check_block_classes(&self) -> Result<()> { let mut 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(&mut self.ast.clone()); // Build the set of known good classes let mut known_classes: HashSet = HashSet::new(); for class in std::iter::empty() .chain(SPECIAL_CLASSES.iter().map(Deref::deref)) .chain(KNOWN_FILE_CLASSES.iter().map(Deref::deref)) .chain(KNOWN_PANDOC_CLASSES.iter().map(Deref::deref)) .chain(self.meta().classes()) { known_classes.insert(class.to_string()); } // Acquire the set of used names which are not known let unknown_classes: Vec<_> = visitor .classes .difference(&known_classes) .cloned() .collect(); // If the unknown classes list is not empty, we had a problem and // we will report it to the user. if !unknown_classes.is_empty() { Err(SubplotError::UnknownClasses(unknown_classes.join(", "))) } else { Ok(()) } } /// Typeset a Subplot document. pub fn typeset(&mut self) { let mut visitor = TypesettingVisitor::new(&self.meta.bindings); visitor.walk_pandoc(&mut self.ast); } /// Return all scenarios in a document. pub fn scenarios(&mut self) -> Result> { let mut visitor = StructureVisitor::new(); visitor.walk_pandoc(&mut self.ast); let mut scenarios: Vec = vec![]; let mut i = 0; while i < visitor.elements.len() { let (maybe, new_i) = extract_scenario(&visitor.elements[i..])?; if let Some(scen) = maybe { scenarios.push(scen); } i += new_i; } Ok(scenarios) } /// Return matched scenarios in a document. pub fn matched_scenarios(&mut self) -> Result> { let scenarios = self.scenarios()?; let bindings = self.meta().bindings(); let vec: Result> = scenarios .iter() .map(|scen| MatchedScenario::new(scen, bindings)) .collect(); Ok(vec?) } } fn extract_scenario(e: &[Element]) -> Result<(Option, 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) => { let mut scen = Scenario::new(&title); let mut prevkind = None; for (i, item) in e.iter().enumerate().skip(1) { match item { Element::Heading(_, level2) => { let is_subsection = *level2 > *level; if is_subsection { if scen.has_steps() { } else { return Ok((None, i)); } } else if scen.has_steps() { return Ok((Some(scen), i)); } else { return Ok((None, i)); } } Element::Snippet(text) => { for line in parse_scenario_snippet(&text) { let step = ScenarioStep::new_from_str(line, prevkind)?; scen.add(&step); prevkind = Some(step.kind()); } } } } if scen.has_steps() { Ok((Some(scen), e.len())) } else { Ok((None, e.len())) } } } } #[cfg(test)] mod test_extract { use super::extract_scenario; use super::Element; use crate::Result; use crate::Scenario; use crate::SubplotError; fn h(title: &str, level: i64) -> Element { Element::Heading(title.to_string(), level) } fn s(text: &str) -> Element { Element::Snippet(text.to_string()) } fn check_result(r: Result<(Option, usize)>, title: Option<&str>, i: usize) { eprintln!("checking result: {:?}", r); assert!(r.is_ok()); let (actual_scen, actual_i) = r.unwrap(); if title.is_none() { assert!(actual_scen.is_none()); } else { assert!(actual_scen.is_some()); let scen = actual_scen.unwrap(); assert_eq!(scen.title(), title.unwrap()); } assert_eq!(actual_i, i); } #[test] fn returns_nothing_if_there_is_no_scenario() { let elements: Vec = vec![h("title", 1)]; let r = extract_scenario(&elements); check_result(r, None, 1); } #[test] fn returns_scenario_if_there_is_one() { let elements = vec![h("title", 1), s("given something")]; let r = extract_scenario(&elements); check_result(r, Some("title"), 2); } #[test] fn skips_scenarioless_section_in_favour_of_same_level() { let elements = vec![h("first", 1), h("second", 1), s("given something")]; let r = extract_scenario(&elements); check_result(r, None, 1); let r = extract_scenario(&elements[1..]); check_result(r, Some("second"), 2); } #[test] fn returns_parent_section_with_scenario_snippet() { let elements = vec![ h("1", 1), s("given something"), h("1.1", 2), s("when something"), h("2", 1), ]; let r = extract_scenario(&elements); check_result(r, Some("1"), 4); let r = extract_scenario(&elements[4..]); check_result(r, None, 1); } #[test] fn skips_scenarioless_parent_heading() { let elements = vec![h("1", 1), h("1.1", 2), s("given something"), h("2", 1)]; let r = extract_scenario(&elements); check_result(r, None, 1); let r = extract_scenario(&elements[1..]); check_result(r, Some("1.1"), 2); let r = extract_scenario(&elements[3..]); check_result(r, None, 1); } #[test] fn skips_scenarioless_deeper_headings() { let elements = vec![h("1", 1), h("1.1", 2), h("2", 1), s("given something")]; let r = extract_scenario(&elements); check_result(r, None, 1); let r = extract_scenario(&elements[1..]); check_result(r, None, 1); let r = extract_scenario(&elements[2..]); check_result(r, Some("2"), 2); } #[test] fn returns_error_if_scenario_has_no_title() { let elements = vec![s("given something")]; let r = extract_scenario(&elements); match r { Err(SubplotError::ScenarioBeforeHeading) => (), _ => panic!("unexpected result {:?}", r), } } } /// Metadata of a document, as needed by Subplot. #[derive(Debug)] pub struct Metadata { title: String, date: Option, bindings_filenames: Vec, bindings: Bindings, functions_filenames: Vec, template: Option, bibliographies: Vec, /// Extra class names which should be considered 'correct' for this document classes: Vec, } impl Metadata { /// Construct a Metadata from a Document, if possible. pub fn new

(basedir: P, doc: &Pandoc) -> Result where P: AsRef, { 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 { self.classes.iter().map(Deref::deref) } } type Mapp = Map; fn get_title(map: &Mapp) -> Result { if let Some(s) = get_string(map, "title") { Ok(s) } else { Ok("".to_string()) } } fn get_date(map: &Mapp) -> Option { if let Some(s) = get_string(map, "date") { Some(s) } else { None } } fn get_bindings_filenames

(basedir: P, map: &Mapp) -> Vec where P: AsRef, { get_paths(basedir, map, "bindings") } fn get_functions_filenames

(basedir: P, map: &Mapp) -> Vec where P: AsRef, { get_paths(basedir, map, "functions") } fn get_template_name(map: &Mapp) -> Result> { match get_string(map, "template") { Some(s) => Ok(Some(s)), None => Ok(None), } } fn get_paths

(basedir: P, map: &Mapp, field: &str) -> Vec where P: AsRef, { match map.get(field) { None => vec![], Some(v) => pathbufs(basedir, v), } } fn get_string(map: &Mapp, field: &str) -> Option { 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

(basedir: P, map: &Mapp) -> Vec where P: AsRef, { let v = match map.get("bibliography") { None => return vec![], Some(s) => s, }; pathbufs(basedir, v) } fn pathbufs

(basedir: P, v: &MetaValue) -> Vec where P: AsRef, { let mut bufs = vec![]; push_pathbufs(basedir, v, &mut bufs); bufs } fn get_classes(map: &Mapp) -> Vec { 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) { 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

(basedir: P, v: &MetaValue, bufs: &mut Vec) where P: AsRef, { 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

(filenames: &[P], bindings: &mut Bindings) -> Result<()> where P: AsRef, { 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) { 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 { 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 = 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, ) -> (Vec, Option) { 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 = bindings.find(&step); if m.is_none() { eprintln!("Could not finding binding for: {}", text); return ( error_msg(&format!("Could not find binding for: {}", text)), defkind, ); } let m = m.unwrap(); 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) -> 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) -> 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, } impl StructureVisitor { pub fn new() -> Self { Self { elements: vec![] } } } impl MutVisitor for StructureVisitor { fn visit_vec_block(&mut self, vec_block: &mut Vec) { 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: &str, contents: &str) -> DataFile { DataFile { filename: filename.to_string(), contents: contents.to_string(), } } pub fn filename(&self) -> &str { &self.filename } 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, } 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) { for block in vec_block { match block { Block::CodeBlock(attr, contents) => { if is_class(attr, "file") { 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, } impl ImageVisitor { fn new() -> Self { ImageVisitor { images: vec![] } } 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)); } } } #[derive(Default)] struct BlockClassVisitor { 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); } } } } } /// 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 { let dirname = match filename.parent() { None => return Err(SubplotError::BasedirError(filename.to_path_buf())), Some(x) => x.to_path_buf(), }; Ok(dirname) }