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
//! An mdbook preprocessor that prevents line breaks between inline math blocks and punctuation marks when using katex.

use fancy_regex::{Captures, Regex};
use lazy_static::lazy_static;
use mdbook::book::{Book, BookItem};
use mdbook::errors::Result;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};

/// The preprocessor name.
const NAME: &str = "mathpunc";

/// The preprocessor.
pub struct MathpuncPreprocessor;

lazy_static! {
    /// The regex used for replacement.
    static ref RE: Regex =
        // see https://regex101.com/ for an explanation of the regex
        Regex::new(r"(?<!\\)\$\s*(?<punc>\)?[,,.,;,:,)])").unwrap();
}

impl MathpuncPreprocessor {
    pub fn new() -> Self {
        Self
    }
}

impl Preprocessor for MathpuncPreprocessor {
    fn name(&self) -> &str {
        NAME
    }

    fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
        book.for_each_mut(|item: &mut BookItem| {
            if let BookItem::Chapter(chapter) = item {
                chapter.content = find_and_replace(&chapter.content);
            }
        });

        Ok(book)
    }
}

/// Replaces all occurrences of "$p" in `s`, where p is zero or one closing parenthesis
/// followed by one of the five punctuation marks {, . ; : )}
/// (possibly with zero or more white spaces between the dollar sign and p)
/// by "p$", except if the dollar sign is escaped with a backslash.
fn find_and_replace(s: &str) -> String {
    // RE.replace_all(s, "$punc$$").to_string()
    RE.replace_all(s, |caps: &Captures| {
        match &caps["punc"] {
            ":" => {
                r"\!\!:$".to_string()
            }
            "):" => {
                r")\!\!:$".to_string()
            }
            _ => {
                format!("{}$", &caps["punc"])
            }
        }
    }).to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn basic() {
        let input = String::from(
            r"Consider a group $\GG$, of order $p$; and a generator $G$: for example an elliptic curve $E$.",
        );
        let output = find_and_replace(&input);
        let expected = String::from(
            r"Consider a group $\GG,$ of order $p;$ and a generator $G\!\!:$ for example an elliptic curve $E.$",
        );
        assert_eq!(output, expected);
    }

    #[test]
    fn escaped_dollar() {
        let input = String::from(r"This is an escaped dollar \$, don't replace. This as well \& .");
        let output = find_and_replace(&input);
        assert_eq!(output, input);
    }

    #[test]
    fn whitespaces() {
        let input = String::from(
            r"Consider a group $\GG$  , of order $p$ ; and a generator $G$   : for example an elliptic curve $E$ .",
        );
        let output = find_and_replace(&input);
        let expected = String::from(
            r"Consider a group $\GG,$ of order $p;$ and a generator $G\!\!:$ for example an elliptic curve $E.$",
        );
        assert_eq!(output, expected);
    }

    #[test]
    fn parenthesis() {
        let input =
            String::from(r"Consider a group $\GG$ (of order $p$), and a generator $G$ (of $\GG$).");
        let output = find_and_replace(&input);
        let expected =
            String::from(r"Consider a group $\GG$ (of order $p),$ and a generator $G$ (of $\GG).$");
        assert_eq!(output, expected);
    }

    #[test]
    fn parenthesis_and_colon() {
        let input =
            String::from(r"Consider a group $\GG$ (of order $p$):");
        let output = find_and_replace(&input);
        let expected =
            String::from(r"Consider a group $\GG$ (of order $p)\!\!:$");
        assert_eq!(output, expected);
    }
}