1use log::warn;
4use mdbook::book::{Book, BookItem};
5use mdbook::errors::Result;
6use mdbook::preprocess::{Preprocessor, PreprocessorContext};
7use pathdiff::diff_paths;
8use regex::Regex;
9use serde::Deserialize;
10use std::collections::HashMap;
11use std::ops::{Deref, DerefMut};
12use std::path::{Path, PathBuf};
13
14const NAME: &str = "numthm";
16
17#[derive(Debug, Clone, Deserialize)]
19struct Env {
20 #[serde(default = "Env::name_default")]
22 name: String,
23 #[serde(default = "Env::emph_default")]
25 emph: String,
26}
27
28impl Env {
29 fn create(name: &str, emph: &str) -> Self {
30 Env {
31 name: name.to_string(),
32 emph: emph.to_string(),
33 }
34 }
35 fn name_default() -> String {
36 String::from("Environment")
37 }
38 fn emph_default() -> String {
39 String::from("**")
40 }
41}
42
43#[derive(Debug, Clone, Deserialize)]
45struct EnvMap(HashMap<String, Env>);
46
47impl Default for EnvMap {
48 fn default() -> Self {
49 let mut envs: HashMap<String, Env> = HashMap::new();
50 envs.insert("thm".to_string(), Env::create("Theorem", "**"));
51 envs.insert("lem".to_string(), Env::create("Lemma", "**"));
52 envs.insert("prop".to_string(), Env::create("Proposition", "**"));
53 envs.insert("def".to_string(), Env::create("Definition", "**"));
54 envs.insert("rem".to_string(), Env::create("Remark", "*"));
55 EnvMap(envs)
56 }
57}
58
59impl Deref for EnvMap {
60 type Target = HashMap<String, Env>;
61 fn deref(&self) -> &Self::Target {
62 &self.0
63 }
64}
65impl DerefMut for EnvMap {
66 fn deref_mut(&mut self) -> &mut Self::Target {
67 &mut self.0
68 }
69}
70
71#[derive(Debug, PartialEq)]
73struct LabelInfo {
74 num_name: String,
76 path: PathBuf,
78 title: Option<String>,
80}
81
82#[derive(Clone, Debug, Default, Deserialize)]
84pub struct NumThmPreprocessor {
85 environments: EnvMap,
87 with_prefix: bool,
89}
90
91impl NumThmPreprocessor {
92 pub fn new(ctx: &PreprocessorContext) -> Self {
93 let mut config = Self::default();
94
95 let toml_config: &toml::value::Table = ctx.config.get_preprocessor("numthm").unwrap();
96
97 if let Some(b) = toml_config.get("prefix").and_then(toml::Value::as_bool) {
99 config.with_prefix = b;
100 }
101
102 if let Some(envs) = toml_config
104 .get("environments")
105 .and_then(toml::Value::as_table)
106 {
107 for (key, value) in envs.iter() {
108 if let Some(entry) = toml::Value::as_table(value) {
110 if let Some(ignore) = entry.get("ignore").and_then(toml::Value::as_bool) {
112 if ignore {
113 config.environments.remove(key);
114 continue;
115 }
116 }
117
118 let name = entry.get("name").and_then(toml::Value::as_str);
119 let emph = entry.get("emph").and_then(toml::Value::as_str);
120
121 if let Some(env) = config.environments.get_mut(key) {
122 if let Some(v) = name {
123 env.name = v.to_string();
124 }
125
126 if let Some(v) = emph {
127 env.emph = v.to_string();
128 }
129 } else {
130 config.environments.insert(
131 String::from(key),
132 Env::create(name.unwrap_or("Environment"), emph.unwrap_or("**")),
133 );
134 }
135 }
136 }
137 }
138
139 config
140 }
141}
142
143impl Preprocessor for NumThmPreprocessor {
144 fn name(&self) -> &str {
145 NAME
146 }
147
148 fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
149 let mut refs: HashMap<String, LabelInfo> = HashMap::new();
151
152 book.for_each_mut(|item: &mut BookItem| {
153 if let BookItem::Chapter(chapter) = item {
154 if !chapter.is_draft_chapter() {
155 let prefix = if self.with_prefix {
157 match &chapter.number {
158 Some(sn) => sn.to_string(),
159 None => String::new(),
160 }
161 } else {
162 String::new()
163 };
164 let path = chapter.path.as_ref().unwrap();
165 chapter.content = find_and_replace_envs(
166 &chapter.content,
167 &prefix,
168 path,
169 &self.environments,
170 &mut refs,
171 );
172 }
173 }
174 });
175
176 book.for_each_mut(|item: &mut BookItem| {
177 if let BookItem::Chapter(chapter) = item {
178 if !chapter.is_draft_chapter() {
179 let path = chapter.path.as_ref().unwrap();
181 chapter.content = find_and_replace_refs(&chapter.content, path, &refs);
182 }
183 }
184 });
185
186 Ok(book)
187 }
188}
189
190fn find_and_replace_envs(
196 s: &str,
197 prefix: &str,
198 path: &Path,
199 envs: &EnvMap,
200 refs: &mut HashMap<String, LabelInfo>,
201) -> String {
202 let mut counter: HashMap<String, u32> = envs.iter().map(|(k, _)| (k.clone(), 0)).collect();
203
204 let keys = envs
205 .keys()
206 .map(String::as_str)
207 .collect::<Vec<&str>>()
208 .join("|");
209 let pattern = format!(
210 r"\{{\{{(?P<key>{})\}}\}}(\{{(?P<label>.*?)\}})?(\[(?P<title>.*?)\])?",
211 keys
212 );
213 let re: Regex = Regex::new(pattern.as_str()).unwrap();
216
217 re.replace_all(s, |caps: ®ex::Captures| {
218 let key = caps.name("key").unwrap().as_str();
220
221 let env = envs.get(key).unwrap();
223 let name = &env.name;
224 let emph = &env.emph;
225 let ctr = counter.get_mut(key).unwrap();
226 *ctr += 1;
227
228 let anchor = match caps.name("label") {
229 Some(match_label) => {
230 let label = match_label.as_str().to_string();
232 if refs.contains_key(&label) {
233 warn!("{name} {prefix}{ctr}: Label `{label}' already used");
235 } else {
236 refs.insert(
237 label.clone(),
238 LabelInfo {
239 num_name: format!("{name} {prefix}{ctr}"),
240 path: path.to_path_buf(),
241 title: caps.name("title").map(|t| t.as_str().to_string()),
242 },
243 );
244 }
245 format!("<a name=\"{label}\"></a>\n")
246 }
247 None => String::new(),
248 };
249 let header = match caps.name("title") {
250 Some(match_title) => {
251 let title = match_title.as_str().to_string();
252 format!("{emph}{name} {prefix}{ctr} ({title}).{emph}")
253 }
254 None => {
255 format!("{emph}{name} {prefix}{ctr}.{emph}")
256 }
257 };
258 format!("{anchor}{header}")
259 })
260 .to_string()
261}
262
263fn find_and_replace_refs(
266 s: &str,
267 chap_path: &PathBuf,
268 refs: &HashMap<String, LabelInfo>,
269) -> String {
270 let re: Regex = Regex::new(r"\{\{(?P<reftype>ref:|tref:)\s*(?P<label>.*?)\}\}").unwrap();
272
273 re.replace_all(s, |caps: ®ex::Captures| {
274 let label = caps.name("label").unwrap().as_str().to_string();
275 if refs.contains_key(&label) {
276 let text = match caps.name("reftype").unwrap().as_str() {
277 "ref:" => &refs.get(&label).unwrap().num_name,
278 _ => {
279 match &refs.get(&label).unwrap().title {
281 Some(t) => t,
282 None => &refs.get(&label).unwrap().num_name,
284 }
285 }
286 };
287 let path_to_ref = &refs.get(&label).unwrap().path;
288 let rel_path = compute_rel_path(chap_path, path_to_ref);
289 format!("[{text}]({rel_path}#{label})")
290 } else {
291 warn!("Unknown reference: {}", label);
292 "**[??]**".to_string()
293 }
294 })
295 .to_string()
296}
297
298fn compute_rel_path(chap_path: &PathBuf, path_to_ref: &PathBuf) -> String {
300 if chap_path == path_to_ref {
301 return "".to_string();
302 }
303 let mut local_chap_path = chap_path.clone();
304 local_chap_path.pop();
305 format!(
306 "{}",
307 diff_paths(path_to_ref, &local_chap_path).unwrap().display()
308 )
309}
310
311#[cfg(test)]
312mod test {
313 use super::*;
314 use lazy_static::lazy_static;
315
316 const SECNUM: &str = "1.2.";
317
318 lazy_static! {
319 static ref ENVMAP: EnvMap = EnvMap::default();
320 static ref PATH: PathBuf = "crypto/groups.md".into();
321 }
322
323 #[test]
324 fn wo_label_wo_title() {
325 let mut refs = HashMap::new();
326 let input = String::from(r"{{prop}}");
327 let output = find_and_replace_envs(&input, SECNUM, &PATH, &ENVMAP, &mut refs);
328 let expected = String::from("**Proposition 1.2.1.**");
329 assert_eq!(output, expected);
330 assert!(refs.is_empty());
331 }
332
333 #[test]
334 fn wo_label_wo_title_replace_default() {
335 let mut env_map = EnvMap::default();
336 env_map.insert(String::from("prop"), Env::create("Proposal", "*"));
337 let mut refs = HashMap::new();
338 let input = String::from(r"{{prop}}");
339 let output = find_and_replace_envs(&input, SECNUM, &PATH, &env_map, &mut refs);
340 let expected = String::from("*Proposal 1.2.1.*");
341 assert_eq!(output, expected);
342 assert!(refs.is_empty());
343 }
344
345 #[test]
346 fn with_label_wo_title() {
347 let mut refs = HashMap::new();
348 let input = String::from(r"{{prop}}{prop:lagrange}");
349 let output = find_and_replace_envs(&input, SECNUM, &PATH, &ENVMAP, &mut refs);
350 let expected = String::from(
351 "<a name=\"prop:lagrange\"></a>\n\
352 **Proposition 1.2.1.**",
353 );
354 assert_eq!(output, expected);
355 assert_eq!(refs.len(), 1);
356 assert_eq!(
357 *refs.get("prop:lagrange").unwrap(),
358 LabelInfo {
359 num_name: "Proposition 1.2.1".to_string(),
360 path: "crypto/groups.md".into(),
361 title: None,
362 }
363 )
364 }
365
366 #[test]
367 fn wo_label_with_title() {
368 let mut refs = HashMap::new();
369 let input = String::from(r"{{prop}}[Lagrange Theorem]");
370 let output = find_and_replace_envs(&input, SECNUM, &PATH, &ENVMAP, &mut refs);
371 let expected = String::from("**Proposition 1.2.1 (Lagrange Theorem).**");
372 assert_eq!(output, expected);
373 assert!(refs.is_empty());
374 }
375
376 #[test]
377 fn with_label_with_title() {
378 let mut refs = HashMap::new();
379 let input = String::from(r"{{prop}}{prop:lagrange}[Lagrange Theorem]");
380 let output = find_and_replace_envs(&input, SECNUM, &PATH, &ENVMAP, &mut refs);
381 let expected = String::from(
382 "<a name=\"prop:lagrange\"></a>\n\
383 **Proposition 1.2.1 (Lagrange Theorem).**",
384 );
385 assert_eq!(output, expected);
386 }
387
388 #[test]
389 fn double_label() {
390 let mut refs = HashMap::new();
391 let input = String::from(
392 r"{{prop}}{prop:lagrange}[Lagrange Theorem] {{thm}}{prop:lagrange}[Another Lagrange Theorem]",
393 );
394 let output = find_and_replace_envs(&input, SECNUM, &PATH, &ENVMAP, &mut refs);
395 let expected = String::from(
396 "<a name=\"prop:lagrange\"></a>\n\
397 **Proposition 1.2.1 (Lagrange Theorem).** \
398 <a name=\"prop:lagrange\"></a>\n\
399 **Theorem 1.2.1 (Another Lagrange Theorem).**",
400 );
401 assert_eq!(output, expected);
402 assert_eq!(refs.len(), 1);
403 }
404
405 #[test]
406 fn label_and_ref_in_same_file() {
407 let mut refs = HashMap::new();
408 let input =
409 String::from(r"{{prop}}{prop:lagrange}[Lagrange Theorem] {{ref: prop:lagrange}}");
410 let output = find_and_replace_envs(&input, SECNUM, &PATH, &ENVMAP, &mut refs);
411 let output = find_and_replace_refs(&output, &PATH, &refs);
412 let expected = String::from(
413 "<a name=\"prop:lagrange\"></a>\n\
414 **Proposition 1.2.1 (Lagrange Theorem).** \
415 [Proposition 1.2.1](#prop:lagrange)",
416 );
417 assert_eq!(output, expected);
418 }
419
420 #[test]
421 fn label_and_ref_in_different_files() {
422 let mut refs = HashMap::new();
423 let label_file: PathBuf = "math/groups.md".into();
424 let ref_file: PathBuf = "crypto/bls_signatures.md".into();
425 let label_input = String::from(r"{{prop}}{prop:lagrange}[Lagrange Theorem]");
426 let ref_input = String::from(r"{{ref: prop:lagrange}}");
427 let _label_output =
428 find_and_replace_envs(&label_input, SECNUM, &label_file, &ENVMAP, &mut refs);
429 let ref_output = find_and_replace_refs(&ref_input, &ref_file, &refs);
430 let expected = String::from("[Proposition 1.2.1](../math/groups.md#prop:lagrange)");
431 assert_eq!(ref_output, expected);
432 }
433
434 #[test]
435 fn label_and_ref_in_different_files_2() {
436 let mut refs = HashMap::new();
437 let label_file: PathBuf = "math/algebra/groups.md".into();
438 let ref_file: PathBuf = "math/crypto//signatures/bls_signatures.md".into();
439 let label_input = String::from(r"{{prop}}{prop:lagrange}[Lagrange Theorem]");
440 let ref_input = String::from(r"{{ref: prop:lagrange}}");
441 let _label_output =
442 find_and_replace_envs(&label_input, SECNUM, &label_file, &ENVMAP, &mut refs);
443 let ref_output = find_and_replace_refs(&ref_input, &ref_file, &refs);
444 let expected = String::from("[Proposition 1.2.1](../../algebra/groups.md#prop:lagrange)");
445 assert_eq!(ref_output, expected);
446 }
447
448 #[test]
449 fn title_ref() {
450 let mut refs = HashMap::new();
451 let label_file: PathBuf = "math/algebra/groups.md".into();
452 let ref_file: PathBuf = "math/crypto//signatures/bls_signatures.md".into();
453 let label_input = String::from(r"{{prop}}{prop:lagrange}[Lagrange Theorem]");
454 let ref_input = String::from(r"{{tref: prop:lagrange}}");
455 let _label_output =
456 find_and_replace_envs(&label_input, SECNUM, &label_file, &ENVMAP, &mut refs);
457 let ref_output = find_and_replace_refs(&ref_input, &ref_file, &refs);
458 let expected = String::from("[Lagrange Theorem](../../algebra/groups.md#prop:lagrange)");
459 assert_eq!(ref_output, expected);
460 }
461
462 #[test]
463 fn title_ref_without_title() {
464 let mut refs = HashMap::new();
465 let label_file: PathBuf = "math/algebra/groups.md".into();
466 let ref_file: PathBuf = "math/crypto//signatures/bls_signatures.md".into();
467 let label_input = String::from(r"{{prop}}{prop:lagrange}");
468 let ref_input = String::from(r"{{tref: prop:lagrange}}");
469 let _label_output =
470 find_and_replace_envs(&label_input, SECNUM, &label_file, &ENVMAP, &mut refs);
471 let ref_output = find_and_replace_refs(&ref_input, &ref_file, &refs);
472 let expected = String::from("[Proposition 1.2.1](../../algebra/groups.md#prop:lagrange)");
473 assert_eq!(ref_output, expected);
474 }
475}