diff options
author | Daniel Silverstone <dsilvers+gitlab@digital-scurf.org> | 2022-04-26 11:07:50 +0000 |
---|---|---|
committer | Daniel Silverstone <dsilvers+gitlab@digital-scurf.org> | 2022-04-26 11:07:50 +0000 |
commit | 12059fcb1ce8237e5587773043cd442e036531c2 (patch) | |
tree | 8940d4eca8c663e72cf051b22b9232eb6a84b056 /src/diagrams.rs | |
parent | 924e05bc3d109340eeee7e06c6badeae12d45c10 (diff) | |
parent | 3422999e5a4fc6b08637d00cae11f523c747f036 (diff) | |
download | subplot-12059fcb1ce8237e5587773043cd442e036531c2.tar.gz |
Merge branch 'liw/diagram' into 'main'
refactor: use "diagram" instead of "graph"
Closes #255
See merge request subplot/subplot!273
Diffstat (limited to 'src/diagrams.rs')
-rw-r--r-- | src/diagrams.rs | 224 |
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) + } + } +} |