use crate::visitor;
use crate::DataFile;
use crate::DataFiles;
use crate::LintingVisitor;
use crate::MatchedScenario;
use crate::Metadata;
use crate::PartialStep;
use crate::Scenario;
use crate::ScenarioStep;
use crate::Style;
use crate::{bindings::CaptureType, parser::parse_scenario_snippet};
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};
static SPECIAL_CLASSES: &[&str] = &["scenario", "file", "dot", "pikchr", "plantuml", "roadmap"];
static KNOWN_FILE_CLASSES: &[&str] = &["rust", "yaml", "python", "sh", "shell", "markdown", "bash"];
static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines", "noNumberLines"];
#[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,
}
}
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() {
return Err(linter.issues.remove(0));
}
let files = DataFiles::new(&mut ast);
Ok(Document::new(markdowns, ast, meta, files, style))
}
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);
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::ToBuffer(o) => o,
_ => return Err(SubplotError::NotJson),
};
let doc = Document::from_json(basedir, markdowns, &output, style)?;
Ok(doc)
}
pub fn ast(&self) -> Result<String> {
let json = serde_json::to_string(&self.ast)?;
Ok(json)
}
pub fn meta(&self) -> &Metadata {
&self.meta
}
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
}
pub fn files(&self) -> &[DataFile] {
self.files.files()
}
pub fn lint(&self) -> Result<()> {
self.check_doc_has_title()?;
self.check_filenames_are_unique()?;
self.check_block_classes()?;
Ok(())
}
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(())
}
fn check_doc_has_title(&self) -> Result<()> {
if self.meta().title().is_empty() {
Err(SubplotError::NoTitle)
} else {
Ok(())
}
}
fn check_block_classes(&self) -> Result<()> {
let mut visitor = visitor::BlockClassVisitor::default();
visitor.walk_pandoc(&mut self.ast.clone());
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());
}
let unknown_classes: Vec<_> = visitor
.classes
.difference(&known_classes)
.cloned()
.collect();
if !unknown_classes.is_empty() {
Err(SubplotError::UnknownClasses(unknown_classes.join(", ")))
} else {
Ok(())
}
}
pub fn check_named_files_exist(&mut self) -> Result<bool> {
let filenames: HashSet<_> = self
.files()
.iter()
.map(|f| f.filename().to_lowercase())
.collect();
let mut okay = true;
let scenarios = match self.matched_scenarios() {
Ok(scenarios) => scenarios,
Err(_) => return Ok(true), };
for scenario in scenarios {
for step in scenario.steps() {
for captured in step.parts() {
if let PartialStep::CapturedText { name, text } = captured {
if matches!(step.types().get(name.as_str()), Some(CaptureType::File))
&& !filenames.contains(&text.to_lowercase())
{
eprintln!("Found reference to unknown file {}", text);
okay = false;
}
}
}
}
}
Ok(okay)
}
pub fn typeset(&mut self) {
let mut visitor =
visitor::TypesettingVisitor::new(self.style.clone(), &self.meta.bindings());
visitor.walk_pandoc(&mut self.ast);
}
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)
}
pub fn matched_scenarios(&mut self) -> Result<Vec<MatchedScenario>> {
let scenarios = self.scenarios()?;
let bindings = self.meta().bindings();
scenarios
.iter()
.map(|scen| MatchedScenario::new(scen, bindings))
.collect()
}
}
fn extract_scenario(e: &[visitor::Element]) -> Result<(Option<Scenario>, usize)> {
if e.is_empty() {
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),
}
}
}