kroki_rs/diagrams/providers/
excalidraw.rs1use crate::diagrams::{DiagramError, DiagramProvider, DiagramResult};
2use async_trait::async_trait;
3use std::io::Write;
4use std::process::Stdio;
5use tempfile::NamedTempFile;
6use tokio::process::Command;
7
8crate::diagrams::define_provider!(ExcalidrawProvider);
9
10#[async_trait]
11impl DiagramProvider for ExcalidrawProvider {
12 fn validate(&self, source: &str) -> DiagramResult<()> {
13 if source.trim().is_empty() {
14 return Err(DiagramError::ValidationFailed(
15 "Diagram source is empty".into(),
16 ));
17 }
18 Ok(())
19 }
20
21 async fn generate(&self, source: &str, format: &str) -> DiagramResult<Vec<u8>> {
22 if format != "svg" {
23 return Err(DiagramError::UnsupportedFormat {
24 format: format.into(),
25 provider: "Excalidraw".into(),
26 });
27 }
28
29 let mut temp_input = NamedTempFile::new().map_err(|e| {
30 DiagramError::Internal(format!("Failed to create temporary input file: {}", e))
31 })?;
32 temp_input.write_all(source.as_bytes()).map_err(|e| {
33 DiagramError::Internal(format!("Failed to write source to temp file: {}", e))
34 })?;
35 let input_path = temp_input.path().to_str().unwrap().to_string();
36
37 let mut cmd = Command::new(&self.bin_path);
38 cmd.arg(&input_path)
39 .stdout(Stdio::piped())
40 .stderr(Stdio::piped());
41
42 let output = crate::diagrams::run_process_with_timeout(
43 "excalidraw-to-svg",
44 cmd,
45 None,
46 self.timeout_ms,
47 source.len(),
48 )
49 .await?;
50
51 if output.status.success() {
52 if output.stdout.is_empty() {
53 return Err(DiagramError::ProcessFailed(
54 "Excalidraw conversion succeeded but output is empty".into(),
55 ));
56 }
57 Ok(output.stdout)
58 } else {
59 let stderr = String::from_utf8_lossy(&output.stderr);
60 Err(DiagramError::ProcessFailed(format!(
61 "Excalidraw conversion failed: {}",
62 stderr
63 )))
64 }
65 }
66}