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 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: ®ex::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}