summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-11-29 12:49:12 +0200
committerLars Wirzenius <liw@liw.fi>2021-11-29 18:31:17 +0200
commita73ab1321d0646655e3415094aa46a36da67d315 (patch)
treee82a2133619e9db7945336484d8a1450d4b7e5d5 /src
parent1d982b22f51468d87d9577d06007d107b67fe532 (diff)
downloadpandoc-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.rs18
-rw-r--r--src/lib.rs307
-rw-r--r--src/main.rs3
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!");
-}