summaryrefslogtreecommitdiff
path: root/src/ast.rs
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2020-09-19 16:11:19 +0300
committerLars Wirzenius <liw@liw.fi>2020-09-19 16:15:25 +0300
commit576e29025b893c4844acd7f4206a77e391176b5f (patch)
treeb101f5a304575c79658442f38d342acd560dd928 /src/ast.rs
parent5af231e686849eb05115cd627f935ac674321dad (diff)
downloadsubplot-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.rs455
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,
-}