diff options
author | Lars Wirzenius <liw@liw.fi> | 2021-11-29 12:49:12 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2021-11-29 18:31:17 +0200 |
commit | a73ab1321d0646655e3415094aa46a36da67d315 (patch) | |
tree | e82a2133619e9db7945336484d8a1450d4b7e5d5 /src | |
parent | 1d982b22f51468d87d9577d06007d107b67fe532 (diff) | |
download | pandoc-filter-diagram-a73ab1321d0646655e3415094aa46a36da67d315.tar.gz |
feat: add basic features
Add rendering of dot, plantuml, pikchr, roadmap, and raw SVG, from
fenced code blocks into SVG.
Sponsored-by: pep.foundation
Diffstat (limited to 'src')
-rw-r--r-- | src/bin/pandoc-filter-diagram.rs | 18 | ||||
-rw-r--r-- | src/lib.rs | 307 | ||||
-rw-r--r-- | src/main.rs | 3 |
3 files changed, 325 insertions, 3 deletions
diff --git a/src/bin/pandoc-filter-diagram.rs b/src/bin/pandoc-filter-diagram.rs new file mode 100644 index 0000000..80c8ba9 --- /dev/null +++ b/src/bin/pandoc-filter-diagram.rs @@ -0,0 +1,18 @@ +use pandoc_filter_diagram::DiagramFilter; +use std::io::{Read, Write}; + +fn main() { + if let Err(err) = real_main() { + eprintln!("ERROR: {}", err); + std::process::exit(1); + } +} + +fn real_main() -> anyhow::Result<()> { + 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)); + std::io::stdout().write_all(json.as_bytes())?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..471a42e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,307 @@ +//! Render diagram markup in a Pandoc AST into SVG +//! +//! Process a Pandoc abstract syntax tree and convert fenced code +//! blocks with diagram markup into embedded SVG images. Supported +//! diagram markup languages: +//! +//! - [pikchr](https://pikchr.org/home/doc/trunk/homepage.md) +//! - [Graphviz dot](https://graphviz.org/doc/info/lang.html) +//! - [PlantUML](https://plantuml.com/) +//! - [roadmap](https://gitlab.com/larswirzenius/roadmap) +//! - [raw SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) +//! +//! 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. +//! +//! Note that this library does not do any parsing. The +//! `pandoc_ast::filter` function does that. + +use pandoc_ast::{Block, Inline, MutVisitor, Pandoc}; +use std::env; +use std::ffi::OsString; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +/// Represent the diagram filter. +#[derive(Debug)] +pub struct DiagramFilter { + dot: PathBuf, + roadmap_width: usize, + java_path: PathBuf, + plantuml_jar: PathBuf, +} + +/// Possible errors for diagram filtering. +#[derive(Debug, thiserror::Error)] +pub enum DiagramError { + /// When rendering a pikchr, something went wrong. + #[error("failure rendering pikchr diagram: {0}")] + PikchrRenderError(String), + + /// When rendering a roadmap, something went wrong. + #[error("failure rendering roadmap diagram: {0}")] + Roadmap(#[from] roadmap::RoadmapError), + + /// Failed to invoke a program. + /// + /// 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), + + /// A helper program failed + /// + /// The filter uses some helper programs to implement some of its + /// functionality, for example the GraphViz dot program. This + /// error means that the helper program failed (exit code was not + /// zero). + /// + /// 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), + + /// I/O error + /// + /// The filter did some I/O, and it failed. This is a generic + /// wrapper for any kind of I/O error. + #[error(transparent)] + IoError(#[from] std::io::Error), +} + +type Svg = Vec<u8>; + +impl DiagramFilter { + pub fn new() -> Self { + Self { + dot: PathBuf::from("dot"), + roadmap_width: 50, + java_path: PathBuf::from("java"), + plantuml_jar: PathBuf::from("/usr/share/plantuml/plantuml.jar"), + } + } + + pub fn dot_path<P>(&mut self, path: P) -> &mut Self + where + P: AsRef<Path>, + { + self.dot = path.as_ref().to_path_buf(); + self + } + + pub fn java_path<P>(&mut self, path: P) -> &mut Self + where + P: AsRef<Path>, + { + self.java_path = path.as_ref().to_path_buf(); + self + } + + pub fn plantuml_jar<P>(&mut self, path: P) -> &mut Self + where + P: AsRef<Path>, + { + self.plantuml_jar = path.as_ref().to_path_buf(); + self + } + + 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 { + let msg = format!("ERROR: {}", error.to_string()); + let msg = Inline::Str(String::from(msg)); + let msg = vec![Inline::Strong(vec![msg])]; + Block::Para(msg) + } + + fn svg_block(&self, svg: &[u8]) -> Block { + let url = self.svg_as_data_url(svg); + let attr = ("".to_string(), vec![], vec![]); + let img = Inline::Image(attr, vec![], (url, "".to_string())); + Block::Para(vec![img]) + } + + fn svg_as_data_url(&self, svg: &[u8]) -> String { + let svg = base64::encode(&svg); + format!("data:image/svg+xml;base64,{}", svg) + } + + fn pikchr_to_svg(&self, markup: &str, _class: Option<&str>) -> Result<Svg, DiagramError> { + let mut flags = pikchr::PikchrFlags::default(); + flags.generate_plain_errors(); + let image = + pikchr::Pikchr::render(markup, None, flags).map_err(DiagramError::PikchrRenderError)?; + Ok(image.as_bytes().to_vec()) + } + + fn dot_to_svg(&self, markup: &str) -> Result<Svg, DiagramError> { + filter_via(markup, "dot", &self.dot, &["-Tsvg"], None) + } + + fn roadmap_to_svg(&self, markup: &str) -> Result<Svg, DiagramError> { + match roadmap::from_yaml(markup) { + Ok(ref mut roadmap) => { + roadmap.set_missing_statuses(); + match roadmap.format_as_dot(self.roadmap_width) { + Ok(dot) => self.dot_to_svg(&dot), + Err(e) => Err(DiagramError::Roadmap(e)), + } + } + Err(err) => Err(DiagramError::Roadmap(err)), + } + } + + fn plantuml_to_svg(&self, markup: &str) -> Result<Svg, DiagramError> { + let args = &[ + "-Djava.awt.headless=true", + "-jar", + self.plantuml_jar.to_str().unwrap(), + "--", + "-pipe", + "-tsvg", + "-v", + "-graphvizdot", + self.dot.to_str().unwrap(), + ]; + filter_via(markup, "java", &self.java_path, args, build_java_path()) + } +} + +// If JAVA_HOME is set, and PATH is set, then: +// Check if JAVA_HOME/bin is in PATH, if not, prepend it and return a new +// PATH +fn build_java_path() -> Option<OsString> { + let java_home = env::var_os("JAVA_HOME")?; + let cur_path = env::var_os("PATH")?; + let cur_path: Vec<_> = env::split_paths(&cur_path).collect(); + let java_home = PathBuf::from(java_home); + let java_bin = java_home.join("bin"); + if cur_path.iter().any(|v| v.as_os_str() == java_bin) { + // No need to add JAVA_HOME/bin it's already on-path + return None; + } + env::join_paths(Some(java_bin).iter().chain(cur_path.iter())).ok() +} + +fn filter_via( + markup: &str, + name: &str, + argv0: &Path, + args: &[&str], + cmdpath: Option<OsString>, +) -> Result<Svg, DiagramError> { + // Start program without waiting for it to finish. + let mut cmd = Command::new(argv0); + cmd.args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if let Some(p) = cmdpath { + cmd.env("PATH", p); + } + let mut child = cmd.spawn()?; + + if let Some(stdin) = child.stdin.as_mut() { + stdin.write_all(markup.as_bytes())?; + let output = child.wait_with_output()?; + if output.status.success() { + Ok(output.stdout) + } else { + let status = if let Some(code) = output.status.code() { + format!("{}", code) + } else { + format!("terminated by signal") + }; + let stderr = String::from_utf8_lossy(&output.stderr); + Err(DiagramError::HelperFailed( + name.to_string(), + argv0.to_path_buf(), + status, + stderr.into_owned(), + )) + } + } else { + // Something went wrong when we invoked the program. Read stderr. + let mut error_message = vec![]; + if let Some(mut stderr) = child.stderr { + stderr.read_to_end(&mut error_message)?; + } + Err(DiagramError::InvokeFailed( + name.to_string(), + argv0.to_path_buf(), + String::from_utf8_lossy(&error_message).into_owned(), + )) + } +} + +impl MutVisitor for DiagramFilter { + fn visit_block(&mut self, block: &mut Block) { + 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), + Ok(svg) => *block = self.svg_block(&svg), + }, + DiagramKind::Roadmap => match self.roadmap_to_svg(text) { + Err(err) => *block = self.error_block(err), + Ok(svg) => *block = self.svg_block(&svg), + }, + DiagramKind::Plantuml => match self.plantuml_to_svg(text) { + Err(err) => *block = self.error_block(err), + Ok(svg) => *block = self.svg_block(&svg), + }, + DiagramKind::Pikchr => match self.pikchr_to_svg(text, None) { + Err(err) => *block = self.error_block(err), + Ok(svg) => *block = self.svg_block(&svg), + }, + DiagramKind::Svg => *block = self.svg_block(text.as_bytes()), + DiagramKind::Other => (), + }, + _ => { + self.walk_block(block); + } + } + } +} + +enum DiagramKind { + GraphvizDot, + Pikchr, + Plantuml, + Roadmap, + Svg, + Other, +} + +impl DiagramKind { + fn from(classes: &Vec<String>) -> Self { + if has_class(classes, "pikchr") { + Self::Pikchr + } else if has_class(classes, "dot") { + Self::GraphvizDot + } else if has_class(classes, "roadmap") { + Self::Roadmap + } else if has_class(classes, "plantuml") { + Self::Plantuml + } else if has_class(classes, "svg") { + Self::Svg + } else { + Self::Other + } + } +} + +fn has_class(classes: &Vec<String>, wanted: &str) -> bool { + classes.iter().any(|s| s == wanted) +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index e7a11a9..0000000 --- a/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} |