diff options
author | Daniel Silverstone <dsilvers+gitlab@digital-scurf.org> | 2022-09-03 09:45:23 +0000 |
---|---|---|
committer | Daniel Silverstone <dsilvers+gitlab@digital-scurf.org> | 2022-09-03 09:45:23 +0000 |
commit | da1aea95d5304ac5dd8c4658acf5bd3efaee0fa8 (patch) | |
tree | 676130f43ff2270f2f2ef116cc927214a67983ad | |
parent | 404006dfb651687713b7b8aa3183ed5c2fbe8acb (diff) | |
parent | c4b98e69050a94cac865af4d5f92985b9e5262b3 (diff) | |
download | subplot-da1aea95d5304ac5dd8c4658acf5bd3efaee0fa8.tar.gz |
Merge branch 'liw/refactor' into 'main'
Refactoring ready for YAML-from-separate-file
See merge request subplot/subplot!286
-rw-r--r-- | src/ast.rs | 149 | ||||
-rw-r--r-- | src/bin/cli/mod.rs | 4 | ||||
-rw-r--r-- | src/bin/subplot.rs | 4 | ||||
-rw-r--r-- | src/diagrams.rs | 40 | ||||
-rw-r--r-- | src/doc.rs | 45 | ||||
-rw-r--r-- | src/embedded.rs (renamed from src/datafiles.rs) | 22 | ||||
-rw-r--r-- | src/lib.rs | 10 | ||||
-rw-r--r-- | src/typeset.rs | 8 | ||||
-rw-r--r-- | src/visitor/embedded.rs (renamed from src/visitor/datafiles.rs) | 8 | ||||
-rw-r--r-- | src/visitor/mod.rs | 2 |
10 files changed, 140 insertions, 152 deletions
@@ -25,51 +25,42 @@ lazy_static! { #[derive(Debug)] pub struct AbstractSyntaxTree { blocks: Vec<Block>, - meta: Map<String, MetaValue>, + meta: YamlMetadata, } impl AbstractSyntaxTree { - // Create a new AST. - // - // Note that this is not public. - fn new(meta: Map<String, MetaValue>, blocks: Vec<Block>) -> Self { + /// 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.clone(), + meta: self.meta.to_map(), blocks: self.blocks.clone(), pandoc_api_version: vec![1, 20], } } } -impl std::str::FromStr for AbstractSyntaxTree { - type Err = Error; - - /// Create an abstract syntax tree from a string. - fn from_str(markdown: &str) -> Result<Self, Self::Err> { - trace!("Parsing markdown"); - let ast = if let Some((yaml, markdown)) = get_yaml(&LEADING_YAML_PATTERN, markdown) { - trace!("Found leading YAML: {:?}", yaml); - let meta = Metadata::new(yaml)?.to_map(); - let blocks = parse_blocks(markdown); - AbstractSyntaxTree::new(meta, blocks) - } else if let Some((yaml, _markdown)) = get_yaml(&TRAILING_YAML_PATTERN, markdown) { - trace!("Found trailing YAML: {:?}", yaml); - let meta = Metadata::new(yaml)?.to_map(); - let blocks = parse_blocks(markdown); - AbstractSyntaxTree::new(meta, blocks) - } else { - trace!("No YAML to be found"); - let blocks = parse_blocks(markdown); - AbstractSyntaxTree::new(Map::new(), blocks) - }; - trace!("Parsing markdown: OK"); - Ok(ast) - } +/// Extract YAML metadata from a Markdown document. +pub fn extract_metadata(markdown: &str) -> Result<(YamlMetadata, &str), Error> { + trace!("Extracting YAML from Markdown"); + let (yaml, md) = if let Some((yaml, markdown)) = get_yaml(&LEADING_YAML_PATTERN, markdown) { + trace!("Found leading YAML: {:?}", yaml); + (yaml, markdown) + } else if let Some((yaml, _markdown)) = get_yaml(&TRAILING_YAML_PATTERN, markdown) { + trace!("Found trailing YAML: {:?}", yaml); + (yaml, markdown) + } else { + trace!("No YAML to be found"); + return Err(Error::NoMetadata); + }; + let meta = YamlMetadata::new(yaml)?; + trace!("Parsing markdown: OK"); + Ok((meta, md)) } // Extract a YAML metadata block using a given regex. @@ -272,22 +263,25 @@ pub enum Error { #[error(transparent)] Regex(#[from] regex::Error), + #[error("Markdown doesn't contain a YAML block for document metadata")] + NoMetadata, + #[error(transparent)] Yaml(#[from] serde_yaml::Error), } -// Document metadata. -// -// This is expressed in the Markdown input file as an embedded YAML -// block. -// -// Note that this structure needs to be able to capture any metadata -// block we can work with, in any input file. By being strict here we -// make it easier to tell the user when a metadata block has, say, a -// misspelled field. +/// Document metadata. +/// +/// This is expressed in the Markdown input file as an embedded YAML +/// block. +/// +/// Note that this structure needs to be able to capture any metadata +/// block we can work with, in any input file. By being strict here we +/// make it easier to tell the user when a metadata block has, say, a +/// misspelled field. #[derive(Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] -struct Metadata { +pub struct YamlMetadata { title: String, subtitle: Option<String>, author: Option<String>, @@ -300,7 +294,7 @@ struct Metadata { impls: BTreeMap<String, Vec<PathBuf>>, } -impl Metadata { +impl YamlMetadata { fn new(yaml_text: &str) -> Result<Self, Error> { trace!("Parsing YAML"); let meta: Self = serde_yaml::from_str(yaml_text)?; @@ -310,45 +304,51 @@ impl Metadata { fn to_map(&self) -> Map<String, MetaValue> { trace!("Creating metadata map from parsed YAML"); let mut map: Map<String, MetaValue> = Map::new(); - map.insert(s("title"), meta_string(&self.title)); + + map.insert("title".into(), meta_string(&self.title)); + if let Some(v) = &self.subtitle { - map.insert(s("subtitle"), meta_string(v)); + map.insert("subtitle".into(), meta_string(v)); } + if let Some(v) = &self.author { - map.insert(s("author"), meta_string(v)); + map.insert("author".into(), meta_string(v)); } + if let Some(v) = &self.date { - map.insert(s("date"), meta_string(v)); + map.insert("date".into(), meta_string(v)); } + if let Some(v) = &self.classes { - map.insert(s("classes"), meta_strings(v)); + 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(s("impls"), MetaValue::MetaMap(impls)); + map.insert("impls".into(), MetaValue::MetaMap(impls)); } + if let Some(v) = &self.bibliography { - map.insert(s("bibliography"), meta_path_bufs(v)); + map.insert("bibliography".into(), meta_path_bufs(v)); } + if let Some(v) = &self.bindings { - map.insert(s("bindings"), meta_path_bufs(v)); + map.insert("bindings".into(), meta_path_bufs(v)); } + if let Some(v) = &self.documentclass { - map.insert(s("documentclass"), meta_string(v)); + map.insert("documentclass".into(), meta_string(v)); } + trace!("Created metadata map from parsed YAML"); map } } -fn s(s: &str) -> String { - s.to_string() -} - fn meta_string(s: &str) -> MetaValue { MetaValue::MetaString(s.to_string()) } @@ -367,10 +367,8 @@ fn meta_path_bufs(v: &[PathBuf]) -> MetaValue { #[cfg(test)] mod test { - use super::{parse_code_block_attrs, AbstractSyntaxTree, Metadata}; - use super::{Block, Inline}; + use super::{extract_metadata, parse_code_block_attrs, AbstractSyntaxTree, YamlMetadata}; use std::path::PathBuf; - use std::str::FromStr; #[test] fn code_block_attrs() { @@ -394,38 +392,10 @@ mod test { } #[test] - fn empty_input() { - let ast = AbstractSyntaxTree::from_str("").unwrap(); - let doc = ast.to_pandoc(); - assert!(doc.blocks.is_empty()); - assert!(doc.meta.is_empty()); - assert!(!doc.pandoc_api_version.is_empty()); - } - - #[test] - fn simple() { - let ast = AbstractSyntaxTree::from_str( - "\ - # Introduction \n\ - \n\ - First paragraph.\n\ - ", - ) - .unwrap(); - let doc = ast.to_pandoc(); - assert!(doc.meta.is_empty()); - assert!(!doc.pandoc_api_version.is_empty()); - - let attr = ("".to_string(), vec![], vec![]); - let h = Block::Header(1, attr, vec![Inline::Str("Introduction".to_string())]); - let para = Block::Para(vec![Inline::Str("First paragraph.".to_string())]); - assert_eq!(doc.blocks, &[h, para]); - } - - #[test] fn parses_leading_meta() { let markdown = "\n\n---\ntitle: Foo Bar\n...\nfoobar\n"; - let ast = AbstractSyntaxTree::from_str(markdown).unwrap(); + let (meta, markdown) = extract_metadata(markdown).unwrap(); + let ast = AbstractSyntaxTree::new(meta, markdown); let doc = ast.to_pandoc(); let keys: Vec<String> = doc.meta.keys().cloned().collect(); assert_eq!(keys, ["title"]); @@ -434,7 +404,8 @@ mod test { #[test] fn parses_trailing_meta() { let markdown = "foobar\n---\ntitle: Foo Bar\n...\n\n\n"; - let ast = AbstractSyntaxTree::from_str(markdown).unwrap(); + let (meta, markdown) = extract_metadata(markdown).unwrap(); + let ast = AbstractSyntaxTree::new(meta, markdown); let doc = ast.to_pandoc(); let keys: Vec<String> = doc.meta.keys().cloned().collect(); assert_eq!(keys, ["title"]); @@ -442,7 +413,7 @@ mod test { #[test] fn full_meta() { - let meta = Metadata::new( + let meta = YamlMetadata::new( "\ title: Foo Bar date: today diff --git a/src/bin/cli/mod.rs b/src/bin/cli/mod.rs index a16df87..e78a92d 100644 --- a/src/bin/cli/mod.rs +++ b/src/bin/cli/mod.rs @@ -9,9 +9,9 @@ use std::fmt::Debug; use std::path::Path; use std::str::FromStr; use std::{collections::HashMap, convert::TryFrom}; -use subplot::{DataFile, Document, Style, SubplotError}; +use subplot::{Document, EmbeddedFile, Style, SubplotError}; -pub fn extract_file<'a>(doc: &'a Document, filename: &str) -> Result<&'a DataFile> { +pub fn extract_file<'a>(doc: &'a Document, filename: &str) -> Result<&'a EmbeddedFile> { for file in doc.files() { if file.filename() == filename { return Ok(file); diff --git a/src/bin/subplot.rs b/src/bin/subplot.rs index 759b70b..b618ac9 100644 --- a/src/bin/subplot.rs +++ b/src/bin/subplot.rs @@ -6,7 +6,7 @@ use anyhow::Result; use env_logger::fmt::Color; use log::{debug, error, info, trace, warn}; use subplot::{ - codegen, load_document, resource, DataFile, Document, MarkupOpts, Style, SubplotError, + codegen, load_document, resource, Document, EmbeddedFile, MarkupOpts, Style, SubplotError, }; use time::{format_description::FormatItem, macros::format_description, OffsetDateTime}; @@ -165,7 +165,7 @@ impl Extract { fn run(&self) -> Result<()> { let doc = load_linted_doc(&self.filename, Style::default(), None, self.merciful)?; - let files: Vec<&DataFile> = if self.embedded.is_empty() { + let files: Vec<&EmbeddedFile> = if self.embedded.is_empty() { doc.files() .iter() .map(Result::Ok) diff --git a/src/diagrams.rs b/src/diagrams.rs index 6e0b875..a62553f 100644 --- a/src/diagrams.rs +++ b/src/diagrams.rs @@ -59,6 +59,32 @@ lazy_static! { static ref JAVA_PATH: Mutex<PathBuf> = Mutex::new(env!("BUILTIN_JAVA_PATH").into()); } +/// An SVG image. +/// +/// SVG images are vector images, but we only need to treat them as +/// opaque blobs of bytes, so we don't try to represent them in any +/// other way. +pub struct Svg { + data: Vec<u8>, +} + +impl Svg { + fn new(data: Vec<u8>) -> Self { + Self { data } + } + + /// Return slice of the bytes of the image. + pub fn data(&self) -> &[u8] { + &self.data + } + + /// Number of bytes in the binary representation of the image. + #[allow(clippy::len_without_is_empty)] // is-empty doesn't make sense + pub fn len(&self) -> usize { + self.data.len() + } +} + /// A code block with markup for a diagram. /// /// The code block will be converted to an SVG image using an external @@ -71,7 +97,7 @@ lazy_static! { /// for the trait. pub trait DiagramMarkup { /// Convert the markup into an SVG. - fn as_svg(&self) -> Result<Vec<u8>, SubplotError>; + fn as_svg(&self) -> Result<Svg, SubplotError>; } /// A code block with pikchr markup. @@ -98,12 +124,12 @@ impl PikchrMarkup { } impl DiagramMarkup for PikchrMarkup { - fn as_svg(&self) -> Result<Vec<u8>, SubplotError> { + fn as_svg(&self) -> Result<Svg, SubplotError> { let mut flags = pikchr::PikchrFlags::default(); flags.generate_plain_errors(); let image = pikchr::Pikchr::render(&self.markup, self.class.as_deref(), flags) .map_err(SubplotError::PikchrRenderError)?; - Ok(image.as_bytes().to_vec()) + Ok(Svg::new(image.as_bytes().to_vec())) } } @@ -129,7 +155,7 @@ impl DotMarkup { } impl DiagramMarkup for DotMarkup { - fn as_svg(&self) -> Result<Vec<u8>, SubplotError> { + fn as_svg(&self) -> Result<Svg, SubplotError> { let path = DOT_PATH.lock().unwrap().clone(); let mut child = Command::new(&path) .arg("-Tsvg") @@ -146,7 +172,7 @@ impl DiagramMarkup for DotMarkup { .wait_with_output() .map_err(SubplotError::WaitForChild)?; if output.status.success() { - Ok(output.stdout) + Ok(Svg::new(output.stdout)) } else { Err(SubplotError::child_failed("dot", &output)) } @@ -196,7 +222,7 @@ impl PlantumlMarkup { } impl DiagramMarkup for PlantumlMarkup { - fn as_svg(&self) -> Result<Vec<u8>, SubplotError> { + fn as_svg(&self) -> Result<Svg, SubplotError> { let path = JAVA_PATH.lock().unwrap().clone(); let mut cmd = Command::new(&path); cmd.arg("-Djava.awt.headless=true") @@ -225,7 +251,7 @@ impl DiagramMarkup for PlantumlMarkup { .wait_with_output() .map_err(SubplotError::WaitForChild)?; if output.status.success() { - Ok(output.stdout) + Ok(Svg::new(output.stdout)) } else { Err(SubplotError::child_failed("plantuml", &output)) } @@ -2,8 +2,8 @@ use crate::ast; use crate::generate_test_program; use crate::get_basedir_from; use crate::visitor; -use crate::DataFile; -use crate::DataFiles; +use crate::EmbeddedFile; +use crate::EmbeddedFiles; use crate::LintingVisitor; use crate::MatchedScenario; use crate::Metadata; @@ -20,7 +20,6 @@ use std::default::Default; use std::fmt::Debug; use std::ops::Deref; use std::path::{Path, PathBuf}; -use std::str::FromStr; use pandoc_ast::{MutVisitor, Pandoc}; @@ -84,7 +83,7 @@ pub struct Document { markdowns: Vec<PathBuf>, ast: Pandoc, meta: Metadata, - files: DataFiles, + files: EmbeddedFiles, style: Style, warnings: Warnings, } @@ -94,7 +93,7 @@ impl Document { markdowns: Vec<PathBuf>, ast: Pandoc, meta: Metadata, - files: DataFiles, + files: EmbeddedFiles, style: Style, ) -> Document { Document { @@ -130,28 +129,12 @@ impl Document { // Currently we can't really return more than one error so return one return Err(linter.issues.remove(0)); } - let files = DataFiles::new(&mut ast); + let files = EmbeddedFiles::new(&mut ast); let doc = Document::new(markdowns, ast, meta, files, style); trace!("Loaded from JSON OK"); Ok(doc) } - /// Construct a Document from a JSON AST - pub fn from_json<P>( - basedir: P, - markdowns: Vec<PathBuf>, - json: &str, - style: Style, - template: Option<&str>, - ) -> Result<Document, SubplotError> - where - P: AsRef<Path> + Debug, - { - trace!("Parsing document..."); - let ast: Pandoc = serde_json::from_str(json).map_err(SubplotError::AstJson)?; - Self::from_ast(basedir, markdowns, ast, style, template) - } - /// Construct a Document from a named file. /// /// The file can be in any format Pandoc understands. This runs @@ -182,13 +165,20 @@ impl Document { // Add external Pandoc filters. crate::policy::add_citeproc(&mut pandoc); - trace!("Invoking Pandoc to parse document {:?}", filename); - let output = match pandoc.execute().map_err(SubplotError::Pandoc)? { + trace!( + "Invoking Pandoc to parse document {:?} into AST as JSON", + filename + ); + let json = match pandoc.execute().map_err(SubplotError::Pandoc)? { pandoc::PandocOutput::ToBuffer(o) => o, _ => return Err(SubplotError::NotJson), }; trace!("Pandoc was happy"); - let doc = Document::from_json(basedir, markdowns, &output, style, template)?; + + trace!("Parsing document AST as JSON..."); + let ast: Pandoc = serde_json::from_str(&json).map_err(SubplotError::AstJson)?; + let doc = Self::from_ast(basedir, markdowns, ast, style, template)?; + trace!("Loaded document OK"); Ok(doc) } @@ -208,7 +198,8 @@ impl Document { let filename = filename.to_path_buf(); let markdown = std::fs::read_to_string(&filename) .map_err(|err| SubplotError::ReadFile(filename.clone(), err))?; - let ast = ast::AbstractSyntaxTree::from_str(&markdown)?; + let (meta, markdown) = ast::extract_metadata(&markdown)?; + let ast = ast::AbstractSyntaxTree::new(meta, markdown); trace!("Parsed document OK"); Self::from_ast(basedir, vec![filename], ast.to_pandoc(), style, template) @@ -273,7 +264,7 @@ impl Document { } /// Return list of files embeddedin the document. - pub fn files(&self) -> &[DataFile] { + pub fn files(&self) -> &[EmbeddedFile] { self.files.files() } diff --git a/src/datafiles.rs b/src/embedded.rs index 83e90d9..c868054 100644 --- a/src/datafiles.rs +++ b/src/embedded.rs @@ -3,15 +3,15 @@ use serde::{Deserialize, Serialize}; /// A data file embedded in the document. #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] -pub struct DataFile { +pub struct EmbeddedFile { filename: String, contents: String, } -impl DataFile { +impl EmbeddedFile { /// Create a new data file, with a name and contents. - pub fn new(filename: String, contents: String) -> DataFile { - DataFile { filename, contents } + pub fn new(filename: String, contents: String) -> EmbeddedFile { + EmbeddedFile { filename, contents } } /// Return name of embedded file. @@ -27,25 +27,25 @@ impl DataFile { /// A collection of data files embedded in document. #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] -pub struct DataFiles { - files: Vec<DataFile>, +pub struct EmbeddedFiles { + files: Vec<EmbeddedFile>, } -impl DataFiles { +impl EmbeddedFiles { /// Create new set of data files. - pub fn new(ast: &mut Pandoc) -> DataFiles { - let mut files = DataFiles { files: vec![] }; + 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) -> &[DataFile] { + pub fn files(&self) -> &[EmbeddedFile] { &self.files } /// Append a new data file. - pub fn push(&mut self, file: DataFile) { + pub fn push(&mut self, file: EmbeddedFile) { self.files.push(file); } } @@ -21,11 +21,11 @@ pub use error::Warnings; pub mod resource; mod diagrams; -pub use diagrams::{DiagramMarkup, DotMarkup, MarkupOpts, PikchrMarkup, PlantumlMarkup}; +pub use diagrams::{DiagramMarkup, DotMarkup, MarkupOpts, PikchrMarkup, PlantumlMarkup, Svg}; -mod datafiles; -pub use datafiles::DataFile; -pub use datafiles::DataFiles; +mod embedded; +pub use embedded::EmbeddedFile; +pub use embedded::EmbeddedFiles; mod panhelper; mod typeset; @@ -73,4 +73,4 @@ mod codegen; pub use codegen::generate_test_program; mod ast; -pub use ast::AbstractSyntaxTree; +pub use ast::{extract_metadata, AbstractSyntaxTree, YamlMetadata}; diff --git a/src/typeset.rs b/src/typeset.rs index 9522e69..f63206a 100644 --- a/src/typeset.rs +++ b/src/typeset.rs @@ -4,7 +4,7 @@ use crate::PartialStep; use crate::ScenarioStep; use crate::StepKind; use crate::SubplotError; -use crate::{DiagramMarkup, DotMarkup, PikchrMarkup, PlantumlMarkup}; +use crate::{DiagramMarkup, DotMarkup, PikchrMarkup, PlantumlMarkup, Svg}; use crate::{Warning, Warnings}; use pandoc_ast::Attr; @@ -213,7 +213,7 @@ pub fn roadmap_to_block(yaml: &str, warnings: &mut Warnings) -> Block { // Typeset an SVG, represented as a byte vector, as a Pandoc AST Block // element. -fn typeset_svg(svg: Vec<u8>) -> Block { +fn typeset_svg(svg: Svg) -> Block { let url = svg_as_data_url(svg); let attr = ("".to_string(), vec![], vec![]); let img = Inline::Image(attr, vec![], (url, "".to_string())); @@ -223,7 +223,7 @@ fn typeset_svg(svg: Vec<u8>) -> Block { // 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); +fn svg_as_data_url(svg: Svg) -> String { + let svg = base64::encode(svg.data()); format!("data:image/svg+xml;base64,{}", svg) } diff --git a/src/visitor/datafiles.rs b/src/visitor/embedded.rs index 53ab7bb..891240b 100644 --- a/src/visitor/datafiles.rs +++ b/src/visitor/embedded.rs @@ -1,10 +1,10 @@ use crate::panhelper; -use crate::DataFile; -use crate::DataFiles; +use crate::EmbeddedFile; +use crate::EmbeddedFiles; use pandoc_ast::{Block, MutVisitor}; -impl MutVisitor for DataFiles { +impl MutVisitor for EmbeddedFiles { fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) { use panhelper::is_class; for block in vec_block { @@ -23,7 +23,7 @@ impl MutVisitor for DataFiles { } else { contents.clone() }; - self.push(DataFile::new(panhelper::get_filename(attr), contents)); + self.push(EmbeddedFile::new(panhelper::get_filename(attr), contents)); } } _ => { diff --git a/src/visitor/mod.rs b/src/visitor/mod.rs index 95bf2b1..1c095ac 100644 --- a/src/visitor/mod.rs +++ b/src/visitor/mod.rs @@ -1,7 +1,7 @@ mod block_class; pub use block_class::BlockClassVisitor; -mod datafiles; +mod embedded; mod image; pub use image::ImageVisitor; |