1use crate::SubplotError;
2
3use std::env;
4use std::ffi::OsString;
5use std::io::prelude::*;
6use std::path::PathBuf;
7use std::process::{Command, Stdio};
8use std::sync::Mutex;
9
10#[cfg(feature = "unstable-cli")]
11use clap::Parser;
12use lazy_static::lazy_static;
13#[allow(missing_docs)]
16#[derive(Debug)]
17#[cfg_attr(feature = "unstable-cli", derive(Parser))]
18pub struct MarkupOpts {
19 #[cfg_attr(
20 feature = "unstable-cli",
21 clap(
22 long = "dot",
23 help = "Path to the `dot` binary.",
24 name = "DOTPATH",
25 env = "SUBPLOT_DOT_PATH"
26 )
27 )]
28 dot_path: Option<PathBuf>,
29 #[cfg_attr(
30 feature = "unstable-cli",
31 clap(
32 long = "plantuml-jar",
33 help = "Path to the `plantuml.jar` file.",
34 name = "PLANTUMLJARPATH",
35 env = "SUBPLOT_PLANTUML_JAR_PATH"
36 )
37 )]
38 plantuml_jar_path: Option<PathBuf>,
39 #[cfg_attr(
40 feature = "unstable-cli",
41 clap(
42 long = "java",
43 help = "Path to Java executable (note, effectively overrides JAVA_HOME if set to an absolute path)",
44 name = "JAVA_PATH",
45 env = "SUBPLOT_JAVA_PATH"
46 )
47 )]
48 java_path: Option<PathBuf>,
49}
50
51impl MarkupOpts {
52 pub fn handle(&self) {
54 if let Some(dotpath) = &self.dot_path {
55 DOT_PATH.lock().unwrap().clone_from(dotpath);
56 }
57 if let Some(plantuml_path) = &self.plantuml_jar_path {
58 PLANTUML_JAR_PATH.lock().unwrap().clone_from(plantuml_path);
59 }
60 if let Some(java_path) = &self.java_path {
61 JAVA_PATH.lock().unwrap().clone_from(java_path);
62 }
63 }
64}
65
66lazy_static! {
67 static ref DOT_PATH: Mutex<PathBuf> = Mutex::new(env!("BUILTIN_DOT_PATH").into());
68 static ref PLANTUML_JAR_PATH: Mutex<PathBuf> =
69 Mutex::new(env!("BUILTIN_PLANTUML_JAR_PATH").into());
70 static ref JAVA_PATH: Mutex<PathBuf> = Mutex::new(env!("BUILTIN_JAVA_PATH").into());
71}
72
73pub struct Svg {
79 data: Vec<u8>,
80}
81
82impl Svg {
83 fn new(data: Vec<u8>) -> Self {
84 Self { data }
85 }
86
87 pub fn data(&self) -> &[u8] {
89 &self.data
90 }
91
92 #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize {
95 self.data.len()
96 }
97}
98
99pub trait DiagramMarkup {
110 fn as_svg(&self) -> Result<Svg, SubplotError>;
112}
113
114pub struct PikchrMarkup {
123 markup: String,
124 class: Option<String>,
125}
126
127impl PikchrMarkup {
128 pub fn new(markup: &str, class: Option<&str>) -> PikchrMarkup {
130 PikchrMarkup {
131 markup: markup.to_owned(),
132 class: class.map(str::to_owned),
133 }
134 }
135}
136
137impl DiagramMarkup for PikchrMarkup {
138 fn as_svg(&self) -> Result<Svg, SubplotError> {
139 let mut flags = pikchr::PikchrFlags::default();
140 flags.generate_plain_errors();
141 let image = pikchr::Pikchr::render(&self.markup, self.class.as_deref(), flags)
142 .map_err(SubplotError::PikchrRenderError)?;
143 Ok(Svg::new(image.as_bytes().to_vec()))
144 }
145}
146
147pub struct DotMarkup {
156 markup: String,
157}
158
159impl DotMarkup {
160 pub fn new(markup: &str) -> DotMarkup {
162 DotMarkup {
163 markup: markup.to_owned(),
164 }
165 }
166}
167
168impl DiagramMarkup for DotMarkup {
169 fn as_svg(&self) -> Result<Svg, SubplotError> {
170 let path = DOT_PATH.lock().unwrap().clone();
171 let mut child = Command::new(&path)
172 .arg("-Tsvg")
173 .stdin(Stdio::piped())
174 .stdout(Stdio::piped())
175 .stderr(Stdio::piped())
176 .spawn()
177 .map_err(|err| SubplotError::Spawn(path.clone(), err))?;
178 if let Some(stdin) = child.stdin.as_mut() {
179 stdin
180 .write_all(self.markup.as_bytes())
181 .map_err(SubplotError::WriteToChild)?;
182 let output = child
183 .wait_with_output()
184 .map_err(SubplotError::WaitForChild)?;
185 if output.status.success() {
186 Ok(Svg::new(output.stdout))
187 } else {
188 Err(SubplotError::child_failed("dot", &output))
189 }
190 } else {
191 Err(SubplotError::ChildNoStdin)
192 }
193 }
194}
195
196pub struct PlantumlMarkup {
205 markup: String,
206}
207
208impl PlantumlMarkup {
209 pub fn new(markup: &str) -> PlantumlMarkup {
211 PlantumlMarkup {
212 markup: markup.to_owned(),
213 }
214 }
215
216 fn build_java_path() -> Option<OsString> {
220 let java_home = env::var_os("JAVA_HOME")?;
221 let cur_path = env::var_os("PATH")?;
222 let cur_path: Vec<_> = env::split_paths(&cur_path).collect();
223 let java_home = PathBuf::from(java_home);
224 let java_bin = java_home.join("bin");
225 if cur_path.iter().any(|v| v.as_os_str() == java_bin) {
226 return None;
228 }
229 env::join_paths(Some(java_bin).iter().chain(cur_path.iter())).ok()
230 }
231}
232
233impl DiagramMarkup for PlantumlMarkup {
234 fn as_svg(&self) -> Result<Svg, SubplotError> {
235 let path = JAVA_PATH.lock().unwrap().clone();
236 let mut cmd = Command::new(&path);
237 cmd.arg("-Djava.awt.headless=true")
238 .arg("-jar")
239 .arg(PLANTUML_JAR_PATH.lock().unwrap().clone())
240 .arg("--")
241 .arg("-pipe")
242 .arg("-tsvg")
243 .arg("-v")
244 .arg("-graphvizdot")
245 .arg(DOT_PATH.lock().unwrap().clone())
246 .stdin(Stdio::piped())
247 .stdout(Stdio::piped())
248 .stderr(Stdio::piped());
249 if let Some(path) = Self::build_java_path() {
250 cmd.env("PATH", path);
251 }
252 let mut child = cmd
253 .spawn()
254 .map_err(|err| SubplotError::Spawn(path.clone(), err))?;
255 if let Some(stdin) = child.stdin.as_mut() {
256 stdin
257 .write_all(self.markup.as_bytes())
258 .map_err(SubplotError::WriteToChild)?;
259 let output = child
260 .wait_with_output()
261 .map_err(SubplotError::WaitForChild)?;
262 if output.status.success() {
263 Ok(Svg::new(output.stdout))
264 } else {
265 Err(SubplotError::child_failed("plantuml", &output))
266 }
267 } else {
268 Err(SubplotError::ChildNoStdin)
269 }
270 }
271}