summaryrefslogtreecommitdiff
path: root/src/diagrams.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/diagrams.rs')
-rw-r--r--src/diagrams.rs224
1 files changed, 224 insertions, 0 deletions
diff --git a/src/diagrams.rs b/src/diagrams.rs
new file mode 100644
index 0000000..96f5d70
--- /dev/null
+++ b/src/diagrams.rs
@@ -0,0 +1,224 @@
+use crate::{Result, SubplotError};
+
+use std::env;
+use std::ffi::OsString;
+use std::io::prelude::*;
+use std::path::PathBuf;
+use std::process::{Command, Stdio};
+use std::sync::Mutex;
+
+use lazy_static::lazy_static;
+use structopt::StructOpt;
+
+/// Resources used to configure paths for dot, plantuml.jar, and friends
+
+#[allow(missing_docs)]
+#[derive(Debug, StructOpt)]
+pub struct MarkupOpts {
+ #[structopt(
+ long = "dot",
+ help = "Path to the `dot` binary.",
+ name = "DOTPATH",
+ env = "SUBPLOT_DOT_PATH"
+ )]
+ dot_path: Option<PathBuf>,
+ #[structopt(
+ long = "plantuml-jar",
+ help = "Path to the `plantuml.jar` file.",
+ name = "PLANTUMLJARPATH",
+ env = "SUBPLOT_PLANTUML_JAR_PATH"
+ )]
+ plantuml_jar_path: Option<PathBuf>,
+ #[structopt(
+ long = "java",
+ help = "Path to Java executable (note, effectively overrides JAVA_HOME if set to an absolute path)",
+ name = "JAVA_PATH",
+ env = "SUBPLOT_JAVA_PATH"
+ )]
+ java_path: Option<PathBuf>,
+}
+
+impl MarkupOpts {
+ /// Handle CLI arguments and environment variables for markup binaries
+ pub fn handle(&self) {
+ if let Some(dotpath) = &self.dot_path {
+ *DOT_PATH.lock().unwrap() = dotpath.clone();
+ }
+ if let Some(plantuml_path) = &self.plantuml_jar_path {
+ *PLANTUML_JAR_PATH.lock().unwrap() = plantuml_path.clone();
+ }
+ if let Some(java_path) = &self.java_path {
+ *JAVA_PATH.lock().unwrap() = java_path.clone();
+ }
+ }
+}
+
+lazy_static! {
+ static ref DOT_PATH: Mutex<PathBuf> = Mutex::new(env!("BUILTIN_DOT_PATH").into());
+ static ref PLANTUML_JAR_PATH: Mutex<PathBuf> =
+ Mutex::new(env!("BUILTIN_PLANTUML_JAR_PATH").into());
+ static ref JAVA_PATH: Mutex<PathBuf> = Mutex::new(env!("BUILTIN_JAVA_PATH").into());
+}
+
+/// A code block with markup for a diagram.
+///
+/// The code block will be converted to an SVG image using an external
+/// filter such as Graphviz dot or plantuml. SVG is the chosen image
+/// format as it's suitable for all kinds of output formats from
+/// typesetting.
+///
+/// This trait defines the interface for different kinds of markup
+/// conversions. There's only one function that needs to be defined
+/// for the trait.
+pub trait DiagramMarkup {
+ /// Convert the markup into an SVG.
+ fn as_svg(&self) -> Result<Vec<u8>>;
+}
+
+/// A code block with pikchr markup.
+///
+/// ~~~~
+/// use subplot::{DiagramMarkup, PikchrMarkup};
+/// let markup = r#"line; box "Hello," "World!"; arrow"#;
+/// let svg = PikchrMarkup::new(markup, None).as_svg().unwrap();
+/// assert!(svg.len() > 0);
+/// ~~~~
+pub struct PikchrMarkup {
+ markup: String,
+ class: Option<String>,
+}
+
+impl PikchrMarkup {
+ /// Create a new Pikchr Markup holder
+ pub fn new(markup: &str, class: Option<&str>) -> PikchrMarkup {
+ PikchrMarkup {
+ markup: markup.to_owned(),
+ class: class.map(str::to_owned),
+ }
+ }
+}
+
+impl DiagramMarkup for PikchrMarkup {
+ fn as_svg(&self) -> Result<Vec<u8>> {
+ let mut flags = pikchr::PikchrFlags::default();
+ flags.generate_plain_errors();
+ let image = pikchr::Pikchr::render(&self.markup, self.class.as_deref(), flags)
+ .map_err(SubplotError::PikchrRenderError)?;
+ Ok(image.as_bytes().to_vec())
+ }
+}
+
+/// A code block with Dot markup.
+///
+/// ~~~~
+/// use subplot::{DiagramMarkup, DotMarkup};
+/// let markup = r#"digraph "foo" { a -> b }"#;
+/// let svg = DotMarkup::new(&markup).as_svg().unwrap();
+/// assert!(svg.len() > 0);
+/// ~~~~
+pub struct DotMarkup {
+ markup: String,
+}
+
+impl DotMarkup {
+ /// Create a new DotMarkup.
+ pub fn new(markup: &str) -> DotMarkup {
+ DotMarkup {
+ markup: markup.to_owned(),
+ }
+ }
+}
+
+impl DiagramMarkup for DotMarkup {
+ fn as_svg(&self) -> Result<Vec<u8>> {
+ let mut child = Command::new(DOT_PATH.lock().unwrap().clone())
+ .arg("-Tsvg")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()?;
+ if let Some(stdin) = child.stdin.as_mut() {
+ stdin.write_all(self.markup.as_bytes())?;
+ let output = child.wait_with_output()?;
+ if output.status.success() {
+ Ok(output.stdout)
+ } else {
+ Err(SubplotError::child_failed("dot", &output))
+ }
+ } else {
+ Err(SubplotError::ChildNoStdin)
+ }
+ }
+}
+
+/// A code block with PlantUML markup.
+///
+/// ~~~~
+/// use subplot::{DiagramMarkup, PlantumlMarkup};
+/// let markup = "@startuml\nAlice -> Bob\n@enduml";
+/// let svg = PlantumlMarkup::new(&markup).as_svg().unwrap();
+/// assert!(svg.len() > 0);
+/// ~~~~
+pub struct PlantumlMarkup {
+ markup: String,
+}
+
+impl PlantumlMarkup {
+ /// Create a new PlantumlMarkup.
+ pub fn new(markup: &str) -> PlantumlMarkup {
+ PlantumlMarkup {
+ markup: markup.to_owned(),
+ }
+ }
+
+ // 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()
+ }
+
+ // Acquire path to JAR for pandoc
+}
+
+impl DiagramMarkup for PlantumlMarkup {
+ fn as_svg(&self) -> Result<Vec<u8>> {
+ let mut cmd = Command::new(JAVA_PATH.lock().unwrap().clone());
+ cmd.arg("-Djava.awt.headless=true")
+ .arg("-jar")
+ .arg(PLANTUML_JAR_PATH.lock().unwrap().clone())
+ .arg("--")
+ .arg("-pipe")
+ .arg("-tsvg")
+ .arg("-v")
+ .arg("-graphvizdot")
+ .arg(DOT_PATH.lock().unwrap().clone())
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped());
+ if let Some(path) = Self::build_java_path() {
+ cmd.env("PATH", path);
+ }
+ let mut child = cmd.spawn()?;
+ if let Some(stdin) = child.stdin.as_mut() {
+ stdin.write_all(self.markup.as_bytes())?;
+ let output = child.wait_with_output()?;
+ if output.status.success() {
+ Ok(output.stdout)
+ } else {
+ Err(SubplotError::child_failed("plantuml", &output))
+ }
+ } else {
+ Err(SubplotError::ChildNoStdin)
+ }
+ }
+}