diff options
Diffstat (limited to 'src/doc.rs')
-rw-r--r-- | src/doc.rs | 758 |
1 files changed, 423 insertions, 335 deletions
@@ -1,31 +1,34 @@ -use crate::ast; +use crate::bindings::CaptureType; use crate::generate_test_program; use crate::get_basedir_from; -use crate::visitor; -use crate::DataFile; -use crate::DataFiles; -use crate::LintingVisitor; +use crate::html::Attribute; +use crate::html::HtmlPage; +use crate::html::{Content, Element, ElementTag}; +use crate::md::Markdown; +use crate::resource; +use crate::EmbeddedFile; +use crate::EmbeddedFiles; use crate::MatchedScenario; -use crate::Metadata; use crate::PartialStep; use crate::Scenario; -use crate::ScenarioStep; use crate::Style; use crate::SubplotError; -use crate::{bindings::CaptureType, parser::parse_scenario_snippet}; +use crate::{Metadata, YamlMetadata}; use crate::{Warning, Warnings}; +use std::cmp::Ordering; use std::collections::HashSet; use std::default::Default; use std::fmt::Debug; +use std::fs::read; use std::ops::Deref; use std::path::{Path, PathBuf}; -use std::str::FromStr; - -use pandoc_ast::{MutVisitor, Pandoc}; use log::{error, trace}; +/// Name of standard Subplot CSS file. +const CSS: &str = "subplot.css"; + /// The set of known (special) classes which subplot will always recognise /// as being valid. static SPECIAL_CLASSES: &[&str] = &[ @@ -36,30 +39,21 @@ static SPECIAL_CLASSES: &[&str] = &[ /// 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. We include the subplot-specific noNumberLines class which we use -/// to invert the default numberLines on .file blocks. -static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines", "noNumberLines"]; +/// The set of known classes which subplot will always recognise as +/// being valid. We include the subplot-specific noNumberLines class +/// which we use to invert the default numberLines on .file blocks. +static KNOWN_BLOCK_CLASSES: &[&str] = &["numberLines", "noNumberLines"]; + +/// The set of classes which subplot will recognise as being appropriate +/// for having IDs. +static ID_OK_CLASSES: &[&str] = &["file", "example"]; /// 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; -/// ~~~~ +/// ~~~~ignored /// let markdown = "\ /// --- /// title: Test Title @@ -81,82 +75,43 @@ static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines", "noNumberLines"]; /// ~~~~ #[derive(Debug)] pub struct Document { - markdowns: Vec<PathBuf>, - ast: Pandoc, + subplot: PathBuf, + markdowns: Vec<Markdown>, meta: Metadata, - files: DataFiles, + files: EmbeddedFiles, style: Style, - warnings: Warnings, } -impl<'a> Document { +impl Document { fn new( - markdowns: Vec<PathBuf>, - ast: Pandoc, + subplot: PathBuf, + markdowns: Vec<Markdown>, meta: Metadata, - files: DataFiles, + files: EmbeddedFiles, style: Style, ) -> Document { - Document { + let doc = Document { + subplot, markdowns, - ast, meta, files, style, - warnings: Warnings::default(), - } - } - - /// Return all warnings about this document. - pub fn warnings(&self) -> &[Warning] { - self.warnings.warnings() - } - - fn from_ast<P>( - basedir: P, - markdowns: Vec<PathBuf>, - mut ast: Pandoc, - style: Style, - template: Option<&str>, - ) -> Result<Document, SubplotError> - where - P: AsRef<Path> + Debug, - { - let meta = Metadata::new(basedir, &ast, template)?; - let mut linter = LintingVisitor::default(); - trace!("Walking AST for linting..."); - 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); - let doc = Document::new(markdowns, ast, meta, files, style); - trace!("Loaded from JSON OK"); - Ok(doc) + }; + trace!("Document::new -> {:#?}", doc); + doc } - /// Construct a Document from a JSON AST - pub fn from_json<P>( - basedir: P, - markdowns: Vec<PathBuf>, - json: &str, - style: Style, - template: Option<&str>, - ) -> Result<Document, SubplotError> - where - P: AsRef<Path> + Debug, - { - trace!("Parsing document..."); - let ast: Pandoc = serde_json::from_str(json).map_err(SubplotError::AstJson)?; - Self::from_ast(basedir, markdowns, ast, style, template) + fn all_files(markdowns: &[Markdown]) -> Result<EmbeddedFiles, SubplotError> { + let mut files = EmbeddedFiles::default(); + for md in markdowns { + for file in md.embedded_files()?.files() { + files.push(file.clone()); + } + } + Ok(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, @@ -168,59 +123,218 @@ impl<'a> Document { basedir.display(), filename.display() ); - 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); + let meta = load_metadata_from_yaml_file(filename)?; + trace!("metadata from YAML file: {:#?}", meta); - // Add external Pandoc filters. - crate::policy::add_citeproc(&mut pandoc); + let mut markdowns = vec![]; + for filename in meta.markdowns() { + let filename = basedir.join(filename); + markdowns.push(Markdown::load_file(&filename)?); + } + + let meta = Metadata::from_yaml_metadata(basedir, &meta, template)?; + trace!("metadata from YAML: {:#?}", meta); + let files = Self::all_files(&markdowns)?; + let doc = Document::new(filename.into(), markdowns, meta, files, style); - trace!("Invoking Pandoc to parse document {:?}", filename); - let output = match pandoc.execute().map_err(SubplotError::Pandoc)? { - pandoc::PandocOutput::ToBuffer(o) => o, - _ => return Err(SubplotError::NotJson), - }; - trace!("Pandoc was happy"); - let doc = Document::from_json(basedir, markdowns, &output, style, template)?; trace!("Loaded document OK"); Ok(doc) } - /// Construct a Document from a named file, using the pullmark_cmark crate. - /// - /// The file can be in the CommonMark format, with some - /// extensions. This uses the pulldown-cmark crate to parse the - /// file into an AST. - pub fn from_file_with_pullmark( - basedir: &Path, - filename: &Path, - style: Style, - template: Option<&str>, - ) -> Result<Document, SubplotError> { - trace!("Parsing document with pullmark-cmark from {:?}", filename); - let filename = filename.to_path_buf(); - let markdown = std::fs::read_to_string(&filename) - .map_err(|err| SubplotError::ReadFile(filename.clone(), err))?; - let ast = ast::AbstractSyntaxTree::from_str(&markdown)?; + /// Return Document as an HTML page serialized into HTML text + pub fn to_html(&mut self, date: &str) -> Result<String, SubplotError> { + let css_file = resource::read_as_string(CSS, None) + .map_err(|e| SubplotError::CssFileNotFound(CSS.into(), e))?; + + let mut head = Element::new(crate::html::ElementTag::Head); + let mut title = Element::new(crate::html::ElementTag::Title); + title.push_child(crate::html::Content::Text(self.meta().title().into())); + head.push_child(crate::html::Content::Elt(title)); + + let mut css = Element::new(ElementTag::Style); + css.push_child(Content::Text(css_file)); + for css_file in self.meta.css_embed() { + css.push_child(Content::Text(css_file.into())); + } + head.push_child(Content::Elt(css)); + + for css_url in self.meta.css_urls() { + let mut link = Element::new(ElementTag::Link); + link.push_attribute(Attribute::new("rel", "stylesheet")); + link.push_attribute(Attribute::new("type", "text/css")); + link.push_attribute(Attribute::new("href", css_url)); + head.push_child(Content::Elt(link)); + } - trace!("Parsed document OK"); - Self::from_ast(basedir, vec![filename], ast.to_pandoc(), style, template) + self.meta.set_date(date.into()); + + let mut body_content = Element::new(crate::html::ElementTag::Div); + body_content.push_attribute(Attribute::new("class", "content")); + for md in self.markdowns.iter() { + body_content.push_child(Content::Elt(md.root_element().clone())); + } + + let mut body = Element::new(crate::html::ElementTag::Div); + body.push_child(Content::Elt(self.typeset_meta())); + body.push_child(Content::Elt(self.typeset_toc(&body_content))); + body.push_child(Content::Elt(body_content)); + + let page = HtmlPage::new(head, body); + page.serialize().map_err(SubplotError::ParseMarkdown) } - /// 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, SubplotError> { - let json = serde_json::to_string(&self.ast).map_err(SubplotError::AstJson)?; - Ok(json) + fn typeset_toc(&self, body: &Element) -> Element { + let mut toc = Element::new(ElementTag::Div); + toc.push_attribute(Attribute::new("class", "toc")); + + let mut heading = Element::new(ElementTag::H1); + heading.push_child(Content::Text("Table of Contents".into())); + toc.push_child(Content::Elt(heading)); + + let heading_elements: Vec<&Element> = crate::md::Markdown::visit(body) + .iter() + .filter(|e| { + matches!( + e.tag(), + ElementTag::H1 + | ElementTag::H2 + | ElementTag::H3 + | ElementTag::H4 + | ElementTag::H5 + | ElementTag::H6 + ) + }) + .cloned() + .collect(); + + let mut headings = vec![]; + for e in heading_elements { + let id = e + .attr("id") + .expect("heading has id") + .value() + .expect("id attribute has value"); + match e.tag() { + ElementTag::H1 => headings.push((1, e.content(), id)), + ElementTag::H2 => headings.push((2, e.content(), id)), + ElementTag::H3 => headings.push((3, e.content(), id)), + ElementTag::H4 => headings.push((4, e.content(), id)), + ElementTag::H5 => headings.push((5, e.content(), id)), + ElementTag::H6 => headings.push((6, e.content(), id)), + _ => (), + } + } + + let mut stack = vec![]; + let mut numberer = HeadingNumberer::default(); + for (level, text, id) in headings { + assert!(level >= 1); + assert!(level <= 6); + + let mut number = Element::new(ElementTag::Span); + number.push_attribute(Attribute::new("class", "heading-number")); + number.push_child(Content::Text(numberer.number(level))); + + let mut htext = Element::new(ElementTag::Span); + htext.push_attribute(Attribute::new("class", "heading-text")); + htext.push_child(Content::Text(text)); + + let mut a = Element::new(ElementTag::A); + a.push_attribute(crate::html::Attribute::new("href", &format!("#{}", id))); + a.push_attribute(Attribute::new("class", "toc-link")); + a.push_child(Content::Elt(number)); + a.push_child(Content::Text(" ".into())); + a.push_child(Content::Elt(htext)); + + let mut li = Element::new(ElementTag::Li); + li.push_child(Content::Elt(a)); + + match level.cmp(&stack.len()) { + Ordering::Equal => (), + Ordering::Greater => stack.push(Element::new(ElementTag::Ol)), + Ordering::Less => { + assert!(!stack.is_empty()); + let child = stack.pop().unwrap(); + assert!(child.tag() == ElementTag::Ol); + let mut li = Element::new(ElementTag::Li); + li.push_child(Content::Elt(child)); + assert!(!stack.is_empty()); + let mut parent = stack.pop().unwrap(); + parent.push_child(Content::Elt(li)); + stack.push(parent); + } + } + + assert!(!stack.is_empty()); + let mut ol = stack.pop().unwrap(); + ol.push_child(Content::Elt(li)); + stack.push(ol); + } + + while stack.len() > 1 { + let child = stack.pop().unwrap(); + assert!(child.tag() == ElementTag::Ol); + let mut li = Element::new(ElementTag::Li); + li.push_child(Content::Elt(child)); + + let mut parent = stack.pop().unwrap(); + parent.push_child(Content::Elt(li)); + stack.push(parent); + } + + assert!(stack.len() <= 1); + if let Some(ol) = stack.pop() { + toc.push_child(Content::Elt(ol)); + } + + toc + } + + fn typeset_meta(&self) -> Element { + let mut div = Element::new(ElementTag::Div); + div.push_attribute(Attribute::new("class", "meta")); + + div.push_child(Content::Elt(Self::title(self.meta.title()))); + + if let Some(authors) = self.meta.authors() { + div.push_child(Content::Elt(Self::authors(authors))); + } + + if let Some(date) = self.meta.date() { + div.push_child(Content::Elt(Self::date(date))); + } + + div + } + + fn title(title: &str) -> Element { + let mut e = Element::new(ElementTag::H1); + e.push_attribute(Attribute::new("class", "title")); + e.push_child(Content::Text(title.into())); + e + } + + fn authors(authors: &[String]) -> Element { + let mut list = Element::new(ElementTag::P); + list.push_attribute(Attribute::new("class", "authors")); + list.push_child(Content::Text("By: ".into())); + let mut first = true; + for a in authors { + if !first { + list.push_child(Content::Text(", ".into())); + } + list.push_child(Content::Text(a.into())); + first = false; + } + list + } + + fn date(date: &str) -> Element { + let mut e = Element::new(ElementTag::P); + e.push_attribute(Attribute::new("class", "date")); + e.push_child(Content::Text(date.into())); + e } /// Return the document's metadata. @@ -228,12 +342,17 @@ impl<'a> Document { &self.meta } + /// Set document date. + pub fn set_date(&mut self, date: String) { + self.meta.set_date(date); + } + /// 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, template: Option<&str>) -> Vec<PathBuf> { - let mut names = vec![]; + pub fn sources(&self, template: Option<&str>) -> Vec<PathBuf> { + let mut names = vec![self.subplot.clone()]; for x in self.meta().bindings_filenames() { names.push(PathBuf::from(x)) @@ -255,25 +374,20 @@ impl<'a> Document { } } - for x in self.meta().bibliographies().iter() { - names.push(PathBuf::from(x)) - } - - for x in self.markdowns.iter() { - names.push(x.to_path_buf()); + for name in self.meta().markdown_filenames() { + names.push(name.into()); } - let mut visitor = visitor::ImageVisitor::new(); - visitor.walk_pandoc(&mut self.ast); - for x in visitor.images().iter() { - names.push(x.to_path_buf()); + for md in self.markdowns.iter() { + let mut images = md.images(); + names.append(&mut images); } names } /// Return list of files embeddedin the document. - pub fn files(&self) -> &[DataFile] { + pub fn embedded_files(&self) -> &[EmbeddedFile] { self.files.files() } @@ -281,16 +395,33 @@ impl<'a> Document { pub fn lint(&self) -> Result<(), SubplotError> { trace!("Linting document"); self.check_doc_has_title()?; + self.check_scenarios_are_unique()?; self.check_filenames_are_unique()?; self.check_block_classes()?; trace!("No linting problems found"); Ok(()) } + // Check that all titles for scenarios are unique. + fn check_scenarios_are_unique(&self) -> Result<(), SubplotError> { + let mut known = HashSet::new(); + for title in self.scenarios()?.iter().map(|s| s.title().to_lowercase()) { + if known.contains(&title) { + return Err(SubplotError::DuplicateScenario(title)); + } + known.insert(title); + } + Ok(()) + } + // Check that all filenames for embedded files are unique. fn check_filenames_are_unique(&self) -> Result<(), SubplotError> { let mut known = HashSet::new(); - for filename in self.files().iter().map(|f| f.filename().to_lowercase()) { + for filename in self + .embedded_files() + .iter() + .map(|f| f.filename().to_lowercase()) + { if known.contains(&filename) { return Err(SubplotError::DuplicateEmbeddedFilename(filename)); } @@ -310,27 +441,20 @@ impl<'a> Document { /// Check that all the block classes in the document are known fn check_block_classes(&self) -> Result<(), SubplotError> { - 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()); + let classes_in_doc = self.all_block_classes(); + // 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(KNOWN_BLOCK_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(); + let unknown_classes: Vec<_> = classes_in_doc.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() { @@ -340,11 +464,61 @@ impl<'a> Document { } } + fn all_block_classes(&self) -> HashSet<String> { + let mut set = HashSet::new(); + for md in self.markdowns.iter() { + for class in md.block_classes() { + set.insert(class); + } + } + set + } + + /// Check bindings for warnings + pub fn check_bindings(&self, warnings: &mut Warnings) -> Result<(), SubplotError> { + self.meta.bindings().check(warnings) + } + + /// Check labelled code blocks have some appropriate class + pub fn check_named_code_blocks_have_appropriate_class( + &self, + warnings: &mut Warnings, + ) -> Result<bool, SubplotError> { + let mut okay = true; + for md in self.markdowns.iter() { + for block in md.named_blocks() { + if !block.all_attrs().iter().any(|attr| { + attr.name() == "class" + && ID_OK_CLASSES + .iter() + .any(|class| attr.value() == Some(class)) + }) { + // For now, named blocks must be files + warnings.push(Warning::MissingAppropriateClassOnNamedCodeBlock( + block + .attr("id") + .expect("Named blocks should have IDs") + .value() + .unwrap_or("(unknown-id)") + .to_string(), + block.location().to_string(), + )); + okay = false; + } + } + } + Ok(okay) + } + /// Check that all named files (in matched steps) are actually present in the /// document. - pub fn check_named_files_exist(&mut self, template: &str) -> Result<bool, SubplotError> { + pub fn check_named_files_exist( + &self, + template: &str, + warnings: &mut Warnings, + ) -> Result<bool, SubplotError> { let filenames: HashSet<_> = self - .files() + .embedded_files() .iter() .map(|f| f.filename().to_lowercase()) .collect(); @@ -357,11 +531,16 @@ impl<'a> Document { for scenario in scenarios { for step in scenario.steps() { for captured in step.parts() { - if let PartialStep::CapturedText { name, text } = captured { + if let PartialStep::CapturedText { + name, + text, + kind: _, + } = captured + { if matches!(step.types().get(name.as_str()), Some(CaptureType::File)) && !filenames.contains(&text.to_lowercase()) { - self.warnings.push(Warning::UnknownEmbeddedFile( + warnings.push(Warning::UnknownEmbeddedFile( scenario.title().to_string(), text.to_string(), )); @@ -375,9 +554,13 @@ impl<'a> Document { } /// Check that all embedded files are used by matched steps. - pub fn check_embedded_files_are_used(&mut self, template: &str) -> Result<bool, SubplotError> { + pub fn check_embedded_files_are_used( + &self, + template: &str, + warnings: &mut Warnings, + ) -> Result<bool, SubplotError> { let mut filenames: HashSet<_> = self - .files() + .embedded_files() .iter() .map(|f| f.filename().to_lowercase()) .collect(); @@ -389,7 +572,12 @@ impl<'a> Document { for scenario in scenarios { for step in scenario.steps() { for captured in step.parts() { - if let PartialStep::CapturedText { name, text } = captured { + if let PartialStep::CapturedText { + name, + text, + kind: _, + } = captured + { if matches!(step.types().get(name.as_str()), Some(CaptureType::File)) { filenames.remove(&text.to_lowercase()); } @@ -398,8 +586,7 @@ impl<'a> Document { } } for filename in filenames.iter() { - self.warnings - .push(Warning::UnusedEmbeddedFile(filename.to_string())); + warnings.push(Warning::UnusedEmbeddedFile(filename.to_string())); } // We always succeed. Subplot's own subplot had valid cases of @@ -410,7 +597,7 @@ impl<'a> Document { } /// Check that all matched steps actually have function implementations - pub fn check_matched_steps_have_impl(&mut self, template: &str) -> bool { + pub fn check_matched_steps_have_impl(&self, template: &str, warnings: &mut Warnings) -> bool { trace!("Checking that steps have implementations"); let mut okay = true; let scenarios = match self.matched_scenarios(template) { @@ -423,7 +610,7 @@ impl<'a> Document { for step in scenario.steps() { if step.function().is_none() { trace!("Missing step implementation: {:?}", step.text()); - self.warnings.push(Warning::MissingStepImplementation( + warnings.push(Warning::MissingStepImplementation( scenario.title().to_string(), step.text().to_string(), )); @@ -435,36 +622,28 @@ impl<'a> Document { } /// 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); - self.warnings.push_all(visitor.warnings()); + pub fn typeset( + &mut self, + warnings: &mut Warnings, + template: Option<&str>, + ) -> Result<(), SubplotError> { + for md in self.markdowns.iter_mut() { + warnings.push_all(md.typeset(self.style.clone(), template, self.meta.bindings())); + } + Ok(()) } /// Return all scenarios in a document. - pub fn scenarios(&mut self) -> Result<Vec<Scenario>, SubplotError> { - 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; + pub fn scenarios(&self) -> Result<Vec<Scenario>, SubplotError> { + let mut scenarios = vec![]; + for md in self.markdowns.iter() { + scenarios.append(&mut md.scenarios()?); } Ok(scenarios) } /// Return matched scenarios in a document. - pub fn matched_scenarios( - &mut self, - template: &str, - ) -> Result<Vec<MatchedScenario>, SubplotError> { + pub fn matched_scenarios(&self, template: &str) -> Result<Vec<MatchedScenario>, SubplotError> { let scenarios = self.scenarios()?; trace!( "Found {} scenarios, checking their bindings", @@ -490,9 +669,15 @@ impl<'a> Document { } } +fn load_metadata_from_yaml_file(filename: &Path) -> Result<YamlMetadata, SubplotError> { + let yaml = read(filename).map_err(|e| SubplotError::ReadFile(filename.into(), e))?; + trace!("Parsing YAML metadata from {}", filename.display()); + let meta: YamlMetadata = serde_yaml::from_slice(&yaml) + .map_err(|e| SubplotError::MetadataFile(filename.into(), e))?; + Ok(meta) +} + /// Load a `Document` from a file. -/// -/// This version uses Pandoc to parse the Markdown. pub fn load_document<P>( filename: P, style: Style, @@ -535,7 +720,7 @@ where style ); crate::resource::add_search_path(filename.parent().unwrap()); - let doc = Document::from_file_with_pullmark(&base_path, filename, style, template)?; + let doc = Document::from_file(&base_path, filename, style, template)?; trace!("Loaded doc from file OK"); Ok(doc) } @@ -562,9 +747,11 @@ pub fn codegen( if !doc.meta().templates().any(|t| t == template) { return Err(SubplotError::TemplateSupportNotPresent); } - if !doc.check_named_files_exist(&template)? - || !doc.check_matched_steps_have_impl(&template) - || !doc.check_embedded_files_are_used(&template)? + let mut warnings = Warnings::default(); + if !doc.check_named_code_blocks_have_appropriate_class(&mut warnings)? + || !doc.check_named_files_exist(&template, &mut warnings)? + || !doc.check_matched_steps_have_impl(&template, &mut warnings) + || !doc.check_embedded_files_are_used(&template, &mut warnings)? { error!("Found problems in document, cannot continue"); std::process::exit(1); @@ -589,155 +776,56 @@ impl CodegenOutput { } } -fn extract_scenario(e: &[visitor::Element]) -> Result<(Option<Scenario>, usize), SubplotError> { - 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()); - } - } +#[derive(Debug, Default)] +struct HeadingNumberer { + prev: Vec<usize>, +} + +impl HeadingNumberer { + fn number(&mut self, level: usize) -> String { + match level.cmp(&self.prev.len()) { + Ordering::Equal => { + if let Some(n) = self.prev.pop() { + self.prev.push(n + 1); + } else { + self.prev.push(1); } } - if scen.has_steps() { - Ok((Some(scen), e.len())) - } else { - Ok((None, e.len())) + Ordering::Greater => { + self.prev.push(1); + } + Ordering::Less => { + assert!(!self.prev.is_empty()); + self.prev.pop(); + if let Some(n) = self.prev.pop() { + self.prev.push(n + 1); + } else { + self.prev.push(1); + } } } - } -} -#[cfg(test)] -mod test_extract { - use super::extract_scenario; - use super::visitor::Element; - 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), SubplotError>, - title: Option<&str>, - i: usize, - ) { - 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()); + let mut s = String::new(); + for i in self.prev.iter() { + if !s.is_empty() { + s.push('.'); + } + s.push_str(&i.to_string()); } - 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); + s } +} - #[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); - } +#[cfg(test)] +mod test_numberer { + use super::HeadingNumberer; #[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), - } + fn numbering() { + let mut n = HeadingNumberer::default(); + assert_eq!(n.number(1), "1"); + assert_eq!(n.number(2), "1.1"); + assert_eq!(n.number(1), "2"); + assert_eq!(n.number(2), "2.1"); } } |