use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::ops::Range;
use std::panic::{AssertUnwindSafe, catch_unwind};
use mermaid_rs_renderer::{RenderOptions, Theme, render_with_options};
use crate::image_cache::{ImageCache, LoadedImage, decode};
pub struct MermaidBlock {
pub block: Range<usize>,
pub content: Range<usize>,
pub anchor_line: usize,
}
pub fn key_for(source: &str) -> String {
let mut h = DefaultHasher::new();
source.hash(&mut h);
format!("mermaid:{:016x}", h.finish())
}
fn render_to_image(source: &str) -> Result<LoadedImage, Option<String>> {
let opts = RenderOptions {
theme: Theme::dark(),
..Default::default()
};
match catch_unwind(AssertUnwindSafe(|| render_with_options(source, opts))) {
Ok(Ok(svg)) => decode(svg.as_bytes()).ok_or(None),
Ok(Err(e)) => Err(Some(e.to_string())),
Err(_) => Err(None), }
}
pub fn spawn_mermaid_renders(
sources: &[(String, String)],
cache: &ImageCache,
notify: impl Fn() + Send + Clone + 'static,
) {
for (key, source) in sources {
if cache.contains(key) {
continue; }
cache.mark_loading(key);
let cache = cache.clone();
let notify = notify.clone();
let key = key.clone();
let source = source.clone();
std::thread::spawn(move || {
cache.store_render(&key, render_to_image(&source));
notify();
});
}
}
pub fn render_mermaid_blocking(sources: &[(String, String)], cache: &ImageCache) {
for (key, source) in sources {
if cache.contains(key) {
continue;
}
cache.store_render(key, render_to_image(source));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image_cache::ImageState;
#[test]
fn renders_diagram_into_cache() {
let src = "flowchart TD\n A[Start] --> B[End]";
let key = key_for(src);
let cache = ImageCache::new();
render_mermaid_blocking(&[(key.clone(), src.to_string())], &cache);
assert!(
matches!(cache.get(&key), Some(ImageState::Loaded(_))),
"a valid flowchart should render to SVG and decode"
);
}
#[test]
fn bad_source_fails_without_panicking() {
let src = "%%%% definitely not mermaid %%%%";
let key = key_for(src);
let cache = ImageCache::new();
render_mermaid_blocking(&[(key.clone(), src.to_string())], &cache);
assert!(cache.get(&key).is_some());
}
#[test]
fn parse_error_surfaces_reason() {
let src = "classDiagram\n class A {\n <<interface>>\n }";
let key = key_for(src);
let cache = ImageCache::new();
render_mermaid_blocking(&[(key.clone(), src.to_string())], &cache);
match cache.get(&key) {
Some(ImageState::Failed(Some(msg))) => {
assert!(!msg.is_empty(), "error message should be non-empty");
}
_ => panic!("expected Failed(Some(reason))"),
}
}
#[test]
fn key_is_stable_per_source() {
assert_eq!(key_for("graph LR; A-->B"), key_for("graph LR; A-->B"));
assert_ne!(key_for("graph LR; A-->B"), key_for("graph LR; A-->C"));
}
}