summaryrefslogtreecommitdiff
path: root/src/doc.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/doc.rs')
-rw-r--r--src/doc.rs749
1 files changed, 404 insertions, 345 deletions
diff --git a/src/doc.rs b/src/doc.rs
index cc6a616..ff52b79 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -1,21 +1,22 @@
-use crate::ast;
+use crate::bindings::CaptureType;
use crate::generate_test_program;
use crate::get_basedir_from;
-use crate::visitor;
+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::LintingVisitor;
use crate::MatchedScenario;
-use crate::Metadata;
use crate::PartialStep;
use crate::Scenario;
-use crate::ScenarioStep;
use crate::Style;
use crate::SubplotError;
-use crate::YamlMetadata;
-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;
@@ -23,10 +24,11 @@ use std::fs::read;
use std::ops::Deref;
use std::path::{Path, PathBuf};
-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] = &[
@@ -37,26 +39,17 @@ 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;
@@ -83,70 +76,42 @@ static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines", "noNumberLines"];
#[derive(Debug)]
pub struct Document {
subplot: PathBuf,
- markdowns: Vec<PathBuf>,
- ast: Pandoc,
+ markdowns: Vec<Markdown>,
meta: Metadata,
files: EmbeddedFiles,
style: Style,
- warnings: Warnings,
}
impl Document {
fn new(
subplot: PathBuf,
- markdowns: Vec<PathBuf>,
- ast: Pandoc,
+ markdowns: Vec<Markdown>,
meta: Metadata,
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()
+ };
+ trace!("Document::new -> {:#?}", doc);
+ doc
}
- fn from_ast<P>(
- basedir: P,
- subplot: PathBuf,
- markdowns: Vec<PathBuf>,
- yamlmeta: &ast::YamlMetadata,
- mut ast: Pandoc,
- style: Style,
- template: Option<&str>,
- ) -> Result<Document, SubplotError>
- where
- P: AsRef<Path> + Debug,
- {
- let meta = Metadata::new(basedir, yamlmeta, 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 = EmbeddedFiles::new(&mut ast);
- let doc = Document::new(subplot, markdowns, ast, meta, files, style);
- trace!("Loaded from JSON OK");
- Ok(doc)
+ 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,
@@ -160,88 +125,216 @@ impl Document {
);
let meta = load_metadata_from_yaml_file(filename)?;
+ trace!("metadata from YAML file: {:#?}", meta);
- let mdfile = meta.markdown();
- let mdfile = basedir.join(mdfile);
- let markdowns = vec![mdfile.clone()];
-
- let mut pandoc = pandoc::new();
- pandoc.add_input(&mdfile);
- 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 mut markdowns = vec![];
+ for filename in meta.markdowns() {
+ let filename = basedir.join(filename);
+ markdowns.push(Markdown::load_file(&filename)?);
+ }
- // Add external Pandoc filters.
- crate::policy::add_citeproc(&mut pandoc);
-
- trace!(
- "Invoking Pandoc to parse document {:?} into AST as JSON",
- mdfile,
- );
- let json = match pandoc.execute().map_err(SubplotError::Pandoc)? {
- pandoc::PandocOutput::ToBuffer(o) => o,
- _ => return Err(SubplotError::NotJson),
- };
- trace!("Pandoc was happy");
-
- trace!("Parsing document AST as JSON...");
- let mut ast: Pandoc = serde_json::from_str(&json).map_err(SubplotError::AstJson)?;
- ast.meta = meta.to_map();
- let doc = Self::from_ast(
- basedir,
- filename.into(),
- markdowns,
- &meta,
- ast,
- style,
- template,
- )?;
+ 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!("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 meta = load_metadata_from_yaml_file(filename)?;
- let mdfile = meta.markdown();
- let mdfile = basedir.join(mdfile);
- let markdown = std::fs::read_to_string(&mdfile)
- .map_err(|err| SubplotError::ReadFile(mdfile.clone(), err))?;
- let ast = ast::AbstractSyntaxTree::new(meta.clone(), &markdown);
-
- trace!("Parsed document OK");
- Self::from_ast(
- basedir,
- filename.into(),
- vec![mdfile],
- &meta,
- ast.to_pandoc(),
- style,
- template,
- )
+ /// 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));
+ }
+
+ 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.
@@ -249,11 +342,16 @@ impl 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> {
+ pub fn sources(&self, template: Option<&str>) -> Vec<PathBuf> {
let mut names = vec![self.subplot.clone()];
for x in self.meta().bindings_filenames() {
@@ -276,25 +374,20 @@ impl 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) -> &[EmbeddedFile] {
+ pub fn embedded_files(&self) -> &[EmbeddedFile] {
self.files.files()
}
@@ -302,16 +395,33 @@ impl 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));
}
@@ -331,27 +441,20 @@ impl 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() {
@@ -361,11 +464,61 @@ impl 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();
@@ -378,11 +531,16 @@ impl 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(),
));
@@ -396,9 +554,13 @@ impl 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();
@@ -410,7 +572,12 @@ impl 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());
}
@@ -419,8 +586,7 @@ impl 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
@@ -431,7 +597,7 @@ impl 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) {
@@ -444,7 +610,7 @@ impl 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(),
));
@@ -456,36 +622,28 @@ impl 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",
@@ -514,14 +672,12 @@ impl 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: ast::YamlMetadata = serde_yaml::from_slice(&yaml)
+ 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,
@@ -564,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)
}
@@ -591,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);
@@ -618,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");
}
}