mdbook_mathpunc/
lib.rs

1//! An mdbook preprocessor that prevents line breaks between inline math blocks and punctuation marks when using katex.
2
3use fancy_regex::{Captures, Regex};
4use lazy_static::lazy_static;
5use mdbook::book::{Book, BookItem};
6use mdbook::errors::Result;
7use mdbook::preprocess::{Preprocessor, PreprocessorContext};
8
9/// The preprocessor name.
10const NAME: &str = "mathpunc";
11
12/// The preprocessor.
13pub struct MathpuncPreprocessor;
14
15lazy_static! {
16    /// The regex used for replacement.
17    static ref RE: Regex =
18        // see https://regex101.com/ for an explanation of the regex
19        Regex::new(r"(?<!\\)\$\s*(?<punc>\)?[,,.,;,:,)])").unwrap();
20}
21
22impl MathpuncPreprocessor {
23    pub fn new() -> Self {
24        Self
25    }
26}
27
28impl Preprocessor for MathpuncPreprocessor {
29    fn name(&self) -> &str {
30        NAME
31    }
32
33    fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
34        book.for_each_mut(|item: &mut BookItem| {
35            if let BookItem::Chapter(chapter) = item {
36                chapter.content = find_and_replace(&chapter.content);
37            }
38        });
39
40        Ok(book)
41    }
42}
43
44/// Replaces all occurrences of "$p" in `s`, where p is zero or one closing parenthesis
45/// followed by one of the five punctuation marks {, . ; : )}
46/// (possibly with zero or more white spaces between the dollar sign and p)
47/// by "p$", except if the dollar sign is escaped with a backslash.
48fn find_and_replace(s: &str) -> String {
49    // RE.replace_all(s, "$punc$$").to_string()
50    RE.replace_all(s, |caps: &Captures| {
51        match &caps["punc"] {
52            ":" => {
53                r"\!\!:$".to_string()
54            }
55            "):" => {
56                r")\!\!:$".to_string()
57            }
58            _ => {
59                format!("{}$", &caps["punc"])
60            }
61        }
62    }).to_string()
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn basic() {
71        let input = String::from(
72            r"Consider a group $\GG$, of order $p$; and a generator $G$: for example an elliptic curve $E$.",
73        );
74        let output = find_and_replace(&input);
75        let expected = String::from(
76            r"Consider a group $\GG,$ of order $p;$ and a generator $G\!\!:$ for example an elliptic curve $E.$",
77        );
78        assert_eq!(output, expected);
79    }
80
81    #[test]
82    fn escaped_dollar() {
83        let input = String::from(r"This is an escaped dollar \$, don't replace. This as well \& .");
84        let output = find_and_replace(&input);
85        assert_eq!(output, input);
86    }
87
88    #[test]
89    fn whitespaces() {
90        let input = String::from(
91            r"Consider a group $\GG$  , of order $p$ ; and a generator $G$   : for example an elliptic curve $E$ .",
92        );
93        let output = find_and_replace(&input);
94        let expected = String::from(
95            r"Consider a group $\GG,$ of order $p;$ and a generator $G\!\!:$ for example an elliptic curve $E.$",
96        );
97        assert_eq!(output, expected);
98    }
99
100    #[test]
101    fn parenthesis() {
102        let input =
103            String::from(r"Consider a group $\GG$ (of order $p$), and a generator $G$ (of $\GG$).");
104        let output = find_and_replace(&input);
105        let expected =
106            String::from(r"Consider a group $\GG$ (of order $p),$ and a generator $G$ (of $\GG).$");
107        assert_eq!(output, expected);
108    }
109
110    #[test]
111    fn parenthesis_and_colon() {
112        let input =
113            String::from(r"Consider a group $\GG$ (of order $p$):");
114        let output = find_and_replace(&input);
115        let expected =
116            String::from(r"Consider a group $\GG$ (of order $p)\!\!:$");
117        assert_eq!(output, expected);
118    }
119}