1use anyhow::Result;
2use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
3
4fn escape_html(s: &str) -> String {
6 let mut output = String::new();
7 for c in s.chars() {
8 match c {
9 '<' => output.push_str("<"),
10 '>' => output.push_str(">"),
11 '"' => output.push_str("""),
12 '&' => output.push_str("&"),
13 '\'' => output.push_str("'"),
14 _ => output.push(c),
15 }
16 }
17 output
18}
19
20pub fn add_excalidraw(content: &str) -> Result<String> {
22 let mut mermaid_blocks = Vec::new();
23
24 let parser = Parser::new(content);
25 let mut in_mermaid_block = false;
26 let mut current_mermaid = String::new();
27
28 for (event, range) in parser.into_offset_iter() {
29 match event {
30 Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => {
31 if lang.as_ref() == "mermaid" {
32 in_mermaid_block = true;
33 current_mermaid.clear();
34 }
35 }
36 Event::End(TagEnd::CodeBlock) => {
37 if in_mermaid_block {
38 in_mermaid_block = false;
39
40 let diagram_id = format!("excalidraw-{}", mermaid_blocks.len());
42
43 let escaped_mermaid = escape_html(¤t_mermaid);
45
46 let escaped_for_attribute = escaped_mermaid.replace('\n', " ");
48
49 let excalidraw_html = format!(
51 r#"<div class="excalidraw-wrapper" id="{id}">
52 <div class="excalidraw-container" data-mermaid="{mermaid}">
53 <div class="excalidraw-loading">Loading Excalidraw diagram...</div>
54 </div>
55 <details class="excalidraw-source">
56 <summary>View Mermaid Source</summary>
57 <pre><code class="language-mermaid">{source}</code></pre>
58 </details>
59</div>"#,
60 id = diagram_id,
61 mermaid = escaped_for_attribute,
62 source = escaped_mermaid
63 );
64
65 mermaid_blocks.push((range.start..range.end, excalidraw_html));
66 current_mermaid.clear();
67 }
68 }
69 Event::Text(text) => {
70 if in_mermaid_block {
71 current_mermaid.push_str(&text);
72 }
73 }
74 Event::Code(text) => {
75 if in_mermaid_block {
76 current_mermaid.push_str(&text);
77 }
78 }
79 Event::SoftBreak => {
80 if in_mermaid_block {
81 current_mermaid.push('\n');
82 }
83 }
84 Event::HardBreak => {
85 if in_mermaid_block {
86 current_mermaid.push('\n');
87 }
88 }
89 Event::Html(html) => {
90 if in_mermaid_block {
91 current_mermaid.push_str(&html);
92 }
93 }
94 _ => {}
95 }
96 }
97
98 let mut result = String::with_capacity(content.len());
100 let mut last_end = 0;
101 for (span, block) in mermaid_blocks.iter() {
102 result.push_str(&content[last_end..span.start]);
103 result.push_str(block);
104 last_end = span.end;
105 }
106 result.push_str(&content[last_end..]);
107
108 Ok(result)
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_escape_html() {
117 assert_eq!(
118 escape_html("<script>alert('XSS')</script>"),
119 "<script>alert('XSS')</script>"
120 );
121 }
122
123 #[test]
124 fn test_add_excalidraw_simple() {
125 let content = r#"# Test
126
127```mermaid
128graph TD
129 A-->B
130```
131
132Some text"#;
133
134 let result = add_excalidraw(content).unwrap();
135 assert!(result.contains("excalidraw-wrapper"));
136 assert!(result.contains("data-mermaid"));
137 assert!(result.contains("graph TD"));
138 }
139
140 #[test]
141 fn test_add_excalidraw_with_special_chars() {
142 let content = r#"```mermaid
143graph TD
144 A["<Component>"]-->B
145```"#;
146
147 let result = add_excalidraw(content).unwrap();
148 assert!(result.contains("<Component>"));
149 }
150
151 #[test]
152 fn test_multiple_mermaid_blocks() {
153 let content = r#"# Test
154
155```mermaid
156graph TD
157 A-->B
158```
159
160Some text
161
162```mermaid
163sequenceDiagram
164 Alice->>Bob: Hello
165```"#;
166
167 let result = add_excalidraw(content).unwrap();
168 assert!(result.contains("excalidraw-0"));
169 assert!(result.contains("excalidraw-1"));
170 }
171
172 #[test]
173 fn test_non_mermaid_blocks_unchanged() {
174 let content = r#"```rust
175fn main() {
176 println!("Hello");
177}
178```"#;
179
180 let result = add_excalidraw(content).unwrap();
181 assert!(result.contains("```rust"));
182 assert!(!result.contains("excalidraw"));
183 }
184}