1use mdbook_driver::book::{Book, BookItem, Chapter};
2use mdbook_driver::errors::Error;
3use mdbook_preprocessor::PreprocessorContext;
4use once_cell::sync::Lazy;
5use regex::Regex;
6use rust_embed::Embed;
7
8#[derive(Embed)]
9#[folder = "assets/"]
10struct Asset;
11
12pub struct Preprocessor;
13
14impl mdbook_preprocessor::Preprocessor for Preprocessor {
15 fn name(&self) -> &str {
16 "callouts"
17 }
18
19 fn supports_renderer(&self, renderer: &str) -> Result<bool, Error> {
20 Ok(renderer == "html")
21 }
22
23 fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
24 let mut error: Option<Error> = None;
25 book.for_each_mut(|item: &mut BookItem| {
26 if error.is_some() {
27 return;
28 }
29 if let BookItem::Chapter(ref mut chapter) = *item {
30 if let Err(err) = handle_chapter(chapter) {
31 error = Some(err)
32 }
33 }
34 });
35 error.map_or(Ok(book), Err)
36 }
37}
38
39fn handle_chapter(chapter: &mut Chapter) -> Result<(), Error> {
40 chapter.content = inject_stylesheet(&chapter.content)?;
41 chapter.content = render_callouts(&chapter.content)?;
42 Ok(())
43}
44
45fn inject_stylesheet(content: &str) -> Result<String, Error> {
46 let style = Asset::get("style.css").expect("style.css not found in assets");
47 let style = std::str::from_utf8(style.data.as_ref())?;
48 Ok(format!("<style>\n{style}\n</style>\n{content}"))
49}
50
51fn render_callouts(content: &str) -> Result<String, Error> {
52 static RE: Lazy<Regex> = Lazy::new(|| {
53 Regex::new(r"(?m)^> \[!(?P<kind>[^\]]+)\](?P<title>\ {1}[^\n]+)?$(?P<body>(?:\n>.*)*)")
55 .expect("failed to parse regex")
56 });
57 let alerts = Asset::get("alerts.tmpl").expect("alerts.tmpl not found in assets");
58 let alerts = std::str::from_utf8(alerts.data.as_ref())?;
59 let content = RE.replace_all(content, |caps: ®ex::Captures| {
60 let kind = caps
61 .name("kind")
62 .expect("kind not found in regex")
63 .as_str()
64 .trim()
65 .to_lowercase();
66 let title = caps
67 .name("title")
68 .map(|m| m.as_str().trim())
69 .unwrap_or(kind.as_str())
70 .to_lowercase();
71 let body = caps
72 .name("body")
73 .expect("body not found in regex")
74 .as_str()
75 .replace("\n>\n", "\n\n")
76 .replace("\n> ", "\n");
77
78 alerts
79 .replace("{title}", &title)
80 .replace("{kind}", &kind)
81 .replace("{body}", &body)
82 });
83 Ok(content.into())
84}