1use anyhow::Result;
2use mdbook::book::{Book, Chapter};
3use mdbook::errors::Result as MdbookResult;
4use mdbook::preprocess::{Preprocessor, PreprocessorContext};
5use mdbook::BookItem;
6use pulldown_cmark::{Event, Options, Parser};
7
8pub struct BlushPreprocessor;
9
10impl BlushPreprocessor {
11 pub fn new() -> Self {
12 Self
13 }
14
15 fn preprocess_bookitem(&self, item: &mut BookItem) -> Result<()> {
16 match item {
17 BookItem::Chapter(chapter) => self.preprocess_chapter(chapter),
18 BookItem::Separator | BookItem::PartTitle(_) => Ok(()),
19 }
20 }
21
22 fn preprocess_chapter(&self, chapter: &mut Chapter) -> Result<()> {
23 let parser = Parser::new_ext(&chapter.content, Options::all()).map(|event| match event {
24 Event::Text(text) => Event::Text(self.preprocess_text(&text).into()),
25 _ => event,
26 });
27 let new_content_capacity = (chapter.content.len() as f64 * 1.05) as usize;
28 let mut new_content = String::with_capacity(new_content_capacity);
29 pulldown_cmark_to_cmark::cmark(parser, &mut new_content)?;
30 chapter.content = new_content;
31
32 for sub_item in &mut chapter.sub_items {
33 self.preprocess_bookitem(sub_item)?;
34 }
35
36 Ok(())
37 }
38
39 fn preprocess_text(&self, text: &str) -> String {
40 let mut fragments = Vec::with_capacity(8);
41 let mut cursor = 0;
42 let mut char_indices = text
43 .char_indices()
44 .filter(|(_, chr)| *chr == '=' || *chr == ' ' || *chr == '\t');
45 while let Some((index, chr)) = char_indices.next() {
46 if chr != '=' {
47 continue;
48 }
49
50 fragments.push(&text[cursor..index]);
51 cursor = index;
52
53 if let Some((end_index, chr)) = char_indices.next() {
54 if chr != '=' || end_index == 1 + index {
55 fragments.push(&text[cursor..end_index]);
56 cursor = end_index;
57 continue;
58 }
59
60 fragments.push(r#"<span class="small-caps">"#);
61 fragments.push(&text[(cursor + 1)..end_index]);
62 fragments.push(r#"</span>"#);
63 cursor = end_index + 1;
64 }
65 }
66
67 fragments.push(&text[cursor..]);
68 fragments.concat()
69 }
70}
71
72impl Preprocessor for BlushPreprocessor {
73 fn name(&self) -> &str {
74 "mdbook-blush"
75 }
76
77 fn supports_renderer(&self, renderer: &str) -> bool {
78 renderer == "html"
79 }
80
81 fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> MdbookResult<Book> {
82 book.sections
83 .iter_mut()
84 .try_for_each(|section| self.preprocess_bookitem(section))?;
85 Ok(book)
86 }
87}