Skip to main content

dioxus_mdx/components/
mermaid.rs

1//! Mermaid diagram rendering component.
2//!
3//! Renders fenced `mermaid` code blocks as actual diagrams by loading
4//! mermaid.js from CDN on demand. Falls back to displaying the raw
5//! mermaid source text if JavaScript is unavailable.
6
7use std::sync::atomic::{AtomicU64, Ordering};
8
9use dioxus::prelude::*;
10
11/// Monotonic counter for unique mermaid element IDs.
12static MERMAID_ID: AtomicU64 = AtomicU64::new(0);
13
14/// Props for MermaidDiagram component.
15#[derive(Props, Clone, PartialEq)]
16pub struct MermaidDiagramProps {
17    /// Raw mermaid diagram source code.
18    pub code: String,
19}
20
21/// Renders a mermaid diagram.
22///
23/// The component outputs a `<pre class="mermaid">` element that mermaid.js
24/// recognises. A `use_effect` hook (WASM-only) lazily loads mermaid from
25/// jsDelivr, detects the current DaisyUI theme, and calls `mermaid.run()`
26/// targeting only this element. A `MutationObserver` on `<html data-theme>`
27/// re-renders the diagram when the user toggles themes.
28#[component]
29pub fn MermaidDiagram(props: MermaidDiagramProps) -> Element {
30    let id = use_signal(|| format!("mermaid-{}", MERMAID_ID.fetch_add(1, Ordering::Relaxed)));
31
32    #[allow(unused_variables)]
33    let code = props.code.clone();
34
35    #[cfg(target_arch = "wasm32")]
36    {
37        let element_id = id().clone();
38        use_effect(move || {
39            let element_id = element_id.clone();
40            let code = code.clone();
41            spawn(async move {
42                let code_json = serde_json::to_string(&code).unwrap_or_default();
43                let js = format!(
44                    r#"
45                    (async function() {{
46                        const elId = {element_id_json};
47                        const code = {code_json};
48
49                        // Load mermaid.js from CDN once
50                        if (!window.mermaid) {{
51                            await new Promise((resolve, reject) => {{
52                                if (document.querySelector('script[data-mermaid-cdn]')) {{
53                                    // Another instance is already loading — wait for it
54                                    const check = setInterval(() => {{
55                                        if (window.mermaid) {{ clearInterval(check); resolve(); }}
56                                    }}, 50);
57                                    return;
58                                }}
59                                const s = document.createElement('script');
60                                s.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
61                                s.setAttribute('data-mermaid-cdn', '1');
62                                s.onload = () => {{
63                                    window.mermaid.initialize({{ startOnLoad: false }});
64                                    resolve();
65                                }};
66                                s.onerror = reject;
67                                document.head.appendChild(s);
68                            }});
69                        }}
70
71                        // Detect DaisyUI theme → mermaid theme
72                        function mermaidTheme() {{
73                            const dt = document.documentElement.getAttribute('data-theme') || '';
74                            return (dt === 'light') ? 'default' : 'dark';
75                        }}
76
77                        // Render helper
78                        async function render() {{
79                            const el = document.getElementById(elId);
80                            if (!el) return;
81                            // Reset element so mermaid re-parses it
82                            el.removeAttribute('data-processed');
83                            el.innerHTML = code;
84                            try {{
85                                await window.mermaid.run({{
86                                    nodes: [el],
87                                    suppressErrors: true,
88                                }});
89                            }} catch (_) {{}}
90                        }}
91
92                        // Initial render with the right theme
93                        window.mermaid.initialize({{ startOnLoad: false, theme: mermaidTheme() }});
94                        await render();
95
96                        // Re-render on theme change
97                        const observer = new MutationObserver(async () => {{
98                            window.mermaid.initialize({{ startOnLoad: false, theme: mermaidTheme() }});
99                            await render();
100                        }});
101                        observer.observe(document.documentElement, {{
102                            attributes: true,
103                            attributeFilter: ['data-theme'],
104                        }});
105                    }})();
106                    "#,
107                    element_id_json = serde_json::to_string(&element_id).unwrap_or_default(),
108                    code_json = code_json,
109                );
110                let _ = document::eval(&js);
111            });
112        });
113    }
114
115    rsx! {
116        div { class: "my-6 flex justify-center",
117            pre {
118                class: "mermaid",
119                id: "{id()}",
120                "{props.code}"
121            }
122        }
123    }
124}