mdbook_svgbob2/
lib.rs

1use mdbook::{
2    book::Book,
3    errors::Error,
4    preprocess::{Preprocessor, PreprocessorContext},
5    BookItem,
6};
7use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
8use pulldown_cmark_to_cmark::cmark;
9use svgbob::Settings;
10
11pub struct Bob;
12
13impl Bob {
14    pub fn new() -> Self {
15        Self
16    }
17}
18
19impl Default for Bob {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl Preprocessor for Bob {
26    fn name(&self) -> &str {
27        "svgbob2"
28    }
29
30    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
31        // first load the configuration from the book.toml
32        // also apply some default settings that are good for mdbook
33        let mut settings = svgbob::Settings {
34            background: "transparent".to_owned(),
35            stroke_color: "var(--fg)".to_owned(),
36            ..Default::default()
37        };
38
39        let mut font_color = "var(--fg)".to_owned();
40
41        if let Some(cfg) = ctx.config.get_preprocessor(self.name()) {
42            cfg.iter().for_each(|(key, val)| match key.as_str() {
43                "font_size" => settings.font_size = val.clone().try_into().unwrap(),
44                "font_family" => settings.font_family = val.clone().try_into().unwrap(),
45                "fill_color" => settings.fill_color = val.clone().try_into().unwrap(),
46                "background" => settings.background = val.clone().try_into().unwrap(),
47                "stroke_color" => settings.stroke_color = val.clone().try_into().unwrap(),
48                "stroke_width" => settings.stroke_width = val.clone().try_into().unwrap(),
49                "scale" => settings.scale = val.clone().try_into().unwrap(),
50                "enhance_circuitries" => {
51                    settings.enhance_circuitries = val.clone().try_into().unwrap()
52                }
53                "include_backdrop" => settings.include_backdrop = val.clone().try_into().unwrap(),
54                "include_styles" => settings.include_styles = val.clone().try_into().unwrap(),
55                "include_defs" => settings.include_defs = val.clone().try_into().unwrap(),
56                "merge_line_with_shapes" => {
57                    settings.merge_line_with_shapes = val.clone().try_into().unwrap()
58                }
59
60                // non-svgbob custom setting
61                "font_color" => font_color = val.clone().try_into().unwrap(),
62
63                _ => (), // this should not happen
64            });
65        }
66
67        book.for_each_mut(|item| {
68            if let BookItem::Chapter(chapter) = item {
69                // saved to check if we are currently inside a codeblock
70                let mut in_block = false;
71
72                // if Windows crlf line endings are used, a code block will consist
73                // of many different Text blocks, thus we need to buffer them in here
74                // see https://github.com/raphlinus/pulldown-cmark/issues/507
75                let mut diagram = String::new();
76                let events =
77                    Parser::new_ext(&chapter.content, Options::all()).filter_map(|event| {
78                        match (&event, in_block) {
79                            // check if we are entering a svgbob codeblock
80                            (
81                                Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(
82                                    CowStr::Borrowed("svgbob"),
83                                ))),
84                                false,
85                            ) => {
86                                in_block = true;
87                                diagram.clear();
88                                None
89                            }
90                            // check if we are currently inside an svgbob block
91                            (Event::Text(content), true) => {
92                                diagram.push_str(content);
93                                None
94                            }
95                            // check if we are exiting an svgbob block
96                            (
97                                Event::End(Tag::CodeBlock(CodeBlockKind::Fenced(
98                                    CowStr::Borrowed("svgbob"),
99                                ))),
100                                true,
101                            ) => {
102                                in_block = false;
103                                Some(Event::Html(
104                                    create_svg_html(&diagram, &settings, &font_color).into(),
105                                ))
106                            }
107                            // if nothing matches, change nothing
108                            _ => Some(event),
109                        }
110                    });
111
112                // create a buffer in which we can place the markdown
113                let mut buf = String::with_capacity(chapter.content.len() + 128);
114
115                // convert it back to markdown and replace the original chapter's content
116                cmark(events, &mut buf).unwrap();
117                chapter.content = buf;
118            }
119        });
120
121        Ok(book)
122    }
123
124    fn supports_renderer(&self, renderer: &str) -> bool {
125        renderer == "html"
126    }
127}
128
129fn create_svg_html(s: &str, settings: &Settings, font_color: &str) -> String {
130    let svg = svgbob::to_svg_with_settings(s, settings);
131
132    // I am actually not sure why this has to be a pre tag
133    // taken from https://github.com/badboy/mdbook-mermaid/blob/main/src/lib.rs
134    format!(
135        "<pre class=\"svgbob\"><style>text{{fill:{}}}</style>{}</pre>",
136        font_color, svg
137    )
138}