From a73ab1321d0646655e3415094aa46a36da67d315 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 29 Nov 2021 12:49:12 +0200 Subject: feat: add basic features Add rendering of dot, plantuml, pikchr, roadmap, and raw SVG, from fenced code blocks into SVG. Sponsored-by: pep.foundation --- Cargo.lock | 460 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 16 ++ README.md | 65 ++++++ src/bin/pandoc-filter-diagram.rs | 18 ++ src/lib.rs | 307 ++++++++++++++++++++++++++ src/main.rs | 3 - 6 files changed, 866 insertions(+), 3 deletions(-) create mode 100644 Cargo.lock create mode 100644 README.md create mode 100644 src/bin/pandoc-filter-diagram.rs create mode 100644 src/lib.rs delete mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1c1ef48 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,460 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc78c299ae753905840c5d3ba036c51f61ce5a98a83f98d9c9d29dffd427f71" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap 0.11.0", + "unicode-width", + "vec_map", +] + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "pandoc-filter-diagram" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "pandoc_ast", + "pikchr", + "roadmap", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "pandoc_ast" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a940e63c65b34a7be2f847de6847b4bc9b68d74e3f7a5c648ca2fa5317f3bd06" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "pikchr" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0060934b0227a96428cbe79a42ad6d88cfbbc8490027bde64d12348948a6d2" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "roadmap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c700a8364fa11a9381dad24240223dfa00fc29083b5a70ebec51a9284ce30980" +dependencies = [ + "anyhow", + "serde_yaml", + "structopt", + "textwrap 0.14.2", + "thiserror", +] + +[[package]] +name = "ryu" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af" +dependencies = [ + "dtoa", + "indexmap", + "serde", + "yaml-rust", +] + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-linebreak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +dependencies = [ + "regex", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index 07b3b4d..1c3d0c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,23 @@ name = "pandoc-filter-diagram" version = "0.1.0" edition = "2021" +authors = [ + "Lars Wirzenius ", + "Daniel Silverstone ", +] +license = "MIT-0" +description = '''render diagram markup in a Pandoc abstract syntax tree as SVG''' +repository = "https://gitlab.com/larswirzenius/pandoc-filter-diagram" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.50" +base64 = "0.13.0" +pandoc_ast = "0.8.0" +pikchr = "0.1.1" +roadmap = "0.3.0" +serde = { version = "1.0.101", features = ["derive"] } +serde_json = "1.0.72" +thiserror = "1.0.30" diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbb4ac9 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# pandoc-filter-diagram -- render diagram markup in a Pandoc AST as SVG + +[Pandoc][] is a powerful tool for processing documents in various +formats. It parses each of its supported input formats into an +in-memory abstract syntax tree, and then renders any supported output +format from such a tree. In between, it can run an arbitrary program +to "filter" the tree, and the program can make any arbitrary changes +to the tree. + +This Rust crate provides a library to render as SVG diagrams in +various markup languages, embedded in Markdown as [fenced code blocks][] +that are marked with the appropriate class. + +~~~~~~~~markdown +# Example + +The following is a simplistic diagram using Graphviz dot language. + +~~~dot +digraph "example" { thing -> other } +~~~ +~~~~~~~~ + +This crate also provides a command line program, +`pandoc-filter-diagram`, which can be used with the Pandoc `--filter` +command line option. + +## Languages + +The library supports the following 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) + + +# Licence + +## The MIT License (MIT) + +Copyright 2019-2021 Lars Wirzenius, Daniel Silverstone + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +“Software”), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Fork this project to create your own MIT license that you can always +link to. 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; + +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

(&mut self, path: P) -> &mut Self + where + P: AsRef, + { + self.dot = path.as_ref().to_path_buf(); + self + } + + pub fn java_path

(&mut self, path: P) -> &mut Self + where + P: AsRef, + { + self.java_path = path.as_ref().to_path_buf(); + self + } + + pub fn plantuml_jar

(&mut self, path: P) -> &mut Self + where + P: AsRef, + { + 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 { + 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 { + filter_via(markup, "dot", &self.dot, &["-Tsvg"], None) + } + + fn roadmap_to_svg(&self, markup: &str) -> Result { + 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 { + 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 { + 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, +) -> Result { + // 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) -> 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, 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!"); -} -- cgit v1.2.1