1use mdbook_preprocessor::book::{Book, BookItem, Chapter};
6use mdbook_preprocessor::errors::Result;
7use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
8use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag, TagEnd};
9
10pub struct Mermaid;
11
12impl Preprocessor for Mermaid {
13 fn name(&self) -> &str {
14 "mermaid"
15 }
16
17 fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
18 let mut res = None;
19 book.for_each_mut(|item: &mut BookItem| {
20 if let Some(Err(_)) = res {
21 return;
22 }
23
24 if let BookItem::Chapter(ref mut chapter) = *item {
25 res = Some(Mermaid::add_mermaid(chapter).map(|md| {
26 chapter.content = md;
27 }));
28 }
29 });
30
31 res.unwrap_or(Ok(())).map(|_| book)
32 }
33
34 fn supports_renderer(&self, renderer: &str) -> Result<bool> {
35 Ok(renderer == "html")
36 }
37}
38
39fn escape_html(s: &str) -> String {
40 let mut output = String::new();
41 for c in s.chars() {
42 match c {
43 '<' => output.push_str("<"),
44 '>' => output.push_str(">"),
45 '"' => output.push_str("""),
46 '&' => output.push_str("&"),
47 _ => output.push(c),
48 }
49 }
50 output
51}
52
53fn add_mermaid(content: &str) -> Result<String> {
54 let mut mermaid_content = String::new();
55 let mut in_mermaid_block = false;
56
57 let mut opts = Options::empty();
58 opts.insert(Options::ENABLE_TABLES);
59 opts.insert(Options::ENABLE_FOOTNOTES);
60 opts.insert(Options::ENABLE_STRIKETHROUGH);
61 opts.insert(Options::ENABLE_TASKLISTS);
62
63 let mut code_span = 0..0;
64 let mut start_new_code_span = true;
65
66 let mut mermaid_blocks = vec![];
67
68 let events = Parser::new_ext(content, opts);
69 for (e, span) in events.into_offset_iter() {
70 log::debug!("e={:?}, span={:?}", e, span);
71 if let Event::Start(Tag::CodeBlock(Fenced(code))) = e.clone() {
72 if &*code == "mermaid" {
73 in_mermaid_block = true;
74 mermaid_content.clear();
75 }
76 continue;
77 }
78
79 if !in_mermaid_block {
80 continue;
81 }
82
83 if let Event::Text(_) = e {
86 if start_new_code_span {
87 code_span = span;
88 start_new_code_span = false;
89 } else {
90 code_span = code_span.start..span.end;
91 }
92
93 continue;
94 }
95
96 if let Event::End(TagEnd::CodeBlock) = e {
97 in_mermaid_block = false;
98
99 let mermaid_content = &content[code_span.clone()];
100 let mermaid_content = escape_html(mermaid_content);
101 let mermaid_content = mermaid_content.replace("\r\n", "\n");
102 let mermaid_code = format!("<pre class=\"mermaid\">{}</pre>\n\n", mermaid_content);
103 mermaid_blocks.push((span, mermaid_code));
104 start_new_code_span = true;
105 }
106 }
107
108 let mut content = content.to_string();
109 for (span, block) in mermaid_blocks.iter().rev() {
110 let pre_content = &content[0..span.start];
111 let post_content = &content[span.end..];
112 content = format!("{}\n{}{}", pre_content, block, post_content);
113 }
114 Ok(content)
115}
116
117impl Mermaid {
118 fn add_mermaid(chapter: &mut Chapter) -> Result<String> {
119 add_mermaid(&chapter.content)
120 }
121}
122
123#[cfg(test)]
124mod test {
125 use pretty_assertions::assert_eq;
126
127 use super::add_mermaid;
128
129 #[test]
130 fn adds_mermaid() {
131 let content = r#"# Chapter
132
133```mermaid
134graph TD
135A --> B
136```
137
138Text
139"#;
140
141 let expected = r#"# Chapter
142
143
144<pre class="mermaid">graph TD
145A --> B
146</pre>
147
148
149
150Text
151"#;
152
153 assert_eq!(expected, add_mermaid(content).unwrap());
154 }
155
156 #[test]
157 fn leaves_tables_untouched() {
158 let content = r#"# Heading
162
163| Head 1 | Head 2 |
164|--------|--------|
165| Row 1 | Row 2 |
166"#;
167
168 let expected = r#"# Heading
169
170| Head 1 | Head 2 |
171|--------|--------|
172| Row 1 | Row 2 |
173"#;
174
175 assert_eq!(expected, add_mermaid(content).unwrap());
176 }
177
178 #[test]
179 fn leaves_html_untouched() {
180 let content = r#"# Heading
184
185<del>
186
187*foo*
188
189</del>
190"#;
191
192 let expected = r#"# Heading
193
194<del>
195
196*foo*
197
198</del>
199"#;
200
201 assert_eq!(expected, add_mermaid(content).unwrap());
202 }
203
204 #[test]
205 fn html_in_list() {
206 let content = r#"# Heading
210
2111. paragraph 1
212 ```
213 code 1
214 ```
2152. paragraph 2
216"#;
217
218 let expected = r#"# Heading
219
2201. paragraph 1
221 ```
222 code 1
223 ```
2242. paragraph 2
225"#;
226
227 assert_eq!(expected, add_mermaid(content).unwrap());
228 }
229
230 #[test]
231 fn escape_in_mermaid_block() {
232 let _ = env_logger::try_init();
233 let content = r#"
234```mermaid
235classDiagram
236 class PingUploader {
237 <<interface>>
238 +Upload() UploadResult
239 }
240```
241
242hello
243"#;
244
245 let expected = r#"
246
247<pre class="mermaid">classDiagram
248 class PingUploader {
249 <<interface>>
250 +Upload() UploadResult
251 }
252</pre>
253
254
255
256hello
257"#;
258
259 assert_eq!(expected, add_mermaid(content).unwrap());
260 }
261
262 #[test]
263 fn more_backticks() {
264 let _ = env_logger::try_init();
265 let content = r#"# Chapter
266
267````mermaid
268graph TD
269A --> B
270````
271
272Text
273"#;
274
275 let expected = r#"# Chapter
276
277
278<pre class="mermaid">graph TD
279A --> B
280</pre>
281
282
283
284Text
285"#;
286
287 assert_eq!(expected, add_mermaid(content).unwrap());
288 }
289
290 #[test]
291 fn crlf_line_endings() {
292 let _ = env_logger::try_init();
293 let content = "# Chapter\r\n\r\n````mermaid\r\n\r\ngraph TD\r\nA --> B\r\n````";
294 let expected =
295 "# Chapter\r\n\r\n\n<pre class=\"mermaid\">\ngraph TD\nA --> B\n</pre>\n\n";
296
297 assert_eq!(expected, add_mermaid(content).unwrap());
298 }
299}