From e87a9335934a8e8317019e4ba13c8512be1dcb51 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 15 Jan 2023 18:12:00 +0200 Subject: chore: compile YamlMetadata::new only for tests YamlMetadata::new is only used in unit tests, so disable it when not testing. Sponsored-by: author --- src/ast.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ast.rs b/src/ast.rs index e638464..9e50af1 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -253,6 +253,7 @@ pub struct YamlMetadata { } impl YamlMetadata { + #[cfg(test)] fn new(yaml_text: &str) -> Result { trace!("Parsing YAML"); let meta: Self = serde_yaml::from_str(yaml_text)?; -- cgit v1.2.1 From b2d716a57fe0f8f9ecfe91995fd30fcb9cea7f84 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 22 Jan 2023 11:29:47 +0200 Subject: fix: markdown in test data A fenced code block can't start in middle of text, so add an empty line to avoid doing that. Sponsored-by: author --- subplot.md | 1 + 1 file changed, 1 insertion(+) diff --git a/subplot.md b/subplot.md index 0901133..d64b93b 100644 --- a/subplot.md +++ b/subplot.md @@ -2277,6 +2277,7 @@ This is a very simple Markdown file that uses every kind of inline markup in the title and chapter heading. To satisfy codegen, we *MUST* have a scenario here + ~~~~scenario when I do bar then bar was done -- cgit v1.2.1 From b940075214d31b632f62a0ba723e63b857f5e5f1 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 22 Jan 2023 11:30:31 +0200 Subject: refactor: add module for parsed Markdown This will eventually be the only module in Subplot that uses pandoc_ast or exposes types from that crate. When we replace pandoc_ast with something pulldown_cmark, only this module will need to change. Sponsored-by: author --- src/lib.rs | 1 + src/md.rs | 390 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 391 insertions(+) create mode 100644 src/md.rs diff --git a/src/lib.rs b/src/lib.rs index 4a3ae81..966118d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,7 @@ mod metadata; pub use metadata::Metadata; mod doc; +pub mod md; pub use doc::Document; pub use doc::{codegen, load_document, load_document_with_pullmark}; diff --git a/src/md.rs b/src/md.rs new file mode 100644 index 0000000..15fe57a --- /dev/null +++ b/src/md.rs @@ -0,0 +1,390 @@ +//! 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), + } + } +} -- cgit v1.2.1 From 27fac843cd0475026e96545b645c11f15a71d432 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 22 Jan 2023 11:32:12 +0200 Subject: refactor: drop abstract syntax tree built using pulldown_cmark We'll want to use the new Markdown type instead. Sponsored-by: author --- src/ast.rs | 222 +------------------------------------------------------------ src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 221 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 9e50af1..c48a1e7 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,7 +1,6 @@ use lazy_static::lazy_static; use log::trace; -use pandoc_ast::{Attr, Block, Inline, Map, MetaValue, Pandoc}; -use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; +use pandoc_ast::{Map, MetaValue}; use regex::Regex; use serde::Deserialize; use serde_yaml::{Mapping, Value}; @@ -17,202 +16,6 @@ lazy_static! { static ref TRAILING_YAML_PATTERN: Regex = Regex::new(r"(?P(.*\n)*)\n*(?P-{3,}\n([^.].*\n)*\.{3,}\n)(?:\S*\n)*$").unwrap(); } -/// An abstract syntax tree representation of a Markdown file. -/// -/// This represents a Markdown file as an abstract syntax tree -/// compatible with Pandoc's AST. The document YAML metadata MUST be -/// at the top or bottom of the file, excluding leading or trailing -/// empty lines. -#[derive(Debug)] -pub struct AbstractSyntaxTree { - blocks: Vec, - meta: YamlMetadata, -} - -impl AbstractSyntaxTree { - /// Create a new AST. - pub fn new(meta: YamlMetadata, markdown: &str) -> Self { - let blocks = parse_blocks(markdown); - Self { blocks, meta } - } - - /// Return a Pandoc-compatible AST. - pub fn to_pandoc(&self) -> Pandoc { - Pandoc { - meta: self.meta.to_map(), - blocks: self.blocks.clone(), - pandoc_api_version: vec![1, 20], - } - } -} - -// Parse Markdown into a sequence of Blocks. -fn parse_blocks(markdown: &str) -> Vec { - trace!("Parsing blocks"); - - // Define the Markdown parser. - let mut options = Options::empty(); - options.insert(Options::ENABLE_TABLES); - options.insert(Options::ENABLE_FOOTNOTES); - options.insert(Options::ENABLE_STRIKETHROUGH); - options.insert(Options::ENABLE_TASKLISTS); - options.insert(Options::ENABLE_SMART_PUNCTUATION); - let parser = Parser::new_ext(markdown, options); - - // The sequence of blocks that represents the parsed document. - let mut blocks = vec![]; - - // The current set of inline elements we've collected. This gets - // emptied whenever we finish a block. - let mut inlines: Vec = vec![]; - - for event in parser { - trace!("Parsing event: {:?}", event); - match event { - // We ignore these for now. They're not needed for codegen. - Event::Html(_) - | Event::FootnoteReference(_) - | Event::SoftBreak - | Event::HardBreak - | Event::Rule - | Event::TaskListMarker(_) => (), - - // Inline text of various kinds. - Event::Text(text) => inlines.push(inline_text(&text)), - Event::Code(text) => inlines.push(inline_code(&text)), - - // We only handle the end events. - Event::Start(_) => (), - - // End of a block or inline. - Event::End(tag) => match tag { - // Collect inline elements for later inclusion in a block. - Tag::Emphasis | Tag::Strong | Tag::Strikethrough => { - inline_from_inlines(&tag, &mut inlines) - } - Tag::Paragraph => blocks.push(paragraph(&mut inlines)), - Tag::Heading(level, _fragment, _classes) => { - blocks.push(heading(level as i64, &mut inlines)) - } - Tag::CodeBlock(kind) => blocks.push(code_block(&kind, &mut inlines)), - Tag::Image(_link, dest, title) => blocks.push(image_block(&dest, &title)), - // We don't handle anything else yet. - _ => (), - }, - } - } - - // We MUST have emptied all inline elements. - // assert!(inlines.is_empty()); - - trace!("Parsing blocks: OK"); - blocks -} - -fn inline_text(text: &str) -> Inline { - Inline::Str(text.to_string()) -} - -fn inline_code(text: &str) -> Inline { - let attr = ("".to_string(), vec![], vec![]); - Inline::Code(attr, text.to_string()) -} - -fn paragraph(inlines: &mut Vec) -> Block { - Block::Para(std::mem::take(inlines)) -} - -fn heading(level: i64, inlines: &mut Vec) -> Block { - let attr = ("".to_string(), vec![], vec![]); - Block::Header(level, attr, std::mem::take(inlines)) -} - -fn image_block(dest: &str, title: &str) -> Block { - let attr = ("".to_string(), vec![], vec![]); - Block::Para(vec![Inline::Image( - attr, - vec![], - (dest.to_string(), title.to_string()), - )]) -} - -fn code_block(kind: &CodeBlockKind, inlines: &mut Vec) -> Block { - trace!("code block: {:?}", kind); - let attr = if let CodeBlockKind::Fenced(lang) = kind { - trace!("fenced code block, lang={:?}", lang); - parse_code_block_attrs(lang) - } else { - trace!("indented code block"); - parse_code_block_attrs("") - }; - trace!("code block attrs: {:?}", attr); - let mut code = String::new(); - for inline in inlines.drain(0..) { - let text = plain_text_inline(inline); - code.push_str(&text); - } - // pulldown_cmark and pandoc differ in their codeblock handling, - // pulldown_cmark has an extra newline which we trim for now to be - // compatible with pandoc's parsing - if !code.is_empty() { - assert_eq!(code.pop(), Some('\n')); - } - Block::CodeBlock(attr, code) -} - -fn plain_text_inline(inline: Inline) -> String { - match inline { - Inline::Str(text) => text, - Inline::Code(_, text) => text, - Inline::Emph(inlines) => { - let mut text = String::new(); - for inline in inlines { - text.push_str(&plain_text_inline(inline)); - } - text - } - _ => panic!("not text in code block: {:?}", inline), - } -} - -fn parse_code_block_attrs(attrs: &str) -> Attr { - trace!("parsing code block attrs: {:?}", attrs); - let mut id = "".to_string(); - let mut classes = vec![]; - let mut keyvalues = vec![]; - if attrs.starts_with('{') && attrs.ends_with('}') { - let attrs = &attrs[1..attrs.len() - 1]; - for word in attrs.split_ascii_whitespace() { - if let Some(x) = word.strip_prefix('#') { - id = x.to_string(); - } else if let Some(x) = word.strip_prefix('.') { - classes.push(x.to_string()); - } else if let Some(i) = word.find('=') { - let k = &word[..i]; - let v = &word[i + 1..]; - keyvalues.push((k.to_string(), v.to_string())); - } - } - } else if !attrs.is_empty() { - classes.push(attrs.to_string()); - } - (id, classes, keyvalues) -} - -fn inline_from_inlines(tag: &Tag, inlines: &mut Vec) { - let new_inlines = inlines.clone(); - inlines.clear(); - - let inline = match tag { - Tag::Emphasis => Inline::Emph(new_inlines), - Tag::Strong => Inline::Strong(new_inlines), - Tag::Strikethrough => Inline::Strikeout(new_inlines), - _ => unreachable!(), - }; - - inlines.push(inline); -} - /// Errors from Markdown parsing. #[derive(Debug, thiserror::Error)] pub enum Error { @@ -368,30 +171,9 @@ fn meta_path_bufs(v: &[PathBuf]) -> MetaValue { #[cfg(test)] mod test { - use super::{parse_code_block_attrs, YamlMetadata}; + use super::YamlMetadata; use std::path::{Path, PathBuf}; - #[test] - fn code_block_attrs() { - assert_eq!(parse_code_block_attrs(""), ("".to_string(), vec![], vec![])); - assert_eq!( - parse_code_block_attrs("foo"), - ("".to_string(), vec!["foo".to_string()], vec![]) - ); - assert_eq!( - parse_code_block_attrs("{#foo}"), - ("foo".to_string(), vec![], vec![]) - ); - assert_eq!( - parse_code_block_attrs("{#foo .file bar=yo}"), - ( - "foo".to_string(), - vec!["file".to_string()], - vec![("bar".to_string(), "yo".to_string())] - ) - ); - } - #[test] fn full_meta() { let meta = YamlMetadata::new( diff --git a/src/lib.rs b/src/lib.rs index 966118d..747b375 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,4 +74,4 @@ mod codegen; pub use codegen::generate_test_program; mod ast; -pub use ast::{AbstractSyntaxTree, YamlMetadata}; +pub use ast::YamlMetadata; -- cgit v1.2.1 From e741180b13826331c886b996423df80b62da4f25 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 22 Jan 2023 11:33:51 +0200 Subject: refactor: create Metadata from YamlMetadata without pandoc_ast We don't want to use pandoc_ast outside of the md module. Sponsored-by: author --- src/ast.rs | 55 ++++++++++++++ src/metadata.rs | 230 ++++++++++---------------------------------------------- 2 files changed, 93 insertions(+), 192 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index c48a1e7..b60df71 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -60,6 +60,7 @@ impl YamlMetadata { fn new(yaml_text: &str) -> Result { trace!("Parsing YAML"); let meta: Self = serde_yaml::from_str(yaml_text)?; + trace!("parsed metadata: {:#?}", meta); Ok(meta) } @@ -68,6 +69,60 @@ impl YamlMetadata { &self.markdowns[0] } + /// Title. + pub fn title(&self) -> &str { + &self.title + } + + /// Subtitle. + pub fn subtitle(&self) -> Option<&str> { + self.subtitle.as_deref() + } + + /// Date. + pub fn date(&self) -> Option<&str> { + self.date.as_deref() + } + + /// Authors. + pub fn authors(&self) -> Option<&[String]> { + self.authors.as_deref() + } + + /// Names of bindings files. + pub fn bindings_filenames(&self) -> Option<&[PathBuf]> { + self.bindings.as_deref() + } + + /// Impls section. + pub fn impls(&self) -> &BTreeMap> { + &self.impls + } + + /// Bibliographies. + pub fn bibliographies(&self) -> Option<&[PathBuf]> { + self.bibliography.as_deref() + } + + /// Classes.. + pub fn classes(&self) -> Option<&[String]> { + self.classes.as_deref() + } + + /// Documentclass. + pub fn documentclass(&self) -> Option<&str> { + self.documentclass.as_deref() + } + + /// Pandoc metadata. + pub fn pandoc(&self) -> Option<&HashMap> { + if let Some(x) = &self.pandoc { + Some(x) + } else { + None + } + } + /// Convert into a pandoc_ast::Map. pub fn to_map(&self) -> Map { trace!("Creating metadata map from parsed YAML"); diff --git a/src/metadata.rs b/src/metadata.rs index 261017a..e88c732 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -1,14 +1,11 @@ use crate::{Bindings, SubplotError, TemplateSpec, YamlMetadata}; +use log::trace; use std::collections::HashMap; use std::fmt::Debug; use std::ops::Deref; use std::path::{Path, PathBuf}; -use pandoc_ast::{Inline, Map, MetaValue}; - -use log::trace; - /// Metadata of a document, as needed by Subplot. #[derive(Debug)] pub struct Metadata { @@ -31,60 +28,58 @@ pub struct DocumentImpl { } impl Metadata { - /// Construct a Metadata from a Document, if possible. - pub fn new

( + /// Create from YamlMetadata. + pub fn from_yaml_metadata

( basedir: P, - meta: &YamlMetadata, + yaml: &YamlMetadata, template: Option<&str>, - ) -> Result + ) -> Result where P: AsRef + Debug, { - let map = meta.to_map(); - let title = get_title(&map); - let date = get_date(&map); - let bindings_filenames = get_bindings_filenames(&map); - let bibliographies = get_bibliographies(basedir.as_ref(), &map); - let classes = get_classes(&map); - trace!("Loaded basic metadata"); + let mut bindings = Bindings::new(); + let bindings_filenames = if let Some(filenames) = yaml.bindings_filenames() { + get_bindings(filenames, &mut bindings, template)?; + filenames.iter().map(|p| p.to_path_buf()).collect() + } else { + vec![] + }; let mut impls = HashMap::new(); - if let Some(raw_impls) = map.get("impls") { - match raw_impls { - MetaValue::MetaMap(raw_impls) => { - for (impl_name, functions_filenames) in raw_impls.iter() { - let template_spec = load_template_spec(impl_name)?; - let filenames = pathbufs("", functions_filenames); - let docimpl = DocumentImpl::new(template_spec, filenames); - impls.insert(impl_name.to_string(), docimpl); - } - } - _ => { - trace!("Ignoring unknown raw implementation value"); - } - } + for (impl_name, functions_filenames) in yaml.impls().iter() { + let template_spec = load_template_spec(impl_name)?; + let filenames = pathbufs("", functions_filenames); + let docimpl = DocumentImpl::new(template_spec, filenames); + impls.insert(impl_name.to_string(), docimpl); } - let template = template.or_else(|| impls.keys().next().map(String::as_str)); - - let mut bindings = Bindings::new(); + let bibliographies = if let Some(v) = yaml.bibliographies() { + v.iter().map(|s| s.to_path_buf()).collect() + } else { + vec![] + }; - get_bindings(&bindings_filenames, &mut bindings, template)?; + let classes = if let Some(v) = yaml.classes() { + v.iter().map(|s| s.to_string()).collect() + } else { + vec![] + }; - trace!("Loaded all metadata successfully"); - - Ok(Metadata { + let meta = Self { basedir: basedir.as_ref().to_path_buf(), - title, - date, - markdown_filename: meta.markdown().into(), + title: yaml.title().into(), + date: yaml.date().map(|s| s.into()), + markdown_filename: yaml.markdown().into(), bindings_filenames, bindings, impls, bibliographies, classes, - }) + }; + trace!("metadata: {:#?}", meta); + + Ok(meta) } /// Return title of document. @@ -152,24 +147,6 @@ impl DocumentImpl { } } -type Mapp = Map; - -fn get_title(map: &Mapp) -> String { - if let Some(s) = get_string(map, "title") { - s - } else { - "".to_string() - } -} - -fn get_date(map: &Mapp) -> Option { - get_string(map, "date") -} - -fn get_bindings_filenames(map: &Mapp) -> Vec { - get_paths("", map, "bindings") -} - fn load_template_spec(template: &str) -> Result { let mut spec_path = PathBuf::from(template); spec_path.push("template"); @@ -177,143 +154,12 @@ fn load_template_spec(template: &str) -> Result { TemplateSpec::from_file(&spec_path) } -fn get_paths

(basedir: P, map: &Mapp, field: &str) -> Vec -where - P: AsRef, -{ - match map.get(field) { - None => vec![], - Some(v) => pathbufs(basedir, v), - } -} - -fn get_string(map: &Mapp, field: &str) -> Option { - let v = match map.get(field) { - None => return None, - Some(s) => s, - }; - let v = match v { - pandoc_ast::MetaValue::MetaString(s) => s.to_string(), - pandoc_ast::MetaValue::MetaInlines(vec) => join(vec), - _ => panic!("don't know how to handle: {:?}", v), - }; - Some(v) -} - -fn get_bibliographies

(basedir: P, map: &Mapp) -> Vec -where - P: AsRef, -{ - let v = match map.get("bibliography") { - None => return vec![], - Some(s) => s, - }; - pathbufs(basedir, v) -} - -fn pathbufs

(basedir: P, v: &MetaValue) -> Vec -where - P: AsRef, -{ - let mut bufs = vec![]; - push_pathbufs(basedir, v, &mut bufs); - bufs -} - -fn get_classes(map: &Mapp) -> Vec { - let mut ret = Vec::new(); - if let Some(classes) = map.get("classes") { - push_strings(classes, &mut ret); - } - ret -} - -fn push_strings(v: &MetaValue, strings: &mut Vec) { - match v { - MetaValue::MetaString(s) => strings.push(s.to_string()), - MetaValue::MetaInlines(vec) => strings.push(join(vec)), - MetaValue::MetaList(values) => { - for value in values { - push_strings(value, strings); - } - } - _ => panic!("don't know how to handle: {:?}", v), - }; -} - -fn push_pathbufs

(basedir: P, v: &MetaValue, bufs: &mut Vec) +fn pathbufs

(basedir: P, v: &[PathBuf]) -> Vec where P: AsRef, { - match v { - MetaValue::MetaString(s) => bufs.push(basedir.as_ref().join(Path::new(s))), - MetaValue::MetaInlines(vec) => bufs.push(basedir.as_ref().join(Path::new(&join(vec)))), - MetaValue::MetaList(values) => { - for value in values { - push_pathbufs(basedir.as_ref(), value, bufs); - } - } - _ => panic!("don't know how to handle: {:?}", v), - }; -} - -fn join(vec: &[Inline]) -> String { - let mut buf = String::new(); - join_into_buffer(vec, &mut buf); - buf -} - -fn join_into_buffer(vec: &[Inline], buf: &mut String) { - for item in vec { - match item { - pandoc_ast::Inline::Str(s) => buf.push_str(s), - pandoc_ast::Inline::Code(_, s) => buf.push_str(s), - pandoc_ast::Inline::Emph(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::Strong(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::Strikeout(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::Superscript(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::Subscript(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::SmallCaps(v) => join_into_buffer(v, buf), - pandoc_ast::Inline::Space => buf.push(' '), - pandoc_ast::Inline::SoftBreak => buf.push(' '), - pandoc_ast::Inline::LineBreak => buf.push(' '), - pandoc_ast::Inline::Quoted(qtype, v) => { - let quote = match qtype { - pandoc_ast::QuoteType::SingleQuote => '\'', - pandoc_ast::QuoteType::DoubleQuote => '"', - }; - buf.push(quote); - join_into_buffer(v, buf); - buf.push(quote); - } - _ => panic!("unknown pandoc_ast::Inline component {:?}", item), - } - } -} - -#[cfg(test)] -mod test_join { - use super::join; - use pandoc_ast::{Inline, QuoteType}; - - #[test] - fn join_all_kinds() { - let v = vec![ - Inline::Str("a".to_string()), - Inline::Emph(vec![Inline::Str("b".to_string())]), - Inline::Strong(vec![Inline::Str("c".to_string())]), - Inline::Strikeout(vec![Inline::Str("d".to_string())]), - Inline::Superscript(vec![Inline::Str("e".to_string())]), - Inline::Subscript(vec![Inline::Str("f".to_string())]), - Inline::SmallCaps(vec![Inline::Str("g".to_string())]), - Inline::Space, - Inline::SoftBreak, - Inline::Quoted(QuoteType::SingleQuote, vec![Inline::Str("h".to_string())]), - Inline::LineBreak, - Inline::Quoted(QuoteType::DoubleQuote, vec![Inline::Str("i".to_string())]), - ]; - assert_eq!(join(&v), r#"abcdefg 'h' "i""#); - } + let basedir = basedir.as_ref(); + v.iter().map(|p| basedir.join(p)).collect() } fn get_bindings

( -- cgit v1.2.1 From d00d3013168f39c136bd877ff3d4328e29e3d991 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 22 Jan 2023 11:34:56 +0200 Subject: refactor: use new Markdown type instead of pandoc_ast visitors Sponsored-by: author --- src/doc.rs | 321 +++++++------------------------------------------------- src/embedded.rs | 10 +- 2 files changed, 39 insertions(+), 292 deletions(-) diff --git a/src/doc.rs b/src/doc.rs index cc6a616..7a7e64c 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -1,30 +1,27 @@ use crate::ast; +use crate::bindings::CaptureType; use crate::generate_test_program; use crate::get_basedir_from; -use crate::visitor; +use crate::md::Markdown; 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::{Warning, Warnings}; use std::collections::HashSet; +use std::convert::TryFrom; use std::default::Default; use std::fmt::Debug; use std::fs::read; use std::ops::Deref; use std::path::{Path, PathBuf}; -use pandoc_ast::{MutVisitor, Pandoc}; - use log::{error, trace}; /// The set of known (special) classes which subplot will always recognise @@ -44,19 +41,6 @@ static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines", "noNumberLines"]; /// 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; @@ -84,7 +68,7 @@ static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines", "noNumberLines"]; pub struct Document { subplot: PathBuf, markdowns: Vec, - ast: Pandoc, + md: Markdown, meta: Metadata, files: EmbeddedFiles, style: Style, @@ -95,20 +79,22 @@ impl Document { fn new( subplot: PathBuf, markdowns: Vec, - ast: Pandoc, + md: Markdown, meta: Metadata, files: EmbeddedFiles, style: Style, ) -> Document { - Document { + let doc = Document { subplot, markdowns, - ast, + md, meta, files, style, warnings: Warnings::default(), - } + }; + trace!("Document::new -> {:#?}", doc); + doc } /// Return all warnings about this document. @@ -121,23 +107,22 @@ impl Document { subplot: PathBuf, markdowns: Vec, yamlmeta: &ast::YamlMetadata, - mut ast: Pandoc, + mut md: Markdown, style: Style, template: Option<&str>, ) -> Result where P: AsRef + 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() { + let meta = Metadata::from_yaml_metadata(basedir, yamlmeta, template)?; + trace!("metadata from YAML: {:#?}", meta); + let mut issues = md.lint(); + if !issues.is_empty() { // Currently we can't really return more than one error so return one - return Err(linter.issues.remove(0)); + return Err(issues.remove(0)); } - let files = EmbeddedFiles::new(&mut ast); - let doc = Document::new(subplot, markdowns, ast, meta, files, style); + let files = md.embedded_files(); + let doc = Document::new(subplot, markdowns, md, meta, files, style); trace!("Loaded from JSON OK"); Ok(doc) } @@ -160,42 +145,20 @@ 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 md = Markdown::try_from(mdfile.as_path())?; + md.set_metadata(&meta); + let markdowns = vec![mdfile]; - 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); - - // 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, + md, style, template, )?; @@ -204,44 +167,12 @@ impl Document { 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 { - 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 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 { - let json = serde_json::to_string(&self.ast).map_err(SubplotError::AstJson)?; - Ok(json) + pub fn ast(&mut self) -> Result { + self.md.to_json() } /// Return the document's metadata. @@ -284,11 +215,8 @@ impl Document { 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()); - } + let mut images = self.md.images(); + names.append(&mut images); names } @@ -299,7 +227,7 @@ impl Document { } /// Check the document for common problems. - pub fn lint(&self) -> Result<(), SubplotError> { + pub fn lint(&mut self) -> Result<(), SubplotError> { trace!("Linting document"); self.check_doc_has_title()?; self.check_filenames_are_unique()?; @@ -330,12 +258,9 @@ 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()); + fn check_block_classes(&mut self) -> Result<(), SubplotError> { + let classes_in_doc = self.md.block_classes(); + // Build the set of known good classes let mut known_classes: HashSet = HashSet::new(); for class in std::iter::empty() @@ -347,11 +272,7 @@ impl Document { 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() { @@ -457,28 +378,15 @@ 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()); + let warnings = self.md.typeset(self.style.clone(), self.meta.bindings()); + for w in warnings { + self.warnings.push(w); + } } /// Return all scenarios in a document. pub fn scenarios(&mut self) -> Result, SubplotError> { - let mut visitor = visitor::StructureVisitor::new(); - visitor.walk_pandoc(&mut self.ast); - - 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; - } - Ok(scenarios) + self.md.scenarios() } /// Return matched scenarios in a document. @@ -564,7 +472,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) } @@ -617,156 +525,3 @@ impl CodegenOutput { Self { template, doc } } } - -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), - } - } -} diff --git a/src/embedded.rs b/src/embedded.rs index c868054..e71fa54 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -1,4 +1,3 @@ -use pandoc_ast::{MutVisitor, Pandoc}; use serde::{Deserialize, Serialize}; /// A data file embedded in the document. @@ -26,19 +25,12 @@ impl EmbeddedFile { } /// A collection of data files embedded in document. -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize)] pub struct EmbeddedFiles { files: Vec, } impl EmbeddedFiles { - /// Create new set of data files. - pub fn new(ast: &mut Pandoc) -> EmbeddedFiles { - let mut files = EmbeddedFiles { files: vec![] }; - files.walk_pandoc(ast); - files - } - /// Return slice of all data files. pub fn files(&self) -> &[EmbeddedFile] { &self.files -- cgit v1.2.1 From 1bd5ab49f17699fd3ae08cd847221ce969e38337 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 22 Jan 2023 11:40:51 +0200 Subject: chore: unused code from ast.rs Sponsored-by: author --- src/ast.rs | 106 +------------------------------------------------------------ 1 file changed, 1 insertion(+), 105 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index b60df71..ed163f0 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,9 +1,7 @@ use lazy_static::lazy_static; -use log::trace; -use pandoc_ast::{Map, MetaValue}; use regex::Regex; use serde::Deserialize; -use serde_yaml::{Mapping, Value}; +use serde_yaml::Value; use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; @@ -58,9 +56,7 @@ pub struct YamlMetadata { impl YamlMetadata { #[cfg(test)] fn new(yaml_text: &str) -> Result { - trace!("Parsing YAML"); let meta: Self = serde_yaml::from_str(yaml_text)?; - trace!("parsed metadata: {:#?}", meta); Ok(meta) } @@ -122,106 +118,6 @@ impl YamlMetadata { None } } - - /// Convert into a pandoc_ast::Map. - pub fn to_map(&self) -> Map { - trace!("Creating metadata map from parsed YAML"); - let mut map: Map = Map::new(); - - map.insert("title".into(), meta_string(&self.title)); - - if let Some(v) = &self.subtitle { - map.insert("subtitle".into(), meta_string(v)); - } - - if let Some(authors) = &self.authors { - let authors: Vec = authors - .iter() - .map(|s| MetaValue::MetaString(s.into())) - .collect(); - map.insert("author".into(), MetaValue::MetaList(authors)); - } - - if let Some(v) = &self.date { - map.insert("date".into(), meta_string(v)); - } - - if let Some(v) = &self.classes { - map.insert("classes".into(), meta_strings(v)); - } - - if !self.impls.is_empty() { - let impls = self - .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) = &self.bibliography { - map.insert("bibliography".into(), meta_path_bufs(v)); - } - - if let Some(v) = &self.bindings { - map.insert("bindings".into(), meta_path_bufs(v)); - } - - if let Some(v) = &self.documentclass { - map.insert("documentclass".into(), meta_string(v)); - } - - if let Some(pandoc) = &self.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()) } #[cfg(test)] -- cgit v1.2.1