//! A parsed Markdown document. use crate::{ parse_scenario_snippet, visitor, Bindings, EmbeddedFiles, LintingVisitor, Scenario, ScenarioStep, Style, SubplotError, Warning, YamlMetadata, }; use log::trace; use pandoc_ast::{Map, MetaValue, MutVisitor, Pandoc}; use serde_yaml::{Mapping, Value}; use std::collections::HashSet; use std::convert::TryFrom; use std::path::{Path, PathBuf}; /// A parsed Markdown document. #[derive(Debug)] pub struct Markdown { pandoc: Pandoc, } impl Markdown { fn new(pandoc: Pandoc) -> Self { Self { pandoc } } fn pandoc(&mut self) -> &mut Pandoc { &mut self.pandoc } /// Set document metadata from subplot. pub fn set_metadata(&mut self, meta: &YamlMetadata) { self.pandoc.meta = to_pandoc_meta(meta); } /// JSON representation of Pandoc AST. pub fn to_json(&self) -> Result { let json = serde_json::to_string(&self.pandoc).map_err(SubplotError::AstJson)?; Ok(json) } /// Find problems. pub fn lint(&mut self) -> Vec { let mut linter = LintingVisitor::default(); linter.walk_pandoc(self.pandoc()); linter.issues } /// Find included images. pub fn images(&mut self) -> Vec { let mut names = vec![]; let mut visitor = visitor::ImageVisitor::new(); visitor.walk_pandoc(self.pandoc()); for x in visitor.images().iter() { names.push(x.to_path_buf()); } names } /// Find classes used for fenced blocks. pub fn block_classes(&mut self) -> HashSet { 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(self.pandoc()); visitor.classes } /// Typeset. pub fn typeset(&mut self, style: Style, bindings: &Bindings) -> Vec { let mut visitor = visitor::TypesettingVisitor::new(style, bindings); visitor.walk_pandoc(self.pandoc()); visitor.warnings().warnings().to_vec() } /// Find scenarios. pub fn scenarios(&mut self) -> Result, SubplotError> { trace!( "Metadata::scenarios: looking for scenarios: {:#?}", self.pandoc ); let mut visitor = visitor::StructureVisitor::new(); visitor.walk_pandoc(self.pandoc()); trace!( "Metadata::scenarios: visitor found {} elements: {:#?}", visitor.elements.len(), visitor.elements ); let mut scenarios: Vec = 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; } trace!("Metadata::scenarios: found {} scenarios", scenarios.len()); Ok(scenarios) } /// Find embedded files. pub fn embedded_files(&mut self) -> EmbeddedFiles { let mut files = EmbeddedFiles::default(); files.walk_pandoc(self.pandoc()); files } } fn to_pandoc_meta(yaml: &YamlMetadata) -> Map { trace!("Creating metadata map from parsed YAML: {:#?}", yaml); let mut map: Map = Map::new(); map.insert("title".into(), meta_string(yaml.title())); if let Some(v) = &yaml.subtitle() { map.insert("subtitle".into(), meta_string(v)); } if let Some(authors) = yaml.authors() { let authors: Vec = authors .iter() .map(|s| MetaValue::MetaString(s.into())) .collect(); map.insert("author".into(), MetaValue::MetaList(authors)); } if let Some(v) = yaml.date() { map.insert("date".into(), meta_string(v)); } if let Some(classes) = yaml.classes() { map.insert("classes".into(), meta_strings(classes)); } if !yaml.impls().is_empty() { let impls = yaml .impls() .iter() .map(|(k, v)| (k.to_owned(), Box::new(meta_path_bufs(v)))) .collect(); map.insert("impls".into(), MetaValue::MetaMap(impls)); } if let Some(v) = yaml.bibliographies() { map.insert("bibliography".into(), meta_path_bufs(v)); } if let Some(v) = yaml.bindings_filenames() { map.insert("bindings".into(), meta_path_bufs(v)); } if let Some(v) = yaml.documentclass() { map.insert("documentclass".into(), meta_string(v)); } if let Some(pandoc) = yaml.pandoc() { for (key, value) in pandoc.iter() { map.insert(key.to_string(), value_to_pandoc(value)); } } trace!("Created metadata map from parsed YAML"); map } fn mapping_to_pandoc(mapping: &Mapping) -> MetaValue { let mut map = Map::new(); for (key, value) in mapping.iter() { let key = if let MetaValue::MetaString(s) = value_to_pandoc(key) { s } else { panic!("key not a string: {:?}", key); }; map.insert(key, Box::new(value_to_pandoc(value))); } MetaValue::MetaMap(map) } fn value_to_pandoc(data: &Value) -> MetaValue { match data { Value::Null => unreachable!("null not OK"), Value::Number(_) => unreachable!("number not OK"), Value::Sequence(_) => unreachable!("sequence not OK"), Value::Bool(b) => MetaValue::MetaBool(*b), Value::String(s) => MetaValue::MetaString(s.clone()), Value::Mapping(mapping) => mapping_to_pandoc(mapping), } } fn meta_string(s: &str) -> MetaValue { MetaValue::MetaString(s.to_string()) } fn meta_strings(v: &[String]) -> MetaValue { MetaValue::MetaList(v.iter().map(|s| meta_string(s)).collect()) } fn meta_path_buf(p: &Path) -> MetaValue { meta_string(&p.display().to_string()) } fn meta_path_bufs(v: &[PathBuf]) -> MetaValue { MetaValue::MetaList(v.iter().map(|p| meta_path_buf(p)).collect()) } impl TryFrom<&Path> for Markdown { type Error = SubplotError; fn try_from(filename: &Path) -> Result { trace!("parsing file as markdown: {}", filename.display()); 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. crate::policy::add_citeproc(&mut pandoc); let json = match pandoc.execute().map_err(SubplotError::Pandoc)? { pandoc::PandocOutput::ToBuffer(o) => o, _ => return Err(SubplotError::NotJson), }; let ast: Pandoc = serde_json::from_str(&json).map_err(SubplotError::AstJson)?; Ok(Self::new(ast)) } } fn extract_scenario(e: &[visitor::Element]) -> Result<(Option, 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()); } } } } 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::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, 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()); } assert_eq!(actual_i, i); } #[test] fn returns_nothing_if_there_is_no_scenario() { let elements: Vec = 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), } } }