summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-12-11 09:00:20 +0000
committerLars Wirzenius <liw@liw.fi>2021-12-11 09:00:20 +0000
commit553dd2eef4e019dca2d2b7040c3a4f02cf4ed336 (patch)
tree015dcdca2eebe2bb6f06ea97b4ea5fcbf952becd
parent6e6cffca71c75a1f8c5f460db1348a070262ccff (diff)
parent4413917ca5dfb53a6ca5f2c6fe78a2f948af0c36 (diff)
downloadpandoc-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.md34
-rw-r--r--src/bin/pandoc-filter-diagram.rs17
-rw-r--r--src/lib.rs140
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(())
}
diff --git a/src/lib.rs b/src/lib.rs
index 40ae760..eb55090 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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()),