Skip to main content

mdbook_gabc/
lib.rs

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("&lt;"),
40            '>' => output.push_str("&gt;"),
41            '"' => output.push_str("&quot;"),
42            '&' => output.push_str("&amp;"),
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        // We're in the code block. The text is what we want.
77        // Code blocks can come in multiple text events.
78        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        // Regression test.
155        // Previously we forgot to enable the same markdwon extensions as mdbook itself.
156
157        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        // Regression test.
177        // Don't remove important newlines for syntax nested inside HTML
178
179        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        // Regression test.
203        // Don't remove important newlines for syntax nested inside HTML
204
205        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        //TODO may be able to delete this method.
230        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}