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}