1use log::warn;
4use mdbook_preprocessor::book::{Book, BookItem};
5use mdbook_preprocessor::errors::Result;
6use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
7use pathdiff::diff_paths;
8use regex::Regex;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12pub fn for_each_mut_ordered<'a, F, I>(func: &mut F, items: I)
13where
14 F: FnMut(&mut BookItem),
15 I: IntoIterator<Item = &'a mut BookItem>,
16{
17 for item in items {
18 func(item);
19 if let BookItem::Chapter(ch) = item {
20 for_each_mut_ordered(func, &mut ch.sub_items);
21 }
22 }
23}
24
25const NAME: &str = "numeq";
27
28#[derive(Default)]
30pub struct NumEqPreprocessor {
31 with_prefix: bool,
33 prefix_depth: usize,
34 global: bool,
35}
36
37#[derive(Debug, PartialEq)]
39struct LabelInfo {
40 num: String,
42 path: PathBuf,
44}
45
46impl NumEqPreprocessor {
47 pub fn new(ctx: &PreprocessorContext) -> Self {
48 let mut preprocessor = Self::default();
49
50 if let Ok(Some(b)) = ctx.config.get::<bool>("preprocessor.numeq.prefix") {
51 preprocessor.with_prefix = b;
52 }
53
54 if let Ok(Some(d)) = ctx.config.get::<i32>("preprocessor.numeq.depth") {
55 if d > 0 {
56 preprocessor.prefix_depth = d as usize;
57 }
58 }
59
60 if let Ok(Some(b)) = ctx.config.get::<bool>("preprocessor.numeq.global") {
61 preprocessor.global = b;
62 }
63
64 preprocessor
65 }
66}
67
68impl Preprocessor for NumEqPreprocessor {
69 fn name(&self) -> &str {
70 NAME
71 }
72
73 fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
74 let mut refs: HashMap<String, LabelInfo> = HashMap::new();
76 let mut ctr = 0;
78 let mut ccn: Vec<usize> = vec![1];
81 ccn.resize(self.prefix_depth, 0);
82
83 book.for_each_chapter_mut(|chapter| {
84 let mut prefix = if self.with_prefix {
86 match &chapter.number {
87 Some(sn) => sn.to_string(),
88 None => String::new(),
89 }
90 } else {
91 String::new()
92 };
93 let path = chapter.path.as_ref().unwrap();
94 if !self.global && self.prefix_depth == 0 {
96 ctr = 0;
97 }
98 if self.prefix_depth > 0 {
99 if prefix.is_empty() {
100 ctr = 0;
102 } else {
103 let mut prefix_vec: Vec<usize> = prefix
105 .trim_end_matches('.')
106 .split('.')
107 .map(|s| s.parse::<usize>().unwrap())
108 .collect::<Vec<usize>>();
109 if prefix_vec.len() < self.prefix_depth {
110 prefix_vec.resize(self.prefix_depth, 0);
111 }
112 if ccn[..] != prefix_vec[..self.prefix_depth] {
114 ccn.copy_from_slice(&prefix_vec[..self.prefix_depth]);
115 ctr = 0;
117 }
118 prefix = ccn
120 .iter()
121 .fold(String::new(), |acc, x| acc + &x.to_string() + ".");
122 }
123 }
124 chapter.content =
125 find_and_replace_eqs(&chapter.content, &prefix, path, &mut refs, &mut ctr);
126 });
127
128 book.for_each_chapter_mut(|chapter| {
129 let path = chapter.path.as_ref().unwrap();
131 chapter.content = find_and_replace_refs(&chapter.content, path, &refs);
132 });
133
134 Ok(book)
135 }
136}
137
138fn find_and_replace_eqs(
141 s: &str,
142 prefix: &str,
143 path: &Path,
144 refs: &mut HashMap<String, LabelInfo>,
145 ctr: &mut usize,
146) -> String {
147 let re: Regex = Regex::new(r"\{\{numeq\}\}(\{(?P<label>.*?)\})?").unwrap();
149
150 re.replace_all(s, |caps: ®ex::Captures| {
151 *ctr += 1;
152 match caps.name("label") {
153 Some(lb) => {
154 let label = lb.as_str().to_string();
156 if refs.contains_key(&label) {
157 warn!("Eq. {prefix}{ctr}: Label `{label}' already used");
159 } else {
160 refs.insert(
161 label.clone(),
162 LabelInfo {
163 num: format!("{prefix}{ctr}"),
164 path: path.to_path_buf(),
165 },
166 );
167 }
168 format!("\\htmlId{{{label}}}{{}} \\tag{{{prefix}{ctr}}}")
169 }
170 None => {
171 format!("\\tag{{{prefix}{ctr}}}")
172 }
173 }
174 })
175 .to_string()
176}
177
178fn find_and_replace_refs(
181 s: &str,
182 chap_path: &PathBuf,
183 refs: &HashMap<String, LabelInfo>,
184) -> String {
185 let re: Regex = Regex::new(r"\{\{eqref:\s*(?P<label>.*?)\}\}").unwrap();
187
188 re.replace_all(s, |caps: ®ex::Captures| {
189 let label = caps.name("label").unwrap().as_str().to_string();
190 if refs.contains_key(&label) {
191 let text = &refs.get(&label).unwrap().num;
192 let path_to_ref = &refs.get(&label).unwrap().path;
193 let rel_path = compute_rel_path(chap_path, path_to_ref);
194 format!("[({text})]({rel_path}#{label})")
195 } else {
196 warn!("Unknown equation reference: {}", label);
197 "**[??]**".to_string()
198 }
199 })
200 .to_string()
201}
202
203fn compute_rel_path(chap_path: &PathBuf, path_to_ref: &PathBuf) -> String {
205 if chap_path == path_to_ref {
206 return "".to_string();
207 }
208 let mut local_chap_path = chap_path.clone();
209 local_chap_path.pop();
210 format!(
211 "{}",
212 diff_paths(path_to_ref, &local_chap_path).unwrap().display()
213 )
214}
215
216#[cfg(test)]
217mod test {
218 use super::*;
219 use lazy_static::lazy_static;
220
221 const SECNUM: &str = "1.2.";
222
223 lazy_static! {
224 static ref PATH: PathBuf = "crypto/groups.md".into();
225 }
226
227 #[test]
228 fn no_label() {
229 let mut refs = HashMap::new();
230 let mut ctr = 0;
231 let input = String::from(r"{{numeq}}");
232 let output = find_and_replace_eqs(&input, SECNUM, &PATH, &mut refs, &mut ctr);
233 let expected = String::from("\\tag{1.2.1}");
234 assert_eq!(output, expected);
235 assert!(refs.is_empty());
236 }
237
238 #[test]
239 fn with_label() {
240 let mut refs = HashMap::new();
241 let mut ctr = 0;
242 let input = String::from(r"{{numeq}}{eq:test}");
243 let output = find_and_replace_eqs(&input, SECNUM, &PATH, &mut refs, &mut ctr);
244 let expected = String::from("\\htmlId{eq:test}{} \\tag{1.2.1}");
245 assert_eq!(output, expected);
246 assert_eq!(
247 *refs.get("eq:test").unwrap(),
248 LabelInfo {
249 num: "1.2.1".to_string(),
250 path: "crypto/groups.md".into(),
251 }
252 )
253 }
254}