summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2020-08-08 18:43:57 +0300
committerLars Wirzenius <liw@liw.fi>2020-08-08 20:47:22 +0300
commitd1651321ec1cd1e02fb93fe6e31ab4de115356c9 (patch)
treef5966966f0cb910659a62afa63c7f089913e9937
parentc3e8c88f3294338e8ea4678fb5493c96150a4e3c (diff)
downloadsubplot-d1651321ec1cd1e02fb93fe6e31ab4de115356c9.tar.gz
refactor: split stuff from src/ast.rs into smaller modules
This only moves things around, to avoid huge source code modules. It doesn't rename functions, add unit tests, or similar. * src/datafiles.rs: DataFile, DataFiles * src/metata.rs: Metadata * src/panhelper.rs: functions for querying Pandoc Attrs * src/policy.rs: the get_basedir_from function; place for later policy functions * src/typeset.rs: functions to produce Pandoc AST nodes * srv/visitor/*: various MutVisitor implementations for traversing ASTs, and their helper functions
-rw-r--r--src/ast.rs749
-rw-r--r--src/datafiles.rs51
-rw-r--r--src/lib.rs18
-rw-r--r--src/metadata.rs277
-rw-r--r--src/panhelper.rs26
-rw-r--r--src/policy.rs15
-rw-r--r--src/typeset.rs181
-rw-r--r--src/visitor/block_class.rs25
-rw-r--r--src/visitor/datafiles.rs35
-rw-r--r--src/visitor/image.rs25
-rw-r--r--src/visitor/linting.rs40
-rw-r--r--src/visitor/mod.rs17
-rw-r--r--src/visitor/structure.rs82
-rw-r--r--src/visitor/typesetting.rs49
14 files changed, 855 insertions, 735 deletions
diff --git a/src/ast.rs b/src/ast.rs
index fedacc2..41a1461 100644
--- a/src/ast.rs
+++ b/src/ast.rs
@@ -1,19 +1,19 @@
use crate::parser::parse_scenario_snippet;
-use crate::Bindings;
+use crate::visitor;
+use crate::DataFile;
+use crate::DataFiles;
+use crate::LintingVisitor;
use crate::MatchedScenario;
-use crate::PartialStep;
+use crate::Metadata;
use crate::Scenario;
use crate::ScenarioStep;
-use crate::StepKind;
-use crate::{DotMarkup, GraphMarkup, PlantumlMarkup};
use crate::{Result, SubplotError};
use std::collections::HashSet;
use std::ops::Deref;
use std::path::{Path, PathBuf};
-use pandoc_ast::{Attr, Block, Inline, Map, MetaValue, MutVisitor, Pandoc};
-use serde::{Deserialize, Serialize};
+use pandoc_ast::{MutVisitor, Pandoc};
/// The set of known (special) classes which subplot will always recognise
/// as being valid.
@@ -165,7 +165,7 @@ impl<'a> Document {
names.push(x.to_path_buf());
}
- let mut visitor = ImageVisitor::new();
+ let mut visitor = visitor::ImageVisitor::new();
visitor.walk_pandoc(&mut self.ast);
for x in visitor.images().iter() {
names.push(x.to_path_buf());
@@ -210,7 +210,7 @@ impl<'a> Document {
/// Check that all the block classes in the document are known
fn check_block_classes(&self) -> Result<()> {
- let mut visitor = BlockClassVisitor::default();
+ 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
@@ -242,13 +242,13 @@ impl<'a> Document {
/// Typeset a Subplot document.
pub fn typeset(&mut self) {
- let mut visitor = TypesettingVisitor::new(&self.meta.bindings);
+ let mut visitor = visitor::TypesettingVisitor::new(&self.meta.bindings());
visitor.walk_pandoc(&mut self.ast);
}
/// Return all scenarios in a document.
pub fn scenarios(&mut self) -> Result<Vec<Scenario>> {
- let mut visitor = StructureVisitor::new();
+ let mut visitor = visitor::StructureVisitor::new();
visitor.walk_pandoc(&mut self.ast);
let mut scenarios: Vec<Scenario> = vec![];
@@ -276,20 +276,20 @@ impl<'a> Document {
}
}
-fn extract_scenario(e: &[Element]) -> Result<(Option<Scenario>, usize)> {
+fn extract_scenario(e: &[visitor::Element]) -> Result<(Option<Scenario>, usize)> {
if e.is_empty() {
// If we get here, it's a programming error.
panic!("didn't expect empty list of elements");
}
match &e[0] {
- Element::Snippet(_) => Err(SubplotError::ScenarioBeforeHeading),
- Element::Heading(title, level) => {
+ 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 {
- Element::Heading(_, level2) => {
+ visitor::Element::Heading(_, level2) => {
let is_subsection = *level2 > *level;
if is_subsection {
if scen.has_steps() {
@@ -302,7 +302,7 @@ fn extract_scenario(e: &[Element]) -> Result<(Option<Scenario>, usize)> {
return Ok((None, i));
}
}
- Element::Snippet(text) => {
+ visitor::Element::Snippet(text) => {
for line in parse_scenario_snippet(&text) {
let step = ScenarioStep::new_from_str(line, prevkind)?;
scen.add(&step);
@@ -323,7 +323,7 @@ fn extract_scenario(e: &[Element]) -> Result<(Option<Scenario>, usize)> {
#[cfg(test)]
mod test_extract {
use super::extract_scenario;
- use super::Element;
+ use super::visitor::Element;
use crate::Result;
use crate::Scenario;
use crate::SubplotError;
@@ -426,720 +426,3 @@ mod test_extract {
}
}
}
-
-/// Metadata of a document, as needed by Subplot.
-#[derive(Debug)]
-pub struct Metadata {
- title: String,
- date: Option<String>,
- bindings_filenames: Vec<PathBuf>,
- bindings: Bindings,
- functions_filenames: Vec<PathBuf>,
- template: Option<String>,
- bibliographies: Vec<PathBuf>,
- /// Extra class names which should be considered 'correct' for this document
- classes: Vec<String>,
-}
-
-impl Metadata {
- /// Construct a Metadata from a Document, if possible.
- pub fn new<P>(basedir: P, doc: &Pandoc) -> Result<Metadata>
- where
- P: AsRef<Path>,
- {
- let title = get_title(&doc.meta)?;
- let date = get_date(&doc.meta);
- let bindings_filenames = get_bindings_filenames(basedir.as_ref(), &doc.meta);
- let functions_filenames = get_functions_filenames(basedir.as_ref(), &doc.meta);
- let template = get_template_name(&doc.meta)?;
- let mut bindings = Bindings::new();
-
- let bibliographies = get_bibliographies(basedir.as_ref(), &doc.meta);
- let classes = get_classes(&doc.meta);
-
- get_bindings(&bindings_filenames, &mut bindings)?;
- Ok(Metadata {
- title,
- date,
- bindings_filenames,
- bindings,
- functions_filenames,
- template,
- bibliographies,
- classes,
- })
- }
-
- /// Return title of document.
- pub fn title(&self) -> &str {
- &self.title
- }
-
- /// Return date of document, if any.
- pub fn date(&self) -> Option<&str> {
- if let Some(date) = &self.date {
- Some(&date)
- } else {
- None
- }
- }
-
- /// Return filename where bindings are specified.
- pub fn bindings_filenames(&self) -> Vec<&Path> {
- self.bindings_filenames.iter().map(|f| f.as_ref()).collect()
- }
-
- /// Return filename where functions are specified.
- pub fn functions_filenames(&self) -> Vec<&Path> {
- self.functions_filenames
- .iter()
- .map(|f| f.as_ref())
- .collect()
- }
-
- /// Return the name of the code template, if specified.
- pub fn template_name(&self) -> Option<&str> {
- match &self.template {
- Some(x) => Some(&x),
- None => None,
- }
- }
-
- /// Return the bindings.
- pub fn bindings(&self) -> &Bindings {
- &self.bindings
- }
-
- /// Return the bibliographies.
- pub fn bibliographies(&self) -> Vec<&Path> {
- self.bibliographies.iter().map(|x| x.as_path()).collect()
- }
-
- /// The classes which this document also claims are valid
- pub fn classes(&self) -> impl Iterator<Item = &str> {
- self.classes.iter().map(Deref::deref)
- }
-}
-
-type Mapp = Map<String, MetaValue>;
-
-fn get_title(map: &Mapp) -> Result<String> {
- if let Some(s) = get_string(map, "title") {
- Ok(s)
- } else {
- Ok("".to_string())
- }
-}
-
-fn get_date(map: &Mapp) -> Option<String> {
- if let Some(s) = get_string(map, "date") {
- Some(s)
- } else {
- None
- }
-}
-
-fn get_bindings_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
-where
- P: AsRef<Path>,
-{
- get_paths(basedir, map, "bindings")
-}
-
-fn get_functions_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
-where
- P: AsRef<Path>,
-{
- get_paths(basedir, map, "functions")
-}
-
-fn get_template_name(map: &Mapp) -> Result<Option<String>> {
- match get_string(map, "template") {
- Some(s) => Ok(Some(s)),
- None => Ok(None),
- }
-}
-
-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>)
-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::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_str(" "),
- pandoc_ast::Inline::SoftBreak => buf.push_str(" "),
- pandoc_ast::Inline::LineBreak => buf.push_str(" "),
- _ => panic!("unknown pandoc_ast::Inline component {:?}", item),
- }
- }
-}
-
-#[cfg(test)]
-mod test_join {
- use super::join;
- use pandoc_ast::Inline;
-
- #[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::LineBreak,
- ];
- assert_eq!(join(&v), "abcdefg ");
- }
-}
-
-fn get_bindings<P>(filenames: &[P], bindings: &mut Bindings) -> Result<()>
-where
- P: AsRef<Path>,
-{
- for filename in filenames {
- bindings.add_from_file(filename)?;
- }
- Ok(())
-}
-
-/// Visitor for the pandoc AST.
-///
-/// This includes rendering stuff which we find as we go
-struct TypesettingVisitor<'a> {
- bindings: &'a Bindings,
-}
-
-impl<'a> TypesettingVisitor<'a> {
- fn new(bindings: &'a Bindings) -> Self {
- TypesettingVisitor { bindings }
- }
-}
-
-// Visit interesting parts of the Pandoc abstract syntax tree. The
-// document top level is a vector of blocks and we visit that and
-// replace any fenced code block with the scenario tag with a typeset
-// paragraph. Also, replace fenced code blocks with known graph markup
-// with the rendered SVG image.
-impl<'a> MutVisitor for TypesettingVisitor<'a> {
- fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
- for block in vec_block {
- match block {
- Block::CodeBlock(attr, s) => {
- if is_class(attr, "scenario") {
- *block = scenario_snippet(&self.bindings, s)
- } else if is_class(attr, "file") {
- *block = file_block(attr, s)
- } else if is_class(attr, "dot") {
- *block = dot_to_block(s)
- } else if is_class(attr, "plantuml") {
- *block = plantuml_to_block(s)
- } else if is_class(attr, "roadmap") {
- *block = roadmap_to_block(s)
- }
- }
- _ => {
- self.visit_block(block);
- }
- }
- }
- }
-}
-
-// Is a code block marked as being of a given type?
-fn is_class(attr: &Attr, class: &str) -> bool {
- let (_id, classes, _kvpairs) = attr;
- classes.iter().any(|s| s == class)
-}
-
-/// Typeset an error as a Pandoc AST Block element.
-pub fn error(err: SubplotError) -> Block {
- let msg = format!("ERROR: {}", err.to_string());
- Block::Para(error_msg(&msg))
-}
-
-// Typeset an error message a vector of inlines.
-pub fn error_msg(msg: &str) -> Vec<Inline> {
- vec![Inline::Strong(vec![inlinestr(msg)])]
-}
-
-/// Typeset a code block tagged as a file.
-pub fn file_block(attr: &Attr, text: &str) -> Block {
- let filename = inlinestr(&attr.0);
- let filename = Inline::Strong(vec![filename]);
- let intro = Block::Para(vec![inlinestr("File:"), space(), filename]);
- let codeblock = Block::CodeBlock(attr.clone(), text.to_string());
- let noattr = ("".to_string(), vec![], vec![]);
- Block::Div(noattr, vec![intro, codeblock])
-}
-
-/// Typeset a scenario snippet as a Pandoc AST Block.
-///
-/// Typesetting here means producing the Pandoc abstract syntax tree
-/// nodes that result in the desired output, when Pandoc processes
-/// them.
-///
-/// The snippet is given as a text string, which is parsed. It need
-/// not be a complete scenario, but it should consist of complete steps.
-pub fn scenario_snippet(bindings: &Bindings, snippet: &str) -> Block {
- let lines = parse_scenario_snippet(snippet);
- let mut steps = vec![];
- let mut prevkind: Option<StepKind> = None;
-
- for line in lines {
- let (this, thiskind) = step(bindings, line, prevkind);
- steps.push(this);
- prevkind = thiskind;
- }
- Block::LineBlock(steps)
-}
-
-// Typeset a single scenario step as a sequence of Pandoc AST Inlines.
-fn step(
- bindings: &Bindings,
- text: &str,
- defkind: Option<StepKind>,
-) -> (Vec<Inline>, Option<StepKind>) {
- let step = ScenarioStep::new_from_str(text, defkind);
- if step.is_err() {
- return (
- error_msg(&format!("Could not parse step: {}", text)),
- defkind,
- );
- }
- let step = step.unwrap();
-
- let m = match bindings.find(&step) {
- Ok(m) => m,
- Err(e) => {
- eprintln!("Could not select binding: {:?}", e);
- return (
- error_msg(&format!("Could not select binding for: {}", text)),
- defkind,
- );
- }
- };
-
- let mut inlines = Vec::new();
-
- inlines.push(keyword(&step));
- inlines.push(space());
-
- for part in m.parts() {
- #[allow(unused_variables)]
- match part {
- PartialStep::UncapturedText(s) => inlines.push(uncaptured(s.text())),
- PartialStep::CapturedText { name, text } => inlines.push(captured(text)),
- }
- }
-
- (inlines, Some(step.kind()))
-}
-
-// Typeset first word, which is assumed to be a keyword, of a scenario
-// step.
-fn keyword(step: &ScenarioStep) -> Inline {
- let word = inlinestr(step.keyword());
- Inline::Emph(vec![word])
-}
-
-fn inlinestr(s: &str) -> Inline {
- Inline::Str(String::from(s))
-}
-
-// Typeset a space between words.
-fn space() -> Inline {
- Inline::Space
-}
-
-// Typeset an uncaptured part of a step.
-fn uncaptured(s: &str) -> Inline {
- inlinestr(s)
-}
-
-// Typeset a captured part of a step.
-fn captured(s: &str) -> Inline {
- Inline::Strong(vec![inlinestr(s)])
-}
-
-// Take a dot graph, render it as SVG, and return an AST Block
-// element. The Block will contain the SVG data. This allows the graph
-// to be rendered without referending external entities.
-fn dot_to_block(dot: &str) -> Block {
- match DotMarkup::new(dot).as_svg() {
- Ok(svg) => typeset_svg(svg),
- Err(err) => {
- eprintln!("dot failed: {}", err);
- error(err)
- }
- }
-}
-
-// Take a PlantUML graph, render it as SVG, and return an AST Block
-// element. The Block will contain the SVG data. This allows the graph
-// to be rendered without referending external entities.
-fn plantuml_to_block(markup: &str) -> Block {
- match PlantumlMarkup::new(markup).as_svg() {
- Ok(svg) => typeset_svg(svg),
- Err(err) => {
- eprintln!("plantuml failed: {}", err);
- error(err)
- }
- }
-}
-
-/// Typeset a project roadmap expressed as textual YAML, and render it
-/// as an SVG image.
-fn roadmap_to_block(yaml: &str) -> Block {
- match roadmap::from_yaml(yaml) {
- Ok(ref mut roadmap) => {
- roadmap.set_missing_statuses();
- let width = 50;
- match roadmap.format_as_dot(width) {
- Ok(dot) => dot_to_block(&dot),
- Err(e) => Block::Para(vec![inlinestr(&e.to_string())]),
- }
- }
- Err(e) => Block::Para(vec![inlinestr(&e.to_string())]),
- }
-}
-
-// Typeset an SVG, represented as a byte vector, as a Pandoc AST Block
-// element.
-fn typeset_svg(svg: Vec<u8>) -> Block {
- let url = svg_as_data_url(svg);
- let attr = ("".to_string(), vec![], vec![]);
- let img = Inline::Image(attr, vec![], (url, "".to_string()));
- Block::Para(vec![img])
-}
-
-// Convert an SVG, represented as a byte vector, into a data: URL,
-// which can be inlined so the image can be rendered without
-// referencing external files.
-fn svg_as_data_url(svg: Vec<u8>) -> String {
- let svg = base64::encode(&svg);
- format!("data:image/svg+xml;base64,{}", svg)
-}
-
-// A structure element in the document: a heading or a scenario snippet.
-#[derive(Debug)]
-enum Element {
- // Headings consist of the text and the level of the heading.
- Heading(String, i64),
-
- // Scenario snippets consist just of the unparsed text.
- Snippet(String),
-}
-
-impl Element {
- pub fn heading(text: &str, level: i64) -> Element {
- Element::Heading(text.to_string(), level)
- }
-
- pub fn snippet(text: &str) -> Element {
- Element::Snippet(text.to_string())
- }
-}
-
-// A MutVisitor for extracting document structure.
-struct StructureVisitor {
- elements: Vec<Element>,
-}
-
-impl StructureVisitor {
- pub fn new() -> Self {
- Self { elements: vec![] }
- }
-}
-
-impl MutVisitor for StructureVisitor {
- fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
- for block in vec_block {
- match block {
- Block::Header(level, _attr, inlines) => {
- let text = join(inlines);
- let heading = Element::heading(&text, *level);
- self.elements.push(heading);
- }
- Block::CodeBlock(attr, s) => {
- if is_class(attr, "scenario") {
- let snippet = Element::snippet(s);
- self.elements.push(snippet);
- }
- }
- _ => {
- self.visit_block(block);
- }
- }
- }
- }
-}
-
-/// A data file embedded in the document.
-#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
-pub struct DataFile {
- filename: String,
- contents: String,
-}
-
-impl DataFile {
- fn new(filename: String, contents: String) -> DataFile {
- DataFile { filename, contents }
- }
-
- /// Return name of embedded file.
- pub fn filename(&self) -> &str {
- &self.filename
- }
-
- /// Return contents of embedded file.
- pub fn contents(&self) -> &str {
- &self.contents
- }
-}
-
-/// A collection of data files embedded in document.
-#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
-struct DataFiles {
- files: Vec<DataFile>,
-}
-
-impl DataFiles {
- fn new(ast: &mut Pandoc) -> DataFiles {
- let mut files = DataFiles { files: vec![] };
- files.walk_pandoc(ast);
- files
- }
-
- fn files(&self) -> &[DataFile] {
- &self.files
- }
-}
-
-impl MutVisitor for DataFiles {
- fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
- for block in vec_block {
- match block {
- Block::CodeBlock(attr, contents) => {
- if is_class(attr, "file") {
- let add_newline = match find_attr_kv(&attr, "add-newline").next() {
- None | Some("auto") => !contents.ends_with('\n'),
- Some("yes") => true,
- Some("no") => false,
- _ => unreachable!(),
- };
- let contents = if add_newline {
- format!("{}\n", contents)
- } else {
- contents.clone()
- };
- self.files.push(DataFile::new(get_filename(attr), contents));
- }
- }
- _ => {
- self.visit_block(block);
- }
- }
- }
- }
-}
-
-fn get_filename(attr: &Attr) -> String {
- attr.0.to_string()
-}
-
-struct ImageVisitor {
- images: Vec<PathBuf>,
-}
-
-impl ImageVisitor {
- fn new() -> Self {
- ImageVisitor { images: vec![] }
- }
-
- fn images(&self) -> Vec<PathBuf> {
- self.images.clone()
- }
-}
-
-impl MutVisitor for ImageVisitor {
- fn visit_inline(&mut self, inline: &mut Inline) {
- if let Inline::Image(_attr, _inlines, target) = inline {
- self.images.push(PathBuf::from(&target.0));
- }
- }
-}
-
-#[derive(Default)]
-struct BlockClassVisitor {
- classes: HashSet<String>,
-}
-
-impl MutVisitor for BlockClassVisitor {
- fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
- for block in vec_block {
- match block {
- Block::CodeBlock(attr, _) => {
- for class in &attr.1 {
- self.classes.insert(class.to_string());
- }
- }
- _ => {
- self.visit_block(block);
- }
- }
- }
- }
-}
-
-#[derive(Default)]
-struct LintingVisitor {
- issues: Vec<SubplotError>,
-}
-
-impl MutVisitor for LintingVisitor {
- fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
- for block in vec_block {
- match block {
- Block::CodeBlock(attr, _) => {
- if is_class(attr, "file") {
- let newlines: Vec<_> = find_attr_kv(&attr, "add-newline").collect();
- match newlines.len() {
- 0 => {}
- 1 => match newlines[0].to_ascii_lowercase().as_ref() {
- "auto" | "yes" | "no" => {}
- _ => self.issues.push(SubplotError::UnrecognisedAddNewline(
- get_filename(&attr),
- newlines[0].to_owned(),
- )),
- },
- _ => self.issues.push(SubplotError::RepeatedAddNewlineAttribute(
- get_filename(&attr),
- )),
- }
- }
- }
- _ => {
- self.visit_block(block);
- }
- }
- }
- }
-}
-
-/// Get the base directory given the name of the markdown file.
-///
-/// All relative filename, such as bindings files, are resolved
-/// against the base directory.
-pub fn get_basedir_from(filename: &Path) -> Result<PathBuf> {
- let dirname = match filename.parent() {
- None => return Err(SubplotError::BasedirError(filename.to_path_buf())),
- Some(x) => x.to_path_buf(),
- };
- Ok(dirname)
-}
-
-/// Utility function to find key/value pairs from an attribute
-fn find_attr_kv<'a>(attr: &'a Attr, key: &'static str) -> impl Iterator<Item = &'a str> {
- attr.2.iter().flat_map(move |(key_, value)| {
- if key == key_ {
- Some(value.as_ref())
- } else {
- None
- }
- })
-}
diff --git a/src/datafiles.rs b/src/datafiles.rs
new file mode 100644
index 0000000..83e90d9
--- /dev/null
+++ b/src/datafiles.rs
@@ -0,0 +1,51 @@
+use pandoc_ast::{MutVisitor, Pandoc};
+use serde::{Deserialize, Serialize};
+
+/// A data file embedded in the document.
+#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
+pub struct DataFile {
+ filename: String,
+ contents: String,
+}
+
+impl DataFile {
+ /// Create a new data file, with a name and contents.
+ pub fn new(filename: String, contents: String) -> DataFile {
+ DataFile { filename, contents }
+ }
+
+ /// Return name of embedded file.
+ pub fn filename(&self) -> &str {
+ &self.filename
+ }
+
+ /// Return contents of embedded file.
+ pub fn contents(&self) -> &str {
+ &self.contents
+ }
+}
+
+/// A collection of data files embedded in document.
+#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
+pub struct DataFiles {
+ files: Vec<DataFile>,
+}
+
+impl DataFiles {
+ /// Create new set of data files.
+ pub fn new(ast: &mut Pandoc) -> DataFiles {
+ let mut files = DataFiles { files: vec![] };
+ files.walk_pandoc(ast);
+ files
+ }
+
+ /// Return slice of all data files.
+ pub fn files(&self) -> &[DataFile] {
+ &self.files
+ }
+
+ /// Append a new data file.
+ pub fn push(&mut self, file: DataFile) {
+ self.files.push(file);
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index c2a2ec7..842a264 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -12,9 +12,23 @@ pub use error::SubplotError;
mod graphmarkup;
pub use graphmarkup::{DotMarkup, GraphMarkup, PlantumlMarkup};
+mod datafiles;
+pub use datafiles::DataFile;
+pub use datafiles::DataFiles;
+
+mod panhelper;
+mod typeset;
+
+mod visitor;
+use visitor::LintingVisitor;
+
+mod policy;
+pub use policy::get_basedir_from;
+
+mod metadata;
+pub use metadata::Metadata;
+
mod ast;
-pub use ast::get_basedir_from;
-pub use ast::DataFile;
pub use ast::Document;
mod scenarios;
diff --git a/src/metadata.rs b/src/metadata.rs
new file mode 100644
index 0000000..05f686b
--- /dev/null
+++ b/src/metadata.rs
@@ -0,0 +1,277 @@
+use crate::Bindings;
+use crate::Result;
+
+use std::ops::Deref;
+use std::path::{Path, PathBuf};
+
+use pandoc_ast::{Inline, Map, MetaValue, Pandoc};
+
+/// Metadata of a document, as needed by Subplot.
+#[derive(Debug)]
+pub struct Metadata {
+ title: String,
+ date: Option<String>,
+ bindings_filenames: Vec<PathBuf>,
+ bindings: Bindings,
+ functions_filenames: Vec<PathBuf>,
+ template: Option<String>,
+ bibliographies: Vec<PathBuf>,
+ /// Extra class names which should be considered 'correct' for this document
+ classes: Vec<String>,
+}
+
+impl Metadata {
+ /// Construct a Metadata from a Document, if possible.
+ pub fn new<P>(basedir: P, doc: &Pandoc) -> Result<Metadata>
+ where
+ P: AsRef<Path>,
+ {
+ let title = get_title(&doc.meta)?;
+ let date = get_date(&doc.meta);
+ let bindings_filenames = get_bindings_filenames(basedir.as_ref(), &doc.meta);
+ let functions_filenames = get_functions_filenames(basedir.as_ref(), &doc.meta);
+ let template = get_template_name(&doc.meta)?;
+ let mut bindings = Bindings::new();
+
+ let bibliographies = get_bibliographies(basedir.as_ref(), &doc.meta);
+ let classes = get_classes(&doc.meta);
+
+ get_bindings(&bindings_filenames, &mut bindings)?;
+ Ok(Metadata {
+ title,
+ date,
+ bindings_filenames,
+ bindings,
+ functions_filenames,
+ template,
+ bibliographies,
+ classes,
+ })
+ }
+
+ /// Return title of document.
+ pub fn title(&self) -> &str {
+ &self.title
+ }
+
+ /// Return date of document, if any.
+ pub fn date(&self) -> Option<&str> {
+ if let Some(date) = &self.date {
+ Some(&date)
+ } else {
+ None
+ }
+ }
+
+ /// Return filename where bindings are specified.
+ pub fn bindings_filenames(&self) -> Vec<&Path> {
+ self.bindings_filenames.iter().map(|f| f.as_ref()).collect()
+ }
+
+ /// Return filename where functions are specified.
+ pub fn functions_filenames(&self) -> Vec<&Path> {
+ self.functions_filenames
+ .iter()
+ .map(|f| f.as_ref())
+ .collect()
+ }
+
+ /// Return the name of the code template, if specified.
+ pub fn template_name(&self) -> Option<&str> {
+ match &self.template {
+ Some(x) => Some(&x),
+ None => None,
+ }
+ }
+
+ /// Return the bindings.
+ pub fn bindings(&self) -> &Bindings {
+ &self.bindings
+ }
+
+ /// Return the bibliographies.
+ pub fn bibliographies(&self) -> Vec<&Path> {
+ self.bibliographies.iter().map(|x| x.as_path()).collect()
+ }
+
+ /// The classes which this document also claims are valid
+ pub fn classes(&self) -> impl Iterator<Item = &str> {
+ self.classes.iter().map(Deref::deref)
+ }
+}
+
+type Mapp = Map<String, MetaValue>;
+
+fn get_title(map: &Mapp) -> Result<String> {
+ if let Some(s) = get_string(map, "title") {
+ Ok(s)
+ } else {
+ Ok("".to_string())
+ }
+}
+
+fn get_date(map: &Mapp) -> Option<String> {
+ if let Some(s) = get_string(map, "date") {
+ Some(s)
+ } else {
+ None
+ }
+}
+
+fn get_bindings_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
+where
+ P: AsRef<Path>,
+{
+ get_paths(basedir, map, "bindings")
+}
+
+fn get_functions_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
+where
+ P: AsRef<Path>,
+{
+ get_paths(basedir, map, "functions")
+}
+
+fn get_template_name(map: &Mapp) -> Result<Option<String>> {
+ match get_string(map, "template") {
+ Some(s) => Ok(Some(s)),
+ None => Ok(None),
+ }
+}
+
+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>)
+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::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_str(" "),
+ pandoc_ast::Inline::SoftBreak => buf.push_str(" "),
+ pandoc_ast::Inline::LineBreak => buf.push_str(" "),
+ _ => panic!("unknown pandoc_ast::Inline component {:?}", item),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test_join {
+ use super::join;
+ use pandoc_ast::Inline;
+
+ #[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::LineBreak,
+ ];
+ assert_eq!(join(&v), "abcdefg ");
+ }
+}
+
+fn get_bindings<P>(filenames: &[P], bindings: &mut Bindings) -> Result<()>
+where
+ P: AsRef<Path>,
+{
+ for filename in filenames {
+ bindings.add_from_file(filename)?;
+ }
+ Ok(())
+}
diff --git a/src/panhelper.rs b/src/panhelper.rs
new file mode 100644
index 0000000..f7ab801
--- /dev/null
+++ b/src/panhelper.rs
@@ -0,0 +1,26 @@
+use pandoc_ast::Attr;
+
+/// Is a code block marked as being of a given type?
+pub fn is_class(attr: &Attr, class: &str) -> bool {
+ let (_id, classes, _kvpairs) = attr;
+ classes.iter().any(|s| s == class)
+}
+
+/// Utility function to find key/value pairs from an attribute
+pub fn find_attr_kv<'a>(attr: &'a Attr, key: &'static str) -> impl Iterator<Item = &'a str> {
+ attr.2.iter().flat_map(move |(key_, value)| {
+ if key == key_ {
+ Some(value.as_ref())
+ } else {
+ None
+ }
+ })
+}
+
+/// Get the filename for a fenced code block tagged .file.
+///
+/// The filename is the first (and presumably only) identifier for the
+/// block.
+pub fn get_filename(attr: &Attr) -> String {
+ attr.0.to_string()
+}
diff --git a/src/policy.rs b/src/policy.rs
new file mode 100644
index 0000000..2d5bda6
--- /dev/null
+++ b/src/policy.rs
@@ -0,0 +1,15 @@
+use crate::{Result, SubplotError};
+
+use std::path::{Path, PathBuf};
+
+/// Get the base directory given the name of the markdown file.
+///
+/// All relative filename, such as bindings files, are resolved
+/// against the base directory.
+pub fn get_basedir_from(filename: &Path) -> Result<PathBuf> {
+ let dirname = match filename.parent() {
+ None => return Err(SubplotError::BasedirError(filename.to_path_buf())),
+ Some(x) => x.to_path_buf(),
+ };
+ Ok(dirname)
+}
diff --git a/src/typeset.rs b/src/typeset.rs
new file mode 100644
index 0000000..32956c6
--- /dev/null
+++ b/src/typeset.rs
@@ -0,0 +1,181 @@
+use crate::parser::parse_scenario_snippet;
+use crate::Bindings;
+use crate::PartialStep;
+use crate::ScenarioStep;
+use crate::StepKind;
+use crate::SubplotError;
+use crate::{DotMarkup, GraphMarkup, PlantumlMarkup};
+
+use pandoc_ast::Attr;
+use pandoc_ast::Block;
+use pandoc_ast::Inline;
+
+/// Typeset an error as a Pandoc AST Block element.
+pub fn error(err: SubplotError) -> Block {
+ let msg = format!("ERROR: {}", err.to_string());
+ Block::Para(error_msg(&msg))
+}
+
+/// Typeset an error message a vector of inlines.
+pub fn error_msg(msg: &str) -> Vec<Inline> {
+ vec![Inline::Strong(vec![inlinestr(msg)])]
+}
+
+/// Typeset a string as an inline element.
+pub fn inlinestr(s: &str) -> Inline {
+ Inline::Str(String::from(s))
+}
+
+/// Typeset a code block tagged as a file.
+pub fn file_block(attr: &Attr, text: &str) -> Block {
+ let filename = inlinestr(&attr.0);
+ let filename = Inline::Strong(vec![filename]);
+ let intro = Block::Para(vec![inlinestr("File:"), space(), filename]);
+ let codeblock = Block::CodeBlock(attr.clone(), text.to_string());
+ let noattr = ("".to_string(), vec![], vec![]);
+ Block::Div(noattr, vec![intro, codeblock])
+}
+
+/// Typeset a scenario snippet as a Pandoc AST Block.
+///
+/// Typesetting here means producing the Pandoc abstract syntax tree
+/// nodes that result in the desired output, when Pandoc processes
+/// them.
+///
+/// The snippet is given as a text string, which is parsed. It need
+/// not be a complete scenario, but it should consist of complete steps.
+pub fn scenario_snippet(bindings: &Bindings, snippet: &str) -> Block {
+ let lines = parse_scenario_snippet(snippet);
+ let mut steps = vec![];
+ let mut prevkind: Option<StepKind> = None;
+
+ for line in lines {
+ let (this, thiskind) = step(bindings, line, prevkind);
+ steps.push(this);
+ prevkind = thiskind;
+ }
+ Block::LineBlock(steps)
+}
+
+// Typeset a single scenario step as a sequence of Pandoc AST Inlines.
+fn step(
+ bindings: &Bindings,
+ text: &str,
+ defkind: Option<StepKind>,
+) -> (Vec<Inline>, Option<StepKind>) {
+ let step = ScenarioStep::new_from_str(text, defkind);
+ if step.is_err() {
+ return (
+ error_msg(&format!("Could not parse step: {}", text)),
+ defkind,
+ );
+ }
+ let step = step.unwrap();
+
+ let m = match bindings.find(&step) {
+ Ok(m) => m,
+ Err(e) => {
+ eprintln!("Could not select binding: {:?}", e);
+ return (
+ error_msg(&format!("Could not select binding for: {}", text)),
+ defkind,
+ );
+ }
+ };
+
+ let mut inlines = Vec::new();
+
+ inlines.push(keyword(&step));
+ inlines.push(space());
+
+ for part in m.parts() {
+ #[allow(unused_variables)]
+ match part {
+ PartialStep::UncapturedText(s) => inlines.push(uncaptured(s.text())),
+ PartialStep::CapturedText { name, text } => inlines.push(captured(text)),
+ }
+ }
+
+ (inlines, Some(step.kind()))
+}
+
+// Typeset first word, which is assumed to be a keyword, of a scenario
+// step.
+fn keyword(step: &ScenarioStep) -> Inline {
+ let word = inlinestr(step.keyword());
+ Inline::Emph(vec![word])
+}
+
+// Typeset a space between words.
+fn space() -> Inline {
+ Inline::Space
+}
+
+// Typeset an uncaptured part of a step.
+fn uncaptured(s: &str) -> Inline {
+ inlinestr(s)
+}
+
+// Typeset a captured part of a step.
+fn captured(s: &str) -> Inline {
+ Inline::Strong(vec![inlinestr(s)])
+}
+
+// Take a dot graph, render it as SVG, and return an AST Block
+// element. The Block will contain the SVG data. This allows the graph
+// to be rendered without referending external entities.
+pub fn dot_to_block(dot: &str) -> Block {
+ match DotMarkup::new(dot).as_svg() {
+ Ok(svg) => typeset_svg(svg),
+ Err(err) => {
+ eprintln!("dot failed: {}", err);
+ error(err)
+ }
+ }
+}
+
+// Take a PlantUML graph, render it as SVG, and return an AST Block
+// element. The Block will contain the SVG data. This allows the graph
+// to be rendered without referending external entities.
+pub fn plantuml_to_block(markup: &str) -> Block {
+ match PlantumlMarkup::new(markup).as_svg() {
+ Ok(svg) => typeset_svg(svg),
+ Err(err) => {
+ eprintln!("plantuml failed: {}", err);
+ error(err)
+ }
+ }
+}
+
+/// Typeset a project roadmap expressed as textual YAML, and render it
+/// as an SVG image.
+pub fn roadmap_to_block(yaml: &str) -> Block {
+ match roadmap::from_yaml(yaml) {
+ Ok(ref mut roadmap) => {
+ roadmap.set_missing_statuses();
+ let width = 50;
+ match roadmap.format_as_dot(width) {
+ Ok(dot) => dot_to_block(&dot),
+ Err(e) => Block::Para(vec![inlinestr(&e.to_string())]),
+ }
+ }
+ Err(e) => Block::Para(vec![inlinestr(&e.to_string())]),
+ }
+}
+
+// Typeset an SVG, represented as a byte vector, as a Pandoc AST Block
+// element.
+fn typeset_svg(svg: Vec<u8>) -> Block {
+ let url = svg_as_data_url(svg);
+ let attr = ("".to_string(), vec![], vec![]);
+ let img = Inline::Image(attr, vec![], (url, "".to_string()));
+ Block::Para(vec![img])
+}
+
+// Convert an SVG, represented as a byte vector, into a data: URL,
+// which can be inlined so the image can be rendered without
+// referencing external files.
+fn svg_as_data_url(svg: Vec<u8>) -> String {
+ let svg = base64::encode(&svg);
+ format!("data:image/svg+xml;base64,{}", svg)
+}
diff --git a/src/visitor/block_class.rs b/src/visitor/block_class.rs
new file mode 100644
index 0000000..303616b
--- /dev/null
+++ b/src/visitor/block_class.rs
@@ -0,0 +1,25 @@
+use std::collections::HashSet;
+
+use pandoc_ast::{Block, MutVisitor};
+
+#[derive(Default)]
+pub struct BlockClassVisitor {
+ pub classes: HashSet<String>,
+}
+
+impl MutVisitor for BlockClassVisitor {
+ fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
+ for block in vec_block {
+ match block {
+ Block::CodeBlock(attr, _) => {
+ for class in &attr.1 {
+ self.classes.insert(class.to_string());
+ }
+ }
+ _ => {
+ self.visit_block(block);
+ }
+ }
+ }
+ }
+}
diff --git a/src/visitor/datafiles.rs b/src/visitor/datafiles.rs
new file mode 100644
index 0000000..bc6961d
--- /dev/null
+++ b/src/visitor/datafiles.rs
@@ -0,0 +1,35 @@
+use crate::panhelper;
+use crate::DataFile;
+use crate::DataFiles;
+
+use pandoc_ast::{Block, MutVisitor};
+
+impl MutVisitor for DataFiles {
+ fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
+ use panhelper::is_class;
+ for block in vec_block {
+ match block {
+ Block::CodeBlock(attr, contents) => {
+ if is_class(attr, "file") {
+ let add_newline = match panhelper::find_attr_kv(&attr, "add-newline").next()
+ {
+ None | Some("auto") => !contents.ends_with('\n'),
+ Some("yes") => true,
+ Some("no") => false,
+ _ => unreachable!(),
+ };
+ let contents = if add_newline {
+ format!("{}\n", contents)
+ } else {
+ contents.clone()
+ };
+ self.push(DataFile::new(panhelper::get_filename(attr), contents));
+ }
+ }
+ _ => {
+ self.visit_block(block);
+ }
+ }
+ }
+ }
+}
diff --git a/src/visitor/image.rs b/src/visitor/image.rs
new file mode 100644
index 0000000..be49d66
--- /dev/null
+++ b/src/visitor/image.rs
@@ -0,0 +1,25 @@
+use std::path::PathBuf;
+
+use pandoc_ast::{Inline, MutVisitor};
+
+pub struct ImageVisitor {
+ images: Vec<PathBuf>,
+}
+
+impl ImageVisitor {
+ pub fn new() -> Self {
+ ImageVisitor { images: vec![] }
+ }
+
+ pub fn images(&self) -> Vec<PathBuf> {
+ self.images.clone()
+ }
+}
+
+impl MutVisitor for ImageVisitor {
+ fn visit_inline(&mut self, inline: &mut Inline) {
+ if let Inline::Image(_attr, _inlines, target) = inline {
+ self.images.push(PathBuf::from(&target.0));
+ }
+ }
+}
diff --git a/src/visitor/linting.rs b/src/visitor/linting.rs
new file mode 100644
index 0000000..a5171f9
--- /dev/null
+++ b/src/visitor/linting.rs
@@ -0,0 +1,40 @@
+use crate::panhelper;
+use crate::SubplotError;
+
+use pandoc_ast::{Block, MutVisitor};
+
+#[derive(Default)]
+pub struct LintingVisitor {
+ pub issues: Vec<SubplotError>,
+}
+
+impl MutVisitor for LintingVisitor {
+ fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
+ for block in vec_block {
+ match block {
+ Block::CodeBlock(attr, _) => {
+ if panhelper::is_class(attr, "file") {
+ let newlines: Vec<_> =
+ panhelper::find_attr_kv(&attr, "add-newline").collect();
+ match newlines.len() {
+ 0 => {}
+ 1 => match newlines[0].to_ascii_lowercase().as_ref() {
+ "auto" | "yes" | "no" => {}
+ _ => self.issues.push(SubplotError::UnrecognisedAddNewline(
+ panhelper::get_filename(&attr),
+ newlines[0].to_owned(),
+ )),
+ },
+ _ => self.issues.push(SubplotError::RepeatedAddNewlineAttribute(
+ panhelper::get_filename(&attr),
+ )),
+ }
+ }
+ }
+ _ => {
+ self.visit_block(block);
+ }
+ }
+ }
+ }
+}
diff --git a/src/visitor/mod.rs b/src/visitor/mod.rs
new file mode 100644
index 0000000..95bf2b1
--- /dev/null
+++ b/src/visitor/mod.rs
@@ -0,0 +1,17 @@
+mod block_class;
+pub use block_class::BlockClassVisitor;
+
+mod datafiles;
+
+mod image;
+pub use image::ImageVisitor;
+
+mod linting;
+pub use linting::LintingVisitor;
+
+mod structure;
+pub use structure::Element;
+pub use structure::StructureVisitor;
+
+mod typesetting;
+pub use typesetting::TypesettingVisitor;
diff --git a/src/visitor/structure.rs b/src/visitor/structure.rs
new file mode 100644
index 0000000..56e61a7
--- /dev/null
+++ b/src/visitor/structure.rs
@@ -0,0 +1,82 @@
+use crate::panhelper;
+
+use pandoc_ast::{Block, Inline, MutVisitor};
+
+// A structure element in the document: a heading or a scenario snippet.
+#[derive(Debug)]
+pub enum Element {
+ // Headings consist of the text and the level of the heading.
+ Heading(String, i64),
+
+ // Scenario snippets consist just of the unparsed text.
+ Snippet(String),
+}
+
+impl Element {
+ pub fn heading(text: &str, level: i64) -> Element {
+ Element::Heading(text.to_string(), level)
+ }
+
+ pub fn snippet(text: &str) -> Element {
+ Element::Snippet(text.to_string())
+ }
+}
+
+// A MutVisitor for extracting document structure.
+pub struct StructureVisitor {
+ pub elements: Vec<Element>,
+}
+
+impl StructureVisitor {
+ pub fn new() -> Self {
+ Self { elements: vec![] }
+ }
+}
+
+impl MutVisitor for StructureVisitor {
+ fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
+ use panhelper::is_class;
+ for block in vec_block {
+ match block {
+ Block::Header(level, _attr, inlines) => {
+ let text = join(inlines);
+ let heading = Element::heading(&text, *level);
+ self.elements.push(heading);
+ }
+ Block::CodeBlock(attr, s) => {
+ if is_class(attr, "scenario") {
+ let snippet = Element::snippet(s);
+ self.elements.push(snippet);
+ }
+ }
+ _ => {
+ self.visit_block(block);
+ }
+ }
+ }
+ }
+}
+
+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::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_str(" "),
+ pandoc_ast::Inline::SoftBreak => buf.push_str(" "),
+ pandoc_ast::Inline::LineBreak => buf.push_str(" "),
+ _ => panic!("unknown pandoc_ast::Inline component {:?}", item),
+ }
+ }
+}
diff --git a/src/visitor/typesetting.rs b/src/visitor/typesetting.rs
new file mode 100644
index 0000000..af6ea01
--- /dev/null
+++ b/src/visitor/typesetting.rs
@@ -0,0 +1,49 @@
+use crate::panhelper;
+use crate::typeset;
+use crate::Bindings;
+
+use pandoc_ast::{Block, MutVisitor};
+
+/// Visitor for the pandoc AST.
+///
+/// This includes rendering stuff which we find as we go
+pub struct TypesettingVisitor<'a> {
+ bindings: &'a Bindings,
+}
+
+impl<'a> TypesettingVisitor<'a> {
+ pub fn new(bindings: &'a Bindings) -> Self {
+ TypesettingVisitor { bindings }
+ }
+}
+
+// Visit interesting parts of the Pandoc abstract syntax tree. The
+// document top level is a vector of blocks and we visit that and
+// replace any fenced code block with the scenario tag with a typeset
+// paragraph. Also, replace fenced code blocks with known graph markup
+// with the rendered SVG image.
+impl<'a> MutVisitor for TypesettingVisitor<'a> {
+ fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
+ use panhelper::is_class;
+ for block in vec_block {
+ match block {
+ Block::CodeBlock(attr, s) => {
+ if is_class(attr, "scenario") {
+ *block = typeset::scenario_snippet(&self.bindings, s)
+ } else if is_class(attr, "file") {
+ *block = typeset::file_block(attr, s)
+ } else if is_class(attr, "dot") {
+ *block = typeset::dot_to_block(s)
+ } else if is_class(attr, "plantuml") {
+ *block = typeset::plantuml_to_block(s)
+ } else if is_class(attr, "roadmap") {
+ *block = typeset::roadmap_to_block(s)
+ }
+ }
+ _ => {
+ self.visit_block(block);
+ }
+ }
+ }
+ }
+}