From ba31a2ad25fa0ae3f3fd49eaa5174f9520248b90 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 3 Sep 2022 09:03:13 +0300 Subject: refactor: add a type for SVG images This allows us to not use a generic byte vector, and slightly lessens the chance of mistakes. Also, it strongly encodes what we need to know about and do with SVG images. Sponsored-by: author --- src/diagrams.rs | 40 +++++++++++++++++++++++++++++++++------- src/lib.rs | 2 +- src/typeset.rs | 8 ++++---- 3 files changed, 38 insertions(+), 12 deletions(-) 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 = 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, +} + +impl Svg { + fn new(data: Vec) -> 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, SubplotError>; + fn as_svg(&self) -> Result; } /// A code block with pikchr markup. @@ -98,12 +124,12 @@ impl PikchrMarkup { } impl DiagramMarkup for PikchrMarkup { - fn as_svg(&self) -> Result, SubplotError> { + fn as_svg(&self) -> Result { 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, SubplotError> { + fn as_svg(&self) -> Result { 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, SubplotError> { + fn as_svg(&self) -> Result { 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)) } diff --git a/src/lib.rs b/src/lib.rs index 3b4e844..23076d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ 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; 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) -> 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) -> 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) -> 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) } -- cgit v1.2.1 From 2341869c78cea3647eadd46bbfc7f703541cee0b Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 3 Sep 2022 09:25:26 +0300 Subject: refactor: rename DataFile{,s} to EmbeddedFile{,s} to be systematic Sponsored-by: author --- src/bin/cli/mod.rs | 4 ++-- src/bin/subplot.rs | 4 ++-- src/datafiles.rs | 51 ------------------------------------------------ src/doc.rs | 12 ++++++------ src/embedded.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 6 +++--- src/visitor/datafiles.rs | 35 --------------------------------- src/visitor/embedded.rs | 35 +++++++++++++++++++++++++++++++++ src/visitor/mod.rs | 2 +- 9 files changed, 100 insertions(+), 100 deletions(-) delete mode 100644 src/datafiles.rs create mode 100644 src/embedded.rs delete mode 100644 src/visitor/datafiles.rs create mode 100644 src/visitor/embedded.rs 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/datafiles.rs b/src/datafiles.rs deleted file mode 100644 index 83e90d9..0000000 --- a/src/datafiles.rs +++ /dev/null @@ -1,51 +0,0 @@ -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, -} - -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/doc.rs b/src/doc.rs index 8a70306..ace3e67 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -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; @@ -84,7 +84,7 @@ pub struct Document { markdowns: Vec, ast: Pandoc, meta: Metadata, - files: DataFiles, + files: EmbeddedFiles, style: Style, warnings: Warnings, } @@ -94,7 +94,7 @@ impl Document { markdowns: Vec, ast: Pandoc, meta: Metadata, - files: DataFiles, + files: EmbeddedFiles, style: Style, ) -> Document { Document { @@ -130,7 +130,7 @@ 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) @@ -273,7 +273,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/embedded.rs b/src/embedded.rs new file mode 100644 index 0000000..c868054 --- /dev/null +++ b/src/embedded.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 EmbeddedFile { + filename: String, + contents: String, +} + +impl EmbeddedFile { + /// Create a new data file, with a name and contents. + pub fn new(filename: String, contents: String) -> EmbeddedFile { + EmbeddedFile { 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 EmbeddedFiles { + files: Vec, +} + +impl EmbeddedFiles { + /// Create new set of data files. + pub fn new(ast: &mut Pandoc) -> EmbeddedFiles { + let mut files = EmbeddedFiles { files: vec![] }; + files.walk_pandoc(ast); + files + } + + /// Return slice of all data files. + pub fn files(&self) -> &[EmbeddedFile] { + &self.files + } + + /// Append a new data file. + pub fn push(&mut self, file: EmbeddedFile) { + self.files.push(file); + } +} diff --git a/src/lib.rs b/src/lib.rs index 23076d5..1d0060b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,9 +23,9 @@ pub mod resource; mod diagrams; 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; diff --git a/src/visitor/datafiles.rs b/src/visitor/datafiles.rs deleted file mode 100644 index 53ab7bb..0000000 --- a/src/visitor/datafiles.rs +++ /dev/null @@ -1,35 +0,0 @@ -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) { - 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/embedded.rs b/src/visitor/embedded.rs new file mode 100644 index 0000000..891240b --- /dev/null +++ b/src/visitor/embedded.rs @@ -0,0 +1,35 @@ +use crate::panhelper; +use crate::EmbeddedFile; +use crate::EmbeddedFiles; + +use pandoc_ast::{Block, MutVisitor}; + +impl MutVisitor for EmbeddedFiles { + fn visit_vec_block(&mut self, vec_block: &mut Vec) { + 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(EmbeddedFile::new(panhelper::get_filename(attr), contents)); + } + } + _ => { + self.visit_block(block); + } + } + } + } +} 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; -- cgit v1.2.1 From 1827c48e73545e2fac1158814bc20b52547cfeda Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 3 Sep 2022 09:52:25 +0300 Subject: refactor: rename ast::Metadata to ast::YamlMetadata for clarity This reduces the confusion with metadata::Metadata and Pandoc's metadata. Sponsored-by: author --- src/ast.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index eb10efc..915e679 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -54,12 +54,12 @@ impl std::str::FromStr for AbstractSyntaxTree { 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 meta = YamlMetadata::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 meta = YamlMetadata::new(yaml)?.to_map(); let blocks = parse_blocks(markdown); AbstractSyntaxTree::new(meta, blocks) } else { @@ -287,7 +287,7 @@ pub enum Error { // misspelled field. #[derive(Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] -struct Metadata { +struct YamlMetadata { title: String, subtitle: Option, author: Option, @@ -300,7 +300,7 @@ struct Metadata { impls: BTreeMap>, } -impl Metadata { +impl YamlMetadata { fn new(yaml_text: &str) -> Result { trace!("Parsing YAML"); let meta: Self = serde_yaml::from_str(yaml_text)?; @@ -367,7 +367,7 @@ fn meta_path_bufs(v: &[PathBuf]) -> MetaValue { #[cfg(test)] mod test { - use super::{parse_code_block_attrs, AbstractSyntaxTree, Metadata}; + use super::{parse_code_block_attrs, AbstractSyntaxTree, YamlMetadata}; use super::{Block, Inline}; use std::path::PathBuf; use std::str::FromStr; @@ -442,7 +442,7 @@ mod test { #[test] fn full_meta() { - let meta = Metadata::new( + let meta = YamlMetadata::new( "\ title: Foo Bar date: today -- cgit v1.2.1 From adbaf65c09550cbc4fdb6d3272d7efa33eefb069 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 3 Sep 2022 09:57:14 +0300 Subject: refactor: use the more idiomatic Into to convert &str to String The helper function was unnecessary baggage, written by an inexperienced Rust programmer (i.e., me, three years ago). It's time to let go. Sponsored-by: author --- src/ast.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 915e679..b869696 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -310,18 +310,18 @@ impl YamlMetadata { fn to_map(&self) -> Map { trace!("Creating metadata map from parsed YAML"); let mut map: Map = 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 @@ -329,26 +329,22 @@ impl YamlMetadata { .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()) } -- cgit v1.2.1 From 7a500b07a7a272c6813b52df954b2e9466a25901 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 3 Sep 2022 10:23:51 +0300 Subject: refactor: make long function a little more readable with empty lines Sponsored-by: author --- src/ast.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ast.rs b/src/ast.rs index b869696..430e26c 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -310,19 +310,25 @@ impl YamlMetadata { fn to_map(&self) -> Map { trace!("Creating metadata map from parsed YAML"); let mut map: Map = Map::new(); + map.insert("title".into(), meta_string(&self.title)); + if let Some(v) = &self.subtitle { map.insert("subtitle".into(), meta_string(v)); } + if let Some(v) = &self.author { map.insert("author".into(), meta_string(v)); } + 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 @@ -331,15 +337,19 @@ impl YamlMetadata { .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)); } + trace!("Created metadata map from parsed YAML"); map } -- cgit v1.2.1 From 32a3e2a805c2470bd460d74f36f0b49b51453e62 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 3 Sep 2022 10:31:30 +0300 Subject: refactor: delay use of pandoc_ast::Map to as late as possible Use YamlMetadata instead. I find this to be clearer and that it reduces the coupling with pandoc_ast a little. This should help us when we implement document metadata in a separate YAML file instead of embedding it in the Markdown. Sponsored-by: author --- src/ast.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 430e26c..4fba52e 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -25,21 +25,21 @@ lazy_static! { #[derive(Debug)] pub struct AbstractSyntaxTree { blocks: Vec, - meta: Map, + meta: YamlMetadata, } impl AbstractSyntaxTree { // Create a new AST. // // Note that this is not public. - fn new(meta: Map, blocks: Vec) -> Self { + fn new(meta: YamlMetadata, blocks: Vec) -> Self { 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], } @@ -54,18 +54,18 @@ impl std::str::FromStr for AbstractSyntaxTree { trace!("Parsing markdown"); let ast = if let Some((yaml, markdown)) = get_yaml(&LEADING_YAML_PATTERN, markdown) { trace!("Found leading YAML: {:?}", yaml); - let meta = YamlMetadata::new(yaml)?.to_map(); + let meta = YamlMetadata::new(yaml)?; 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 = YamlMetadata::new(yaml)?.to_map(); + let meta = YamlMetadata::new(yaml)?; 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) + AbstractSyntaxTree::new(YamlMetadata::default(), blocks) }; trace!("Parsing markdown: OK"); Ok(ast) @@ -404,7 +404,6 @@ mod test { 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()); } @@ -419,7 +418,6 @@ mod test { ) .unwrap(); let doc = ast.to_pandoc(); - assert!(doc.meta.is_empty()); assert!(!doc.pandoc_api_version.is_empty()); let attr = ("".to_string(), vec![], vec![]); -- cgit v1.2.1 From 1898ff35ddbc717304c2afd1e25257013919b995 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 3 Sep 2022 11:19:47 +0300 Subject: refactor: expose functionality to extract embedded YAML from md Sponsored-by: author --- src/ast.rs | 88 +++++++++++++++++++++++++------------------------------------- src/lib.rs | 2 +- 2 files changed, 36 insertions(+), 54 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 4fba52e..afa8b3b 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -51,27 +51,33 @@ impl std::str::FromStr for AbstractSyntaxTree { /// Create an abstract syntax tree from a string. fn from_str(markdown: &str) -> Result { - trace!("Parsing markdown"); - let ast = if let Some((yaml, markdown)) = get_yaml(&LEADING_YAML_PATTERN, markdown) { - trace!("Found leading YAML: {:?}", yaml); - let meta = YamlMetadata::new(yaml)?; - 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 = YamlMetadata::new(yaml)?; - let blocks = parse_blocks(markdown); - AbstractSyntaxTree::new(meta, blocks) - } else { - trace!("No YAML to be found"); - let blocks = parse_blocks(markdown); - AbstractSyntaxTree::new(YamlMetadata::default(), blocks) - }; + trace!("Parsing markdown into AST"); + let (meta, markdown) = extract_metadata(markdown)?; + let blocks = parse_blocks(markdown); + let ast = AbstractSyntaxTree::new(meta, 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. fn get_yaml<'a>(pat: &Regex, markdown: &'a str) -> Option<(&'a str, &'a str)> { trace!("Markdown: {:?}", markdown); @@ -272,22 +278,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 YamlMetadata { +pub struct YamlMetadata { title: String, subtitle: Option, author: Option, @@ -399,33 +408,6 @@ mod test { ); } - #[test] - fn empty_input() { - let ast = AbstractSyntaxTree::from_str("").unwrap(); - let doc = ast.to_pandoc(); - assert!(doc.blocks.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.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"; diff --git a/src/lib.rs b/src/lib.rs index 1d0060b..725e49c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}; -- cgit v1.2.1 From ac65d482c4bae8a98feb0d74e2cbe797b61c1d84 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 3 Sep 2022 11:39:03 +0300 Subject: refactor: AST only parses Markdown, YAML is extracted outside it Sponsored-by: author --- src/ast.rs | 31 ++++++++----------------------- src/doc.rs | 4 ++-- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index afa8b3b..f02fdd1 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -29,10 +29,9 @@ pub struct AbstractSyntaxTree { } impl AbstractSyntaxTree { - // Create a new AST. - // - // Note that this is not public. - fn new(meta: YamlMetadata, blocks: Vec) -> Self { + /// Create a new AST. + pub fn new(meta: YamlMetadata, markdown: &str) -> Self { + let blocks = parse_blocks(markdown); Self { blocks, meta } } @@ -46,20 +45,6 @@ impl AbstractSyntaxTree { } } -impl std::str::FromStr for AbstractSyntaxTree { - type Err = Error; - - /// Create an abstract syntax tree from a string. - fn from_str(markdown: &str) -> Result { - trace!("Parsing markdown into AST"); - let (meta, markdown) = extract_metadata(markdown)?; - let blocks = parse_blocks(markdown); - let ast = AbstractSyntaxTree::new(meta, 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"); @@ -382,10 +367,8 @@ fn meta_path_bufs(v: &[PathBuf]) -> MetaValue { #[cfg(test)] mod test { - use super::{parse_code_block_attrs, AbstractSyntaxTree, YamlMetadata}; - 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() { @@ -411,7 +394,8 @@ mod test { #[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 = doc.meta.keys().cloned().collect(); assert_eq!(keys, ["title"]); @@ -420,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 = doc.meta.keys().cloned().collect(); assert_eq!(keys, ["title"]); diff --git a/src/doc.rs b/src/doc.rs index ace3e67..2f3368e 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -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}; @@ -208,7 +207,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) -- cgit v1.2.1 From c4b98e69050a94cac865af4d5f92985b9e5262b3 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 3 Sep 2022 11:51:02 +0300 Subject: refactor: drop unnecessary single-line function used from one caller Sponsored-by: author --- src/doc.rs | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/doc.rs b/src/doc.rs index 2f3368e..38e14b5 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -135,22 +135,6 @@ impl Document { Ok(doc) } - /// Construct a Document from a JSON AST - pub fn from_json

( - basedir: P, - markdowns: Vec, - json: &str, - style: Style, - template: Option<&str>, - ) -> Result - where - P: AsRef + 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 @@ -181,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) } -- cgit v1.2.1