diff options
author | Lars Wirzenius <liw@liw.fi> | 2021-12-11 09:00:20 +0000 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2021-12-11 09:00:20 +0000 |
commit | 553dd2eef4e019dca2d2b7040c3a4f02cf4ed336 (patch) | |
tree | 015dcdca2eebe2bb6f06ea97b4ea5fcbf952becd | |
parent | 6e6cffca71c75a1f8c5f460db1348a070262ccff (diff) | |
parent | 4413917ca5dfb53a6ca5f2c6fe78a2f948af0c36 (diff) | |
download | pandoc-filter-diagram-553dd2eef4e019dca2d2b7040c3a4f02cf4ed336.tar.gz |
Merge branch 'errors' into 'main'
Error handling improvements
Closes #2
See merge request larswirzenius/pandoc-filter-diagram!9
-rw-r--r-- | pandoc-filter-diagram.md | 34 | ||||
-rw-r--r-- | src/bin/pandoc-filter-diagram.rs | 17 | ||||
-rw-r--r-- | src/lib.rs | 140 |
3 files changed, 167 insertions, 24 deletions
diff --git a/pandoc-filter-diagram.md b/pandoc-filter-diagram.md index 86081d4..30e3cf5 100644 --- a/pandoc-filter-diagram.md +++ b/pandoc-filter-diagram.md @@ -47,7 +47,7 @@ and is not referenced as an external image. ~~~scenario given an installed Rust program pandoc-filter-diagram given file pikchr.md -when I run pandoc --filter subplot-filter pikchr.md -o pikchr.html +when I run pandoc --filter pandoc-filter-diagram pikchr.md -o pikchr.html then file pikchr.html matches regex /img src="data:image/svg\+xml;base64,/ ~~~ @@ -84,7 +84,7 @@ HTML output, not referenced as an external image. ~~~scenario given an installed Rust program pandoc-filter-diagram given file dot.md -when I run pandoc --filter subplot-filter dot.md -o dot.html +when I run pandoc --filter pandoc-filter-diagram dot.md -o dot.html then file dot.html matches regex /img src="data:image/svg\+xml;base64,/ ~~~ @@ -123,7 +123,7 @@ the HTML output, not referenced as an external image. ~~~scenario given an installed Rust program pandoc-filter-diagram given file plantuml.md -when I run pandoc --filter subplot-filter plantuml.md -o plantuml.html +when I run pandoc --filter pandoc-filter-diagram plantuml.md -o plantuml.html then file plantuml.html matches regex /img src="data:image/svg\+xml;base64,/ ~~~ @@ -202,7 +202,7 @@ HTML output, not referenced as an external image. ~~~scenario given an installed Rust program pandoc-filter-diagram given file roadmap.md -when I run pandoc --filter subplot-filter roadmap.md -o roadmap.html +when I run pandoc --filter pandoc-filter-diagram roadmap.md -o roadmap.html then file roadmap.html matches regex /img src="data:image/svg\+xml;base64,/ ~~~ @@ -252,3 +252,29 @@ blocked: - next ~~~ ~~~~~~~~ + + +# Command line tool + +The crate provides a command line tool `pandoc-filter-diagram`, for +use with the `pandoc --filter` option. By default, it ignores any +errors, but if the `PANDOC_FILTER_FAIL` environment variable is set +to 1, it fails if there were any errors. This scenario verifies that +this happens. The happy cases are already verified by other scenarios, +so this one only verifies handling of bad input. + +~~~scenario +given an installed Rust program pandoc-filter-diagram +given file bad.md +when I run pandoc --filter pandoc-filter-diagram bad.md -o bad.html +then exit code is 0 +when I try to run env PANDOC_FILTER_FAIL=1 pandoc --filter pandoc-filter-diagram bad.md -o bad.html +then command fails +~~~ + +~~~{#bad.md .file .markdown .numberLines} +trigraph "example" { +thing -> other +} +~~~ + diff --git a/src/bin/pandoc-filter-diagram.rs b/src/bin/pandoc-filter-diagram.rs index 80c8ba9..c2f808a 100644 --- a/src/bin/pandoc-filter-diagram.rs +++ b/src/bin/pandoc-filter-diagram.rs @@ -1,3 +1,5 @@ +//! A program that can be used with the pandoc --filter option. + use pandoc_filter_diagram::DiagramFilter; use std::io::{Read, Write}; @@ -9,10 +11,25 @@ fn main() { } fn real_main() -> anyhow::Result<()> { + let fail_on_error = if let Ok(v) = std::env::var("PANDOC_FILTER_FAIL") { + v == "1" + } else { + false + }; + let mut df = DiagramFilter::new(); let mut json = String::new(); std::io::stdin().read_to_string(&mut json)?; let json = pandoc_ast::filter(json, |doc| df.filter(doc)); + if !df.errors().is_empty() { + for e in df.errors().iter() { + eprintln!("ERROR: {}", e); + } + if fail_on_error { + eprintln!("Failing as requested"); + std::process::exit(1); + } + } std::io::stdout().write_all(json.as_bytes())?; Ok(()) } @@ -1,5 +1,8 @@ //! Render diagram markup in a Pandoc AST into SVG //! +//! Provide a [Pandoc filter](https://pandoc.org/filters.html) filter +//! to convert inline diagram markup into images. +//! //! Process a Pandoc abstract syntax tree and convert fenced code //! blocks with diagram markup into embedded SVG images. Supported //! diagram markup languages: @@ -10,13 +13,42 @@ //! - [roadmap](https://gitlab.com/larswirzenius/roadmap) //! - [raw SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) //! +//! Example: +//! +//! ~~~~~~markdown +//! This is a sample Markdown file. +//! +//! ```dot +//! digraph "broken" { +//! foo -> bar; +//! } +//! ``` +//! ~~~~~~ +//! //! The Pandoc AST has represents input as `Block` and other types of //! nodes. This crate turns `Block::CodeBlock` nodes that carry a //! class attribute that specifies one of the supported diagram markup -//! languages into blocks with embedded SVG graphics. +//! languages (`dot`, `plantuml`, `pikchr`, `roadmap`, `svg`) into +//! blocks with embedded SVG graphics. +//! +//! Note that this library does not do any parsing of an input +//! document. Use the [`pandoc_ast`](https://crates.io/crates/pandoc_ast) +//! or [`pandoc`](https://crates.io/crates/pandoc) crates for that. +//! +//! # Example //! -//! Note that this library does not do any parsing. The -//! `pandoc_ast::filter` function does that. +//! ``` +//! # use pandoc_filter_diagram::DiagramFilter; +//! # let mut json = r#"{"blocks":[{"t":"CodeBlock","c":[["",["dot"],[]],"digraph \"broken\" {\n foo -> bar;\n}"]}],"pandoc-api-version":[1,20],"meta":{}}"#; +//! let mut df = DiagramFilter::new(); +//! let json = pandoc_ast::filter(json.to_string(), |doc| df.filter(doc)); +//! if !df.errors().is_empty() { +//! for e in df.errors().iter() { +//! eprintln!("ERROR: {}", e); +//! } +//! std::process::exit(1); +//! } +//! ``` use pandoc_ast::{Block, Inline, MutVisitor, Pandoc}; use std::env; @@ -25,13 +57,27 @@ use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -/// Represent the diagram filter. +/// Convert inline diagram markup as images for Pandoc. +/// +/// This acts a filter on the Pandoc Abstract Syntax Tree (AST), to +/// modify it so that any inline markup for diagrams are rendered as +/// SVG images. The library is meant to be used with the +/// `pandoc_ast::filter` function. +/// +/// Filtering may fail. Because of the API constrain imposed by +/// `pandoc_ast::filter`, this library doesn't return a `Result`. +/// Instead, it collects any errors and lets the caller query for them +/// after the filtering is done (see the +/// [`errors`](DiagramFilter::errors) method). All errors are always +/// rendered as text in the document as well, but that requires a +/// human to read the document to spot any errors. #[derive(Debug)] pub struct DiagramFilter { dot: PathBuf, roadmap_width: usize, java_path: PathBuf, plantuml_jar: PathBuf, + errors: Vec<DiagramError>, } /// Possible errors for diagram filtering. @@ -39,7 +85,9 @@ pub struct DiagramFilter { pub enum DiagramError { /// When rendering a pikchr, something went wrong. #[error("failure rendering pikchr diagram: {0}")] - PikchrRenderError(String), + PikchrRenderError( + /// The error message from pikchr. + String), /// When rendering a roadmap, something went wrong. #[error("failure rendering roadmap diagram: {0}")] @@ -50,7 +98,15 @@ pub enum DiagramError { /// This Pandoc filter uses some helper programs to do some of its /// work. It failed to invoke such a helper program. #[error("failed to invoke helper program {0} (as {1}): {2}")] - InvokeFailed(String, PathBuf, String), + InvokeFailed( + /// Name of the program. + String, + + /// Path with which the program was invoked. + PathBuf, + + /// Standard error output of program. + String), /// A helper program failed /// @@ -61,8 +117,19 @@ pub enum DiagramError { /// /// This probably implies there's something wrong in the filter. /// Please report this error. - #[error("helper program failed: {0} (as {1}): {2}")] - HelperFailed(String, PathBuf, String, String), + #[error("helper program failed: {0} (as {1}), exit status {2}:\n{3}")] + HelperFailed( + /// Name of the program. + String, + + /// Path with which the program was invoked. + PathBuf, + + /// How did the program end? Status code or signal? + String, + + /// Standard error output of program. + String), /// I/O error /// @@ -75,21 +142,41 @@ pub enum DiagramError { type Svg = Vec<u8>; impl Default for DiagramFilter { + /// Create a filter with default settings. fn default() -> Self { Self { dot: PathBuf::from("dot"), roadmap_width: 50, java_path: PathBuf::from("java"), plantuml_jar: PathBuf::from("/usr/share/plantuml/plantuml.jar"), + errors: vec![], } } } impl DiagramFilter { + /// Create a new filter. pub fn new() -> Self { Self::default() } + /// Process a parsed document to convert inline diagram markup + /// into SVG. This method is suitable to be passed to + /// `pandoc_ast::filter` as the filter function argument. + pub fn filter(&mut self, mut doc: Pandoc) -> Pandoc { + self.walk_pandoc(&mut doc); + doc + } + + /// Return any errors that occurred during the filtering process. + /// The caller can decide how to report them to the user in a + /// suitable way. + pub fn errors(&self) -> &[DiagramError] { + &self.errors + } + + /// Set the name by which to invoke Graphviz `dot` program. The + /// default is "`dot`". pub fn dot_path<P>(&mut self, path: P) -> &mut Self where P: AsRef<Path>, @@ -98,6 +185,8 @@ impl DiagramFilter { self } + /// Set the name by which to invoke the Java runtime, for + /// PlantUML. The default is "`java`". pub fn java_path<P>(&mut self, path: P) -> &mut Self where P: AsRef<Path>, @@ -106,6 +195,8 @@ impl DiagramFilter { self } + /// Set the location of the PlantUML jar (Java bytecode archive). + /// The default is "`/usr/share/plantuml/plantuml.jar`". pub fn plantuml_jar<P>(&mut self, path: P) -> &mut Self where P: AsRef<Path>, @@ -114,17 +205,14 @@ impl DiagramFilter { self } + /// Set the maximum width, in characters, of the roadmap text + /// nodes. The default is 50. pub fn roadmap_width(&mut self, w: usize) -> &mut Self { self.roadmap_width = w; self } - pub fn filter(&mut self, mut doc: Pandoc) -> Pandoc { - self.walk_pandoc(&mut doc); - doc - } - - fn error_block(&self, error: DiagramError) -> Block { + fn error_block(&self, error: &DiagramError) -> Block { let msg = Inline::Str(format!("ERROR: {}", error.to_string())); let msg = vec![Inline::Strong(vec![msg])]; Block::Para(msg) @@ -228,12 +316,12 @@ fn filter_via( } else { String::from("terminated by signal") }; - let stderr = String::from_utf8_lossy(&output.stderr); + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); Err(DiagramError::HelperFailed( name.to_string(), argv0.to_path_buf(), status, - stderr.into_owned(), + stderr, )) } } else { @@ -255,19 +343,31 @@ impl MutVisitor for DiagramFilter { match block { Block::CodeBlock((_id, classes, _kv), text) => match DiagramKind::from(classes) { DiagramKind::GraphvizDot => match self.dot_to_svg(text) { - Err(err) => *block = self.error_block(err), + Err(err) => { + *block = self.error_block(&err); + self.errors.push(err); + } Ok(svg) => *block = self.svg_block(&svg), }, DiagramKind::Roadmap => match self.roadmap_to_svg(text) { - Err(err) => *block = self.error_block(err), + Err(err) => { + *block = self.error_block(&err); + self.errors.push(err); + } Ok(svg) => *block = self.svg_block(&svg), }, DiagramKind::Plantuml => match self.plantuml_to_svg(text) { - Err(err) => *block = self.error_block(err), + Err(err) => { + *block = self.error_block(&err); + self.errors.push(err); + } Ok(svg) => *block = self.svg_block(&svg), }, DiagramKind::Pikchr => match self.pikchr_to_svg(text, None) { - Err(err) => *block = self.error_block(err), + Err(err) => { + *block = self.error_block(&err); + self.errors.push(err); + } Ok(svg) => *block = self.svg_block(&svg), }, DiagramKind::Svg => *block = self.svg_block(text.as_bytes()), |