summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers+gitlab@digital-scurf.org>2023-01-25 09:09:08 +0000
committerDaniel Silverstone <dsilvers+gitlab@digital-scurf.org>2023-01-25 09:09:08 +0000
commitfa5765189eef33bed301479410c4a53dc274acf0 (patch)
treee3e7ebe4c675f3966ffd79284ed317b4698f298d
parent4a8f87b12aa73b17c88dae2a4a4ba16dd21947af (diff)
parent1bd5ab49f17699fd3ae08cd847221ce969e38337 (diff)
downloadsubplot-fa5765189eef33bed301479410c4a53dc274acf0.tar.gz
Merge branch 'liw/new-ast2' into 'main'
reduce use of pandoc_ast See merge request subplot/subplot!305
-rw-r--r--src/ast.rs354
-rw-r--r--src/doc.rs321
-rw-r--r--src/embedded.rs10
-rw-r--r--src/lib.rs3
-rw-r--r--src/md.rs390
-rw-r--r--src/metadata.rs230
-rw-r--r--subplot.md1
7 files changed, 514 insertions, 795 deletions
diff --git a/src/ast.rs b/src/ast.rs
index e638464..ed163f0 100644
--- a/src/ast.rs
+++ b/src/ast.rs
@@ -1,10 +1,7 @@
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 regex::Regex;
use serde::Deserialize;
-use serde_yaml::{Mapping, Value};
+use serde_yaml::Value;
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
@@ -17,202 +14,6 @@ lazy_static! {
static ref TRAILING_YAML_PATTERN: Regex = Regex::new(r"(?P<text>(.*\n)*)\n*(?P<yaml>-{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<Block>,
- 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<Block> {
- 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<Inline> = 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<Inline>) -> Block {
- Block::Para(std::mem::take(inlines))
-}
-
-fn heading(level: i64, inlines: &mut Vec<Inline>) -> 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<Inline>) -> 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<Inline>) {
- 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 {
@@ -253,8 +54,8 @@ pub struct YamlMetadata {
}
impl YamlMetadata {
+ #[cfg(test)]
fn new(yaml_text: &str) -> Result<Self, Error> {
- trace!("Parsing YAML");
let meta: Self = serde_yaml::from_str(yaml_text)?;
Ok(meta)
}
@@ -264,134 +65,67 @@ impl YamlMetadata {
&self.markdowns[0]
}
- /// Convert into a pandoc_ast::Map.
- pub fn to_map(&self) -> Map<String, MetaValue> {
- trace!("Creating metadata map from parsed YAML");
- let mut map: Map<String, MetaValue> = 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<MetaValue> = 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));
- }
- }
+ /// Title.
+ pub fn title(&self) -> &str {
+ &self.title
+ }
- trace!("Created metadata map from parsed YAML");
- map
+ /// Subtitle.
+ pub fn subtitle(&self) -> Option<&str> {
+ self.subtitle.as_deref()
}
-}
-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)));
+ /// Date.
+ pub fn date(&self) -> Option<&str> {
+ self.date.as_deref()
}
- MetaValue::MetaMap(map)
-}
+ /// Authors.
+ pub fn authors(&self) -> Option<&[String]> {
+ self.authors.as_deref()
+ }
-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"),
+ /// Names of bindings files.
+ pub fn bindings_filenames(&self) -> Option<&[PathBuf]> {
+ self.bindings.as_deref()
+ }
- Value::Bool(b) => MetaValue::MetaBool(*b),
- Value::String(s) => MetaValue::MetaString(s.clone()),
- Value::Mapping(mapping) => mapping_to_pandoc(mapping),
+ /// Impls section.
+ pub fn impls(&self) -> &BTreeMap<String, Vec<PathBuf>> {
+ &self.impls
}
-}
-fn meta_string(s: &str) -> MetaValue {
- MetaValue::MetaString(s.to_string())
-}
+ /// Bibliographies.
+ pub fn bibliographies(&self) -> Option<&[PathBuf]> {
+ self.bibliography.as_deref()
+ }
-fn meta_strings(v: &[String]) -> MetaValue {
- MetaValue::MetaList(v.iter().map(|s| meta_string(s)).collect())
-}
+ /// Classes..
+ pub fn classes(&self) -> Option<&[String]> {
+ self.classes.as_deref()
+ }
-fn meta_path_buf(p: &Path) -> MetaValue {
- meta_string(&p.display().to_string())
-}
+ /// Documentclass.
+ pub fn documentclass(&self) -> Option<&str> {
+ self.documentclass.as_deref()
+ }
-fn meta_path_bufs(v: &[PathBuf]) -> MetaValue {
- MetaValue::MetaList(v.iter().map(|p| meta_path_buf(p)).collect())
+ /// Pandoc metadata.
+ pub fn pandoc(&self) -> Option<&HashMap<String, Value>> {
+ if let Some(x) = &self.pandoc {
+ Some(x)
+ } else {
+ None
+ }
+ }
}
#[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/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<PathBuf>,
- ast: Pandoc,
+ md: Markdown,
meta: Metadata,
files: EmbeddedFiles,
style: Style,
@@ -95,20 +79,22 @@ impl Document {
fn new(
subplot: PathBuf,
markdowns: Vec<PathBuf>,
- 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<PathBuf>,
yamlmeta: &ast::YamlMetadata,
- mut ast: Pandoc,
+ mut md: Markdown,
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() {
+ 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<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 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)
+ pub fn ast(&mut self) -> Result<String, SubplotError> {
+ 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<String> = 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<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;
- }
- 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<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());
- }
- }
- }
- }
- 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<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());
- }
- 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),
- }
- }
-}
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<EmbeddedFile>,
}
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
diff --git a/src/lib.rs b/src/lib.rs
index 4a3ae81..747b375 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};
@@ -73,4 +74,4 @@ mod codegen;
pub use codegen::generate_test_program;
mod ast;
-pub use ast::{AbstractSyntaxTree, YamlMetadata};
+pub use ast::YamlMetadata;
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<String, SubplotError> {
+ let json = serde_json::to_string(&self.pandoc).map_err(SubplotError::AstJson)?;
+ Ok(json)
+ }
+
+ /// Find problems.
+ pub fn lint(&mut self) -> Vec<SubplotError> {
+ let mut linter = LintingVisitor::default();
+ linter.walk_pandoc(self.pandoc());
+ linter.issues
+ }
+
+ /// Find included images.
+ pub fn images(&mut self) -> Vec<PathBuf> {
+ 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<String> {
+ 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<Warning> {
+ 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<Vec<Scenario>, 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<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;
+ }
+ 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<String, MetaValue> {
+ trace!("Creating metadata map from parsed YAML: {:#?}", yaml);
+
+ let mut map: Map<String, MetaValue> = 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<MetaValue> = 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<Self, Self::Error> {
+ 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<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());
+ }
+ }
+ }
+ }
+ 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<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());
+ }
+ 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),
+ }
+ }
+}
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<P>(
+ /// Create from YamlMetadata.
+ pub fn from_yaml_metadata<P>(
basedir: P,
- meta: &YamlMetadata,
+ yaml: &YamlMetadata,
template: Option<&str>,
- ) -> Result<Metadata, SubplotError>
+ ) -> Result<Self, SubplotError>
where
P: AsRef<Path> + 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<String, MetaValue>;
-
-fn get_title(map: &Mapp) -> String {
- if let Some(s) = get_string(map, "title") {
- s
- } else {
- "".to_string()
- }
-}
-
-fn get_date(map: &Mapp) -> Option<String> {
- get_string(map, "date")
-}
-
-fn get_bindings_filenames(map: &Mapp) -> Vec<PathBuf> {
- get_paths("", map, "bindings")
-}
-
fn load_template_spec(template: &str) -> Result<TemplateSpec, SubplotError> {
let mut spec_path = PathBuf::from(template);
spec_path.push("template");
@@ -177,143 +154,12 @@ fn load_template_spec(template: &str) -> Result<TemplateSpec, SubplotError> {
TemplateSpec::from_file(&spec_path)
}
-fn get_paths<P>(basedir: P, map: &Mapp, field: &str) -> Vec<PathBuf>
-where
- P: AsRef<Path>,
-{
- match map.get(field) {
- None => vec![],
- Some(v) => pathbufs(basedir, v),
- }
-}
-
-fn get_string(map: &Mapp, field: &str) -> Option<String> {
- 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<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
-where
- P: AsRef<Path>,
-{
- let v = match map.get("bibliography") {
- None => return vec![],
- Some(s) => s,
- };
- pathbufs(basedir, v)
-}
-
-fn pathbufs<P>(basedir: P, v: &MetaValue) -> Vec<PathBuf>
-where
- P: AsRef<Path>,
-{
- let mut bufs = vec![];
- push_pathbufs(basedir, v, &mut bufs);
- bufs
-}
-
-fn get_classes(map: &Mapp) -> Vec<String> {
- 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<String>) {
- 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<P>(basedir: P, v: &MetaValue, bufs: &mut Vec<PathBuf>)
+fn pathbufs<P>(basedir: P, v: &[PathBuf]) -> Vec<PathBuf>
where
P: AsRef<Path>,
{
- 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<P>(
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