feat: upgrade mermaid-rs-renderer to v0.2.0 with edge label support

Adds font-family sanitization to fix nested double quotes that break
usvg XML parsing. Edge labels (-->|Yes|) now render correctly as
interactive DrawingElements.
This commit is contained in:
David Ibia
2026-02-10 17:05:55 +01:00
parent 9e9d33eb84
commit 64b4f667fb
2 changed files with 81 additions and 4 deletions

View File

@@ -33,7 +33,7 @@ tokio-tungstenite = "0.24"
futures-util = "0.3" futures-util = "0.3"
# Mermaid diagram rendering # Mermaid diagram rendering
mermaid-rs-renderer = { version = "0.1.2", default-features = false } mermaid-rs-renderer = { git = "https://github.com/1jehuang/mermaid-rs-renderer", tag = "v0.2.0", default-features = false }
# Logging # Logging
tracing = "0.1" tracing = "0.1"

View File

@@ -1,9 +1,67 @@
use std::panic;
use anyhow::Result; use anyhow::Result;
use mermaid_rs_renderer::RenderOptions;
pub fn render_mermaid_to_svg(mermaid_source: &str) -> Result<String> { pub fn render_mermaid_to_svg(mermaid_source: &str) -> Result<String> {
let svg = mermaid_rs_renderer::render(mermaid_source) let source = mermaid_source.to_string();
.map_err(|e| anyhow::anyhow!("Mermaid render failed: {}", e))?;
Ok(svg) let mut options = RenderOptions::modern();
options.theme.font_family =
"Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif".to_string();
let result = panic::catch_unwind(|| mermaid_rs_renderer::render_with_options(&source, options));
match result {
Ok(Ok(svg)) => Ok(sanitize_svg_font_families(&svg)),
Ok(Err(e)) => Err(anyhow::anyhow!("Mermaid render failed: {}", e)),
Err(_) => Err(anyhow::anyhow!(
"Mermaid renderer panicked (unsupported syntax)"
)),
}
}
/// Strips nested double quotes inside `font-family` XML attributes.
///
/// mermaid-rs-renderer v0.2.0 emits `font-family="..., "Segoe UI", ..."`
/// which is invalid XML — usvg rejects it. This rewrites those attributes
/// so the inner quotes are removed.
fn sanitize_svg_font_families(svg: &str) -> String {
let mut result = String::with_capacity(svg.len());
let mut chars = svg.chars().peekable();
while let Some(ch) = chars.next() {
result.push(ch);
if ch == 'f' && result.ends_with("font-family=\"") {
let mut value = String::new();
loop {
match chars.next() {
Some('"') => {
if let Some(&next) = chars.peek() {
if next == ' ' || next == '/' || next == '>' || next == '\n' {
result.push_str(&value);
result.push('"');
break;
} else {
continue;
}
} else {
result.push_str(&value);
result.push('"');
break;
}
}
Some(c) => value.push(c),
None => {
result.push_str(&value);
break;
}
}
}
}
}
result
} }
#[cfg(test)] #[cfg(test)]
@@ -21,4 +79,23 @@ mod tests {
let svg = render_mermaid_to_svg("sequenceDiagram\n Alice->>Bob: Hello").unwrap(); let svg = render_mermaid_to_svg("sequenceDiagram\n Alice->>Bob: Hello").unwrap();
assert!(svg.contains("<svg")); assert!(svg.contains("<svg"));
} }
#[test]
fn renders_edge_labels() {
let svg = render_mermaid_to_svg(
"flowchart LR\n A{Decision} -->|Yes| B[OK]\n A -->|No| C[Cancel]",
)
.unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("Yes"));
assert!(svg.contains("No"));
}
#[test]
fn sanitized_svg_is_valid_xml() {
let svg = render_mermaid_to_svg("flowchart LR\n A-->B").unwrap();
let mut options = usvg::Options::default();
options.fontdb_mut().load_system_fonts();
usvg::Tree::from_str(&svg, &options).expect("sanitized SVG should parse with usvg");
}
} }