#![cfg_attr(not(feature = "webview-backend"), allow(dead_code))]
use regex::Regex;
fn preprocess_mermaid_source(source: &str) -> String {
let mut result = String::with_capacity(source.len());
for line in source.lines() {
let processed = line
.replace("<br/>", " ")
.replace("<br>", " ")
.replace("<br />", " ")
.replace("<-->", "---")
.replace("x--x", "---")
.replace("o--o", "---");
result.push_str(&processed);
result.push('\n');
}
result
}
pub fn render_mermaid_to_svg(source: &str) -> Result<String, String> {
let _stderr_guard = suppress_stderr();
let preprocessed = preprocess_mermaid_source(source);
let preprocessed_clone = preprocessed.clone();
if let Ok(Ok(svg)) =
std::panic::catch_unwind(|| mermaid_rs_renderer::render(&preprocessed_clone))
{
return Ok(svg);
}
let source = source.to_string();
match std::panic::catch_unwind(|| mermaid_rs_renderer::render(&source)) {
Ok(Ok(svg)) => Ok(svg),
Ok(Err(e)) => Err(format!("{}", e)),
Err(_) => Err("mermaid renderer panicked (unsupported diagram syntax)".to_string()),
}
}
struct StderrGuard {
#[cfg(unix)]
saved_fd: Option<std::os::unix::io::RawFd>,
}
impl Drop for StderrGuard {
fn drop(&mut self) {
#[cfg(unix)]
if let Some(saved) = self.saved_fd {
unsafe {
libc::dup2(saved, 2);
libc::close(saved);
}
}
}
}
fn suppress_stderr() -> StderrGuard {
#[cfg(unix)]
{
unsafe {
let saved = libc::dup(2);
if saved >= 0 {
let devnull = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY);
if devnull >= 0 {
libc::dup2(devnull, 2);
libc::close(devnull);
return StderrGuard {
saved_fd: Some(saved),
};
}
libc::close(saved);
}
}
StderrGuard { saved_fd: None }
}
#[cfg(not(unix))]
StderrGuard {}
}
pub fn process_mermaid_blocks(html: &str) -> String {
use std::sync::OnceLock;
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| {
Regex::new(r#"<pre><code class="language-mermaid">([\s\S]*?)</code></pre>"#).unwrap()
});
re.replace_all(html, |caps: ®ex::Captures| {
let source = html_decode(&caps[1]);
match render_mermaid_to_svg(&source) {
Ok(svg) => format!(r#"<div class="mermaid-diagram">{}</div>"#, svg),
Err(_) => format!(r#"<pre class="mermaid">{}</pre>"#, html_encode(&source)),
}
})
.to_string()
}
#[cfg(feature = "egui-backend")]
pub fn preprocess_mermaid_for_egui(markdown: &str) -> String {
use std::sync::OnceLock;
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| Regex::new(r"```mermaid\n([\s\S]*?)```").unwrap());
re.replace_all(markdown, |caps: ®ex::Captures| {
let source = &caps[1];
match render_mermaid_to_svg(source) {
Ok(svg) => match svg_to_png_base64(&svg) {
Ok(b64) => format!("", b64),
Err(_) => format!(
"> **◇ Mermaid Diagram** *(SVG to PNG conversion failed)*\n\n```\n{}```",
source
),
},
Err(_) => format!(
"> **◇ Mermaid Diagram** *(unsupported by native renderer)*\n\n```\n{}```",
source
),
}
})
.to_string()
}
#[cfg(feature = "egui-backend")]
fn svg_to_png_base64(svg: &str) -> Result<String, Box<dyn std::error::Error>> {
use base64::Engine;
use std::sync::{Arc, OnceLock};
const MAX_TEXTURE_SIZE: u32 = 8192;
static FONTDB: OnceLock<Arc<usvg::fontdb::Database>> = OnceLock::new();
let fontdb = FONTDB.get_or_init(|| {
let mut db = usvg::fontdb::Database::new();
db.load_system_fonts();
Arc::new(db)
});
let options = usvg::Options {
fontdb: Arc::clone(fontdb),
..Default::default()
};
let tree = usvg::Tree::from_str(svg, &options)?;
let size = tree.size();
let svg_w = size.width();
let svg_h = size.height();
if svg_w <= 0.0 || svg_h <= 0.0 {
return Err("SVG has zero dimensions".into());
}
let scale = {
let scale_w = MAX_TEXTURE_SIZE as f32 / svg_w;
let scale_h = MAX_TEXTURE_SIZE as f32 / svg_h;
scale_w.min(scale_h).min(1.0) };
let width = (svg_w * scale) as u32;
let height = (svg_h * scale) as u32;
if width == 0 || height == 0 {
return Err("SVG dimensions too small after scaling".into());
}
let mut pixmap = tiny_skia::Pixmap::new(width, height).ok_or("Failed to create pixmap")?;
let transform = tiny_skia::Transform::from_scale(scale, scale);
resvg::render(&tree, transform, &mut pixmap.as_mut());
let png_data = pixmap.encode_png()?;
Ok(base64::engine::general_purpose::STANDARD.encode(&png_data))
}
fn html_decode(s: &str) -> String {
s.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
}
fn html_encode(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn html_decode_all_entities() {
assert_eq!(html_decode("&<>"'"), "&<>\"'");
}
#[test]
fn html_decode_no_entities() {
assert_eq!(html_decode("plain text"), "plain text");
}
#[test]
fn html_decode_mixed() {
assert_eq!(html_decode("A & B < C"), "A & B < C");
}
#[test]
fn html_encode_special_chars() {
assert_eq!(html_encode("A & B < C > D"), "A & B < C > D");
}
#[test]
fn html_encode_no_special_chars() {
assert_eq!(html_encode("plain text"), "plain text");
}
#[test]
fn html_encode_decode_roundtrip() {
let original = "graph LR; A-->B";
let encoded = html_encode(original);
let decoded = html_decode(&encoded);
assert_eq!(decoded, original);
}
#[test]
fn preprocess_removes_html_breaks() {
let source = "graph LR\n A[Line 1<br/>Line 2]-->B";
let result = preprocess_mermaid_source(source);
assert!(!result.contains("<br/>"));
assert!(result.contains("Line 1 Line 2"));
}
#[test]
fn preprocess_converts_bidirectional_arrows() {
let source = "graph LR\n A<-->B";
let result = preprocess_mermaid_source(source);
assert!(!result.contains("<-->"));
assert!(result.contains("A---B"));
}
#[test]
fn preprocess_leaves_valid_syntax_unchanged() {
let source = "graph LR\n A-->B\n B-->C";
let result = preprocess_mermaid_source(source);
assert!(result.contains("A-->B"));
assert!(result.contains("B-->C"));
}
#[test]
fn render_mermaid_valid_diagram() {
let source = "graph LR\n A-->B";
let result = render_mermaid_to_svg(source);
match result {
Ok(svg) => {
assert!(
svg.contains("<svg") || svg.contains("<SVG"),
"Expected SVG output, got: {}",
svg
);
}
Err(e) => {
assert!(!e.is_empty(), "Error message should not be empty");
}
}
}
#[test]
fn render_mermaid_empty_input() {
let result = render_mermaid_to_svg("");
assert!(result.is_err() || result.is_ok());
}
#[test]
fn render_mermaid_invalid_syntax() {
let result = render_mermaid_to_svg("this is not valid mermaid syntax at all %%% !@#");
match result {
Ok(_) => {} Err(e) => assert!(!e.is_empty()),
}
}
#[test]
fn render_mermaid_panic_safety() {
let result = render_mermaid_to_svg("\0\0\0");
let _ = result;
}
#[test]
fn process_mermaid_blocks_no_mermaid() {
let html = "<p>Hello</p><pre><code class=\"language-rust\">fn main() {}</code></pre>";
let result = process_mermaid_blocks(html);
assert_eq!(result, html);
}
#[test]
fn process_mermaid_blocks_replaces_mermaid_code() {
let html = r#"<p>Before</p><pre><code class="language-mermaid">graph LR
A-->B</code></pre><p>After</p>"#;
let result = process_mermaid_blocks(html);
assert!(
!result.contains(r#"class="language-mermaid""#),
"Mermaid code block should be replaced, got: {}",
result
);
assert!(
result.contains("mermaid-diagram")
|| result.contains("mermaid-error")
|| result.contains("mermaid-fallback"),
"Should contain diagram or fallback div, got: {}",
result
);
assert!(result.contains("<p>Before</p>"));
assert!(result.contains("<p>After</p>"));
}
#[test]
fn process_mermaid_blocks_preserves_non_mermaid_content() {
let html = "<h1>Title</h1><p>Content</p>";
let result = process_mermaid_blocks(html);
assert_eq!(result, html);
}
#[test]
fn process_mermaid_blocks_error_contains_source() {
let html = r#"<pre><code class="language-mermaid">not valid %%% !@#</code></pre>"#;
let result = process_mermaid_blocks(html);
if result.contains("mermaid-fallback") {
assert!(result.contains("Mermaid Diagram"));
} else if result.contains("mermaid-error") {
assert!(result.contains("Mermaid error:"));
}
}
#[cfg(feature = "egui-backend")]
mod egui_tests {
use super::super::*;
#[test]
fn preprocess_mermaid_for_egui_no_mermaid() {
let md = "# Title\n\nSome text\n\n```rust\nfn main() {}\n```";
let result = preprocess_mermaid_for_egui(md);
assert_eq!(result, md);
}
#[test]
fn preprocess_mermaid_for_egui_replaces_block() {
let md = "Before\n\n```mermaid\ngraph LR\n A-->B\n```\n\nAfter";
let result = preprocess_mermaid_for_egui(md);
assert!(
!result.contains("```mermaid"),
"Mermaid block should be replaced, got: {}",
result
);
assert!(result.contains("Before"));
assert!(result.contains("After"));
}
#[test]
fn preprocess_mermaid_for_egui_error_shows_source() {
let md = "```mermaid\nnot valid mermaid\n```";
let result = preprocess_mermaid_for_egui(md);
if result.contains("error") || result.contains("Error") {
assert!(result.contains("not valid mermaid"));
}
}
}
}