diff options
author | Lars Wirzenius <liw@liw.fi> | 2020-09-19 16:11:19 +0300 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2020-09-19 16:15:25 +0300 |
commit | 576e29025b893c4844acd7f4206a77e391176b5f (patch) | |
tree | b101f5a304575c79658442f38d342acd560dd928 /src/ast.rs | |
parent | 5af231e686849eb05115cd627f935ac674321dad (diff) | |
download | subplot-576e29025b893c4844acd7f4206a77e391176b5f.tar.gz |
refactor: split src/ast.rs into src/doc.rs and src/style.rs
Also, fix anywhere that's affected by the change.
Diffstat (limited to 'src/ast.rs')
-rw-r--r-- | src/ast.rs | 455 |
1 files changed, 0 insertions, 455 deletions
diff --git a/src/ast.rs b/src/ast.rs deleted file mode 100644 index 2ff25ca..0000000 --- a/src/ast.rs +++ /dev/null @@ -1,455 +0,0 @@ -use crate::parser::parse_scenario_snippet; -use crate::visitor; -use crate::DataFile; -use crate::DataFiles; -use crate::LintingVisitor; -use crate::MatchedScenario; -use crate::Metadata; -use crate::Scenario; -use crate::ScenarioStep; -use crate::{Result, SubplotError}; - -use std::collections::HashSet; -use std::default::Default; -use std::ops::Deref; -use std::path::{Path, PathBuf}; - -use pandoc_ast::{MutVisitor, Pandoc}; - -/// 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 style = subplot::Style::default(); -/// let doc = subplot::Document::from_file(&basedir, filename, style).unwrap(); -/// assert_eq!(doc.files(), &[]); -/// ~~~~ -#[derive(Debug)] -pub struct Document { - markdowns: Vec<PathBuf>, - ast: Pandoc, - meta: Metadata, - files: DataFiles, - style: Style, -} - -impl<'a> Document { - fn new( - markdowns: Vec<PathBuf>, - ast: Pandoc, - meta: Metadata, - files: DataFiles, - style: Style, - ) -> Document { - Document { - markdowns, - ast, - meta, - files, - style, - } - } - - /// Construct a Document from a JSON AST - pub fn from_json<P>( - basedir: P, - markdowns: Vec<PathBuf>, - json: &str, - style: Style, - ) -> Result<Document> - where - P: AsRef<Path>, - { - let mut ast: Pandoc = serde_json::from_str(json)?; - let meta = Metadata::new(basedir, &ast)?; - let mut linter = LintingVisitor::default(); - linter.walk_pandoc(&mut ast); - if !linter.issues.is_empty() { - // Currently we can't really return more than one error so return one - return Err(linter.issues.remove(0)); - } - let files = DataFiles::new(&mut ast); - Ok(Document::new(markdowns, ast, meta, files, style)) - } - - /// 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, style: Style) -> Result<Document> { - 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, style)?; - 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<String> { - 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<PathBuf> { - 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 = 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 = 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<String> = 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 = - visitor::TypesettingVisitor::new(self.style.clone(), &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 = visitor::StructureVisitor::new(); - visitor.walk_pandoc(&mut self.ast); - - let mut scenarios: Vec<Scenario> = 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<Vec<MatchedScenario>> { - let scenarios = self.scenarios()?; - let bindings = self.meta().bindings(); - let vec: Result<Vec<MatchedScenario>> = scenarios - .iter() - .map(|scen| MatchedScenario::new(scen, bindings)) - .collect(); - Ok(vec?) - } -} - -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] { - 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 { - visitor::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)); - } - } - visitor::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::visitor::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<Scenario>, 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<Element> = 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), - } - } -} - -/// Typesetting style configuration for documents. -#[derive(Clone, Debug, Default)] -pub struct Style { - /// Should hyperlinks in the document be rendered as footnotes or endnotes? - /// - /// A link is like the HTML `<a>` element. The choice of footnote - /// versus endnote is made by the typesetting backend. HTML uses - /// endnotes, PDF uses footnotes. - pub links_as_notes: bool, -} |