Skip to main content

badascii_mdbook/
lib.rs

1use mdbook::{
2    BookItem,
3    book::{Book, Chapter},
4    errors::Error,
5    preprocess::{Preprocessor, PreprocessorContext},
6};
7use pulldown_cmark::{CodeBlockKind, CowStr, Event, Tag, TagEnd};
8
9// The BadAscii preprocessor.
10pub struct BadAscii;
11
12fn create_svg_html(formal_mode: bool, s: &str) -> String {
13    let tb = badascii::TextBuffer::with_text(s);
14    let job = if !formal_mode {
15        badascii::RenderJob::rough(tb)
16    } else {
17        badascii::RenderJob::formal(tb)
18    };
19    // TODO - figure out light vs dark mode for MDBook?
20    let svg = badascii::svg::render(&job, "currentColor", "none");
21    format!("\n\n<pre>{svg}</pre>\n")
22}
23impl BadAscii {
24    fn process_chapter(formal_mode: bool, chapter: &mut Chapter) {
25        let mut options = pulldown_cmark::Options::empty();
26        options.insert(pulldown_cmark::Options::ENABLE_TABLES);
27        let parser = pulldown_cmark::Parser::new_ext(&chapter.content, options);
28        let mut buf = String::with_capacity(chapter.content.len() + 128);
29        // Inspired by svgbob2 mdbook preprocessor.
30
31        let mut in_block = false;
32        let mut diagram = String::new();
33        let events = parser.filter_map(|event| match (&event, in_block) {
34            (
35                Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::Borrowed("badascii")))),
36                false,
37            ) => {
38                in_block = true;
39                diagram.clear();
40                None
41            }
42            (Event::Text(content), true) => {
43                diagram.push_str(content);
44                None
45            }
46            (Event::End(TagEnd::CodeBlock), true) => {
47                in_block = false;
48                Some(Event::Html(create_svg_html(formal_mode, &diagram).into()))
49            }
50            _ => Some(event),
51        });
52        pulldown_cmark_to_cmark::cmark(events, &mut buf).unwrap();
53        chapter.content = buf;
54    }
55}
56
57impl Preprocessor for BadAscii {
58    fn name(&self) -> &str {
59        "badascii"
60    }
61
62    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
63        // In testing we want to tell the preprocessor to blow up by setting a
64        // particular config value
65        let formal_mode = if let Some(nop_cfg) = &ctx.config.get_preprocessor(self.name()) {
66            nop_cfg.contains_key("formal")
67        } else {
68            false
69        };
70        book.for_each_mut(|item| {
71            if let BookItem::Chapter(chapter) = item {
72                Self::process_chapter(formal_mode, chapter);
73            }
74        });
75
76        // we *are* a no-op preprocessor after all
77        Ok(book)
78    }
79
80    fn supports_renderer(&self, renderer: &str) -> bool {
81        renderer != "not-supported"
82    }
83}
84
85#[cfg(test)]
86mod test {
87    use super::*;
88
89    #[test]
90    fn conversion_works() {
91        let md = r##"
92# Chapter 1
93
94Here is a diagram of the mascot.
95```badascii
96  +----+
97  |  OO|
98  +----+
99```
100        "##;
101        let mut chapter = Chapter {
102            name: "Test".into(),
103            content: md.into(),
104            number: None,
105            sub_items: vec![],
106            path: None,
107            source_path: None,
108            parent_names: vec![],
109        };
110        BadAscii::process_chapter(false, &mut chapter);
111        let expect = expect_test::expect_file!["test.md"];
112        expect.assert_eq(&chapter.content);
113    }
114
115    #[test]
116    fn test_formal_mode_works() {
117        let md = r##"
118# Chapter 1
119
120Here is a diagram of the mascot.
121```badascii
122  +----+
123  |  OO|
124  +----+
125```
126        "##;
127        let mut chapter = Chapter {
128            name: "Test".into(),
129            content: md.into(),
130            number: None,
131            sub_items: vec![],
132            path: None,
133            source_path: None,
134            parent_names: vec![],
135        };
136        BadAscii::process_chapter(true, &mut chapter);
137        let expect = expect_test::expect_file!["test_formal.md"];
138        expect.assert_eq(&chapter.content);
139    }
140
141    #[test]
142    fn badascii_preprocessor_run() {
143        let input_json = r##"[
144                {
145                    "root": "/path/to/book",
146                    "config": {
147                        "book": {
148                            "authors": ["AUTHOR"],
149                            "language": "en",
150                            "multilingual": false,
151                            "src": "src",
152                            "title": "TITLE"
153                        },
154                        "preprocessor": {
155                            "nop": {}
156                        }
157                    },
158                    "renderer": "html",
159                    "mdbook_version": "0.4.21"
160                },
161                {
162                    "sections": [
163                        {
164                            "Chapter": {
165                                "name": "Chapter 1",
166                                "content": "# Chapter 1",
167                                "number": [1],
168                                "sub_items": [],
169                                "path": "chapter_1.md",
170                                "source_path": "chapter_1.md",
171                                "parent_names": []
172                            }
173                        }
174                    ],
175                    "__non_exhaustive": null
176                }
177            ]"##;
178        let input_json = input_json.as_bytes();
179
180        let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
181        let result = BadAscii.run(&ctx, book);
182        assert!(result.is_ok());
183        let expected_book = expect_test::expect_file!["test.txt"];
184        let actual_book = result.unwrap();
185        expected_book.assert_debug_eq(&actual_book);
186    }
187}