Skip to main content

mdbook_codename/
lib.rs

1use mdbook::book::{Book, BookItem, Chapter};
2use mdbook::errors::Error;
3use mdbook::preprocess::PreprocessorContext;
4use rust_embed::RustEmbed;
5
6#[derive(RustEmbed)]
7#[folder = "assets/"]
8struct Asset;
9
10pub struct Preprocessor;
11
12impl mdbook::preprocess::Preprocessor for Preprocessor {
13    fn name(&self) -> &str {
14        "codename"
15    }
16
17    fn supports_renderer(&self, renderer: &str) -> bool {
18        renderer == "html"
19    }
20
21    fn run(&self, _: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
22        let mut error: Option<Error> = None;
23        book.for_each_mut(|item: &mut BookItem| {
24            if error.is_some() {
25                return;
26            }
27            if let BookItem::Chapter(ref mut chapter) = *item {
28                if let Err(err) = handle_chapter(chapter) {
29                    error = Some(err);
30                }
31            }
32        });
33
34        error.map_or(Ok(book), Err)
35    }
36}
37
38struct CodeblockParser {
39    content: String,
40    codeblock: Option<Codeblock>,
41}
42
43struct Codeblock {
44    lines: Vec<String>,
45    lang: Option<String>,
46    filename: Option<String>,
47}
48
49impl Codeblock {
50    fn new(fence_line: &str) -> Self {
51        let s = fence_line.trim_start_matches("```");
52
53        let (lang, filename) = if s.contains(':') {
54            let mut parts = s.split(':');
55            let lang = parts.next().unwrap().trim().into();
56            let filename = parts.collect::<Vec<&str>>().join(":").trim().into();
57
58            (Some(lang), Some(filename))
59        } else {
60            let lang = if s.contains('.') {
61                Some(s.split('.').last().unwrap().into())
62            } else {
63                Some(s.into())
64            };
65
66            (lang, Some(s.into()))
67        };
68
69        Self {
70            lines: Vec::new(),
71            lang,
72            filename,
73        }
74    }
75
76    fn push(&mut self, line: &str) {
77        self.lines.push(line.into());
78    }
79
80    fn parse(&mut self) -> Vec<String> {
81        let mut parsed_lines: Vec<String> = Vec::new();
82
83        if let Some(filename) = &self.filename {
84            if !filename.is_empty() {
85                parsed_lines.push(format!("<div class=\"codeblock_filename_container\"><span class=\"codeblock_filename_inner hljs\">{}</span></div>\n",
86                filename.clone(),
87            ));
88            }
89        }
90
91        parsed_lines.push(format!("```{}", self.lang.clone().unwrap_or("".into())));
92        parsed_lines.extend(self.lines.clone());
93        parsed_lines.push("```".into());
94
95        parsed_lines
96    }
97}
98
99impl CodeblockParser {
100    fn new(content: &str) -> Self {
101        Self {
102            content: content.into(),
103            codeblock: None,
104        }
105    }
106
107    fn parse(&mut self) -> String {
108        let mut parsed_lines: Vec<String> = Vec::new();
109
110        for line in self.content.lines() {
111            if is_fence(line) {
112                if let Some(cb) = self.codeblock.as_mut() {
113                    parsed_lines.extend(cb.parse());
114                    self.codeblock = None;
115                } else {
116                    self.codeblock = Some(Codeblock::new(line));
117                }
118            } else {
119                if let Some(cb) = self.codeblock.as_mut() {
120                    cb.push(line);
121                } else {
122                    parsed_lines.push(line.into());
123                }
124            }
125        }
126
127        parsed_lines.join("\n")
128    }
129}
130
131fn is_fence(line: &str) -> bool {
132    line.starts_with("```")
133}
134
135fn handle_chapter(chapter: &mut Chapter) -> Result<(), Error> {
136    chapter.content = inject_css(&chapter.content)?;
137    chapter.content = render_codename(&chapter.content)?;
138
139    Ok(())
140}
141
142fn inject_css(content: &str) -> Result<String, Error> {
143    let style = Asset::get("style.css")
144        .ok_or_else(|| Error::msg("Failed to read style.css from assets"))?;
145    let style = std::str::from_utf8(style.data.as_ref())?;
146
147    Ok(format!("<style>\n{style}\n</style>\n{content}"))
148}
149
150fn render_codename(content: &str) -> Result<String, Error> {
151    let mut parser = CodeblockParser::new(content);
152
153    Ok(parser.parse())
154}