1use mdbook::book::{Book, BookItem, Chapter};
2use mdbook::errors::Result;
3use mdbook::preprocess::{Preprocessor, PreprocessorContext};
4use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag};
5
6pub struct Gabc;
7
8impl Preprocessor for Gabc {
9 fn name(&self) -> &str {
10 "gabc"
11 }
12
13 fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
14 let mut res = None;
15 book.for_each_mut(|item: &mut BookItem| {
16 if let Some(Err(_)) = res {
17 return;
18 }
19
20 if let BookItem::Chapter(ref mut chapter) = *item {
21 res = Some(Gabc::add_gabc(chapter).map(|md| {
22 chapter.content = md;
23 }));
24 }
25 });
26
27 res.unwrap_or(Ok(())).map(|_| book)
28 }
29
30 fn supports_renderer(&self, renderer: &str) -> bool {
31 renderer == "html"
32 }
33}
34
35fn escape_html(s: &str) -> String {
36 let mut output = String::new();
37 for c in s.chars() {
38 match c {
39 '<' => output.push_str("<"),
40 '>' => output.push_str(">"),
41 '"' => output.push_str("""),
42 '&' => output.push_str("&"),
43 _ => output.push(c),
44 }
45 }
46 output
47}
48
49fn add_gabc(content: &str) -> Result<String> {
50 let mut in_gabc_block = false;
51
52 let mut opts = Options::empty();
53 opts.insert(Options::ENABLE_TABLES);
54 opts.insert(Options::ENABLE_FOOTNOTES);
55 opts.insert(Options::ENABLE_STRIKETHROUGH);
56 opts.insert(Options::ENABLE_TASKLISTS);
57
58 let mut code_span = 0..0;
59 let mut start_new_code_span = true;
60
61 let mut gabc_blocks = vec![];
62
63 let events = Parser::new_ext(content, opts);
64
65 for (e, span) in events.into_offset_iter() {
66 log::debug!("e={:?}, span={:?}", e, span);
67 if let Event::Start(Tag::CodeBlock(Fenced(code))) = e.clone() {
68 in_gabc_block = &*code == "gabc";
69 continue;
70 }
71
72 if !in_gabc_block {
73 continue;
74 }
75
76 if let Event::Text(_) = e {
79 if start_new_code_span {
80 code_span = span;
81 start_new_code_span = false;
82 } else {
83 code_span = code_span.start..span.end;
84 }
85 continue;
86 }
87
88 if let Event::End(Tag::CodeBlock(Fenced(code))) = e {
89 if in_gabc_block {
90 assert_eq!(
91 "gabc", &*code,
92 "After an opening gabc code block we expect it to close again"
93 );
94 }
95 in_gabc_block = false;
96
97 let gabc_content = &content[code_span.clone()];
98 let gabc_content = escape_html(gabc_content);
99 let gabc_content = gabc_content.replace("\r\n", "\n");
100 let gabc_content = format!("<pre class=\"chant-container\">{}</pre>\n\n", gabc_content);
101 gabc_blocks.push((span, gabc_content));
102 start_new_code_span = true;
103 }
104 }
105
106 let mut content = content.to_string();
107 for (span, block) in gabc_blocks.iter().rev() {
108 let pre_content = &content[0..span.start];
109 let post_content = &content[span.end..];
110 content = format!("{}\n{}{}", pre_content, block, post_content);
111 }
112 Ok(content)
113}
114
115impl Gabc {
116 fn add_gabc(chapter: &mut Chapter) -> Result<String> {
117 add_gabc(&chapter.content)
118 }
119}
120
121#[cfg(test)]
122mod test {
123 use pretty_assertions::assert_eq;
124
125 use super::add_gabc;
126
127 #[test]
128 fn adds_gabc() {
129 let content = r#"# Chapter
130
131```gabc
132(f3) EC(ce!fg)CE(f) *(,) ad(fe~)vé(f!gwhf)nit(f) (,)
133```
134
135Text
136"#;
137
138 let expected = r#"# Chapter
139
140
141<pre class="chant-container">(f3) EC(ce!fg)CE(f) *(,) ad(fe~)vé(f!gwhf)nit(f) (,)
142</pre>
143
144
145
146Text
147"#;
148
149 assert_eq!(expected, add_gabc(content).unwrap());
150 }
151
152 #[test]
153 fn leaves_tables_untouched() {
154 let content = r#"# Heading
158
159| Head 1 | Head 2 |
160|--------|--------|
161| Row 1 | Row 2 |
162"#;
163
164 let expected = r#"# Heading
165
166| Head 1 | Head 2 |
167|--------|--------|
168| Row 1 | Row 2 |
169"#;
170
171 assert_eq!(expected, add_gabc(content).unwrap());
172 }
173
174 #[test]
175 fn leaves_html_untouched() {
176 let content = r#"# Heading
180
181<del>
182
183*foo*
184
185</del>
186"#;
187
188 let expected = r#"# Heading
189
190<del>
191
192*foo*
193
194</del>
195"#;
196
197 assert_eq!(expected, add_gabc(content).unwrap());
198 }
199
200 #[test]
201 fn html_in_list() {
202 let content = r#"# Heading
206
2071. paragraph 1
208 ```
209 code 1
210 ```
2112. paragraph 2
212"#;
213
214 let expected = r#"# Heading
215
2161. paragraph 1
217 ```
218 code 1
219 ```
2202. paragraph 2
221"#;
222
223 let ret = add_gabc(content).unwrap();
224 assert_eq!(expected, ret);
225 }
226
227 #[test]
228 fn escape_in_gabc_block() {
229 let _ = env_logger::try_init();
231 let content = r#"
232```gabc
233(f3) EC(ce!fg)CE(f) *(,) ad(fe~)vé(f!gwhf)nit(f) (,)
234```
235
236hello
237"#;
238
239 let expected = r#"
240
241<pre class="chant-container">(f3) EC(ce!fg)CE(f) *(,) ad(fe~)vé(f!gwhf)nit(f) (,)
242</pre>
243
244
245
246hello
247"#;
248
249 assert_eq!(expected, add_gabc(content).unwrap());
250 }
251
252 #[test]
253 fn more_backticks() {
254 let _ = env_logger::try_init();
255 let content = r#"# Chapter
256
257````gabc
258(f3) EC(ce!fg)CE(f) *(,) ad(fe~)vé(f!gwhf)nit(f) (,)
259````
260
261Text
262"#;
263
264 let expected = r#"# Chapter
265
266
267<pre class="chant-container">(f3) EC(ce!fg)CE(f) *(,) ad(fe~)vé(f!gwhf)nit(f) (,)
268</pre>
269
270
271
272Text
273"#;
274
275 assert_eq!(expected, add_gabc(content).unwrap());
276 }
277
278 #[test]
279 fn crlf_line_endings() {
280 let _ = env_logger::try_init();
281 let content = "# Chapter\r\n\r\n````gabc\r\n\r\n(f3) EC(ce!fg)CE(f) *(,)\r\nad(fe~)vé(f!gwhf)nit(f) (,)\r\n````";
282 let expected =
283 "# Chapter\r\n\r\n\n<pre class=\"chant-container\">\n(f3) EC(ce!fg)CE(f) *(,)\nad(fe~)vé(f!gwhf)nit(f) (,)\n</pre>\n\n";
284
285 assert_eq!(expected, add_gabc(content).unwrap());
286 }
287
288 #[test]
289 fn test_leaves_nongabc_untouched() {
290 let content = r#"Chapter\nsample program```python\nprint('output')```\nfinished"#;
291 assert_eq!(content, add_gabc(content).unwrap());
292 }
293
294 #[test]
295 fn test_multiple_blocks() {
296 let content = "```\nsample code\n```\n```gabc\n(f3) ec(f)ce(g)\n```\n";
297 let expected =
298 "```\nsample code\n```\n\n<pre class=\"chant-container\">(f3) ec(f)ce(g)\n</pre>\n\n\n";
299 assert_eq!(expected, add_gabc(content).unwrap());
300 }
301}