mdbook_hints/
lib.rs

1use markdown::{to_html_with_options, CompileOptions, Options};
2use mdbook::{
3    book::{Book, BookItem, Chapter},
4    errors::Error,
5    preprocess::PreprocessorContext,
6};
7use regex::Regex;
8use std::collections::{BTreeMap};
9use std::fs::File;
10use std::io::BufWriter;
11use std::path::{Path, PathBuf};
12
13pub struct HintPreprocessor;
14
15impl mdbook::preprocess::Preprocessor for HintPreprocessor {
16    fn name(&self) -> &str {
17        "hints"
18    }
19
20    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
21        let mut error: Option<Error> = None;
22        book.for_each_mut(|item: &mut BookItem| {
23            if error.is_some() {
24                return;
25            }
26            if let BookItem::Chapter(ref mut chapter) = *item {
27                if let Err(err) = handle_chapter(chapter, &ctx.config.book.src) {
28                    error = Some(err)
29                }
30            }
31        });
32        error.map_or(Ok(book), Err)
33    }
34
35    fn supports_renderer(&self, renderer: &str) -> bool {
36        renderer == "html"
37    }
38}
39
40pub fn handle_chapter(chapter: &mut Chapter, src_path: &Path) -> Result<(), Error> {
41    let extractor = HintsExtractor::new();
42    extractor.output_json(src_path)?;
43
44    render_hints(chapter, extractor.toml()?)?;
45
46    Ok(())
47}
48
49#[derive(serde::Deserialize, Debug)]
50struct HintEntry {
51    hint: String,
52    _auto: Option<Vec<String>>,
53}
54
55fn render_hints(chapter: &mut Chapter, hints: BTreeMap<String, HintEntry>) -> Result<(), Error> {
56    render_manual(chapter, &hints)?;
57    //render_auto(chapter, &hints)?;
58    Ok(())
59}
60fn _render_auto(_chapter: &mut Chapter, _hints: &BTreeMap<String, HintEntry>) -> Result<(), Error> {
61    todo!()
62}
63
64fn render_manual(chapter: &mut Chapter, hints: &BTreeMap<String, HintEntry>) -> Result<(), Error> {
65    let re = Regex::new(r"\[([^]]+)]\(~(.*?)\)")?;
66
67    if re.is_match(&chapter.content) {
68        let content = re
69            .replace_all(&chapter.content, |caps: &regex::Captures| {
70                let first_capture = &caps[1];
71                let second_capture = &caps[2];
72
73                if hints.get(second_capture).is_none() && !second_capture.starts_with("!") {
74                    eprintln!(
75                        "-----\nHint for `{}` ({}) is missing in hints.toml!\n-----",
76                        second_capture,
77                        &chapter.path.clone().unwrap().display()
78                    );
79                    first_capture.to_string()
80                } else if second_capture.starts_with("!") {
81                    first_capture.to_string()
82                } else {
83                    format!(
84                        r#"<span class="hint" hint="{}">{}</span>"#,
85                        second_capture, first_capture
86                    )
87                }
88            })
89            .to_string();
90
91        chapter.content = content;
92    };
93
94    Ok(())
95}
96
97struct HintsExtractor {
98    path: PathBuf,
99}
100
101impl HintsExtractor {
102    fn new() -> Self {
103        let path = std::env::current_dir().unwrap();
104        Self { path }
105    }
106    fn toml(&self) -> Result<BTreeMap<String, HintEntry>, Error> {
107        let hints = self.path.join("hints.toml");
108        let hints = std::fs::read_to_string(hints)?;
109
110        let toml: BTreeMap<String, HintEntry> = toml::from_str(&hints)?;
111
112        Ok(toml)
113    }
114
115    fn output_json(&self, src_path: &Path) -> Result<(), Error> {
116        let mut json: BTreeMap<String, String> = BTreeMap::new();
117
118        for (name, entry) in self.toml()? {
119            let mut hint = to_html_with_options(
120                &entry.hint,
121                &Options {
122                    compile: CompileOptions {
123                        allow_dangerous_html: true,
124                        ..CompileOptions::default()
125                    },
126                    ..Options::default()
127                },
128            )
129            .unwrap();
130
131            hint = hint.replace("<p>", "").replace("</p>", "");
132
133            json.insert(name, hint);
134        }
135
136        let file = File::create(src_path.join("hints.json"))?;
137        let writer = BufWriter::new(file);
138
139        serde_json::to_writer(writer, &json)?;
140
141        Ok(())
142    }
143}