1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use fancy_regex::Regex;
use markdown::{to_html_with_options, CompileOptions, Options};
use mdbook::{
    book::{Book, BookItem, Chapter},
    errors::Error,
    preprocess::PreprocessorContext,
};
use std::collections::HashMap;
use std::fs::File;
use std::io::BufWriter;
use std::path::{Path, PathBuf};

pub struct HintPreprocessor;

impl mdbook::preprocess::Preprocessor for HintPreprocessor {
    fn name(&self) -> &str {
        "hints"
    }

    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
        let mut error: Option<Error> = None;
        book.for_each_mut(|item: &mut BookItem| {
            if error.is_some() {
                return;
            }
            if let BookItem::Chapter(ref mut chapter) = *item {
                if let Err(err) = handle_chapter(chapter, &ctx.config.book.src) {
                    error = Some(err)
                }
            }
        });
        error.map_or(Ok(book), Err)
    }

    fn supports_renderer(&self, renderer: &str) -> bool {
        renderer == "html"
    }
}

pub fn handle_chapter(chapter: &mut Chapter, src_path: &Path) -> Result<(), Error> {
    let extractor = HintsExtractor::new();
    extractor.output_json(src_path)?;

    render_hints(chapter, extractor.toml()?)?;

    Ok(())
}

#[derive(serde::Deserialize, Debug)]
struct HintEntry {
    hint: String,
    _auto: Option<Vec<String>>,
}

fn render_hints(chapter: &mut Chapter, hints: HashMap<String, HintEntry>) -> Result<(), Error> {
    render_manual(chapter, &hints)?;
    //render_auto(chapter, &hints)?;
    Ok(())
}
fn _render_auto(_chapter: &mut Chapter, _hints: &HashMap<String, HintEntry>) -> Result<(), Error> {
    todo!()
}

fn render_manual(chapter: &mut Chapter, hints: &HashMap<String, HintEntry>) -> Result<(), Error> {
    let re = Regex::new(r"\[((?:(?!]\().)*?)]\(~(.*?)\)")?;

    if re.is_match(&chapter.content)? {
        let content = re
            .replace_all(&chapter.content, |caps: &fancy_regex::Captures| {
                let first_capture = &caps[1];
                let second_capture = &caps[2];

                if hints.get(second_capture).is_none() && !second_capture.starts_with("!") {
                    eprintln!(
                        "-----\nHint for `{}` ({}) is missing in hints.toml!\n-----",
                        second_capture,
                        &chapter.path.clone().unwrap().display()
                    );
                    first_capture.to_string()
                } else if second_capture.starts_with("!") {
                    first_capture.to_string()
                } else {
                    format!(
                        r#"<span class="hint" hint="{}">{}</span>"#,
                        second_capture, first_capture
                    )
                }
            })
            .to_string();

        chapter.content = content;
    };

    Ok(())
}

struct HintsExtractor {
    path: PathBuf,
}

impl HintsExtractor {
    fn new() -> Self {
        let path = std::env::current_dir().unwrap();
        Self { path }
    }
    fn toml(&self) -> Result<HashMap<String, HintEntry>, Error> {
        let hints = self.path.join("hints.toml");
        let hints = std::fs::read_to_string(hints)?;

        let toml: HashMap<String, HintEntry> = toml::from_str(&hints)?;

        Ok(toml)
    }

    fn output_json(&self, src_path: &Path) -> Result<(), Error> {
        let mut json: HashMap<String, String> = HashMap::new();

        for (name, entry) in self.toml()? {
            let mut hint = to_html_with_options(
                &entry.hint,
                &Options {
                    compile: CompileOptions {
                        allow_dangerous_html: true,
                        ..CompileOptions::default()
                    },
                    ..Options::default()
                },
            )
            .unwrap();

            hint = hint.replace("<p>", "").replace("</p>", "");

            json.insert(name, hint);
        }

        let file = File::create(src_path.join("hints.json"))?;
        let writer = BufWriter::new(file);

        serde_json::to_writer(writer, &json)?;

        Ok(())
    }
}