latex_rust/parser/
preproc.rs1#[must_use]
22pub fn preprocess(raw_input: &str) -> String {
23 let input = raw_input.trim().to_string();
24 if input.is_empty() {
25 return input;
26 }
27
28 let mut input = input;
29 input = input.replace(r"{ (}", "(").replace(r"{ )}", ")");
30 input = input.replace(r"{ ( }", "(").replace(r"{ ) }", ")");
31 input = input.replace(r"{[}", "[").replace(r"{]}", "]");
32
33 input = input
34 .replace(r"\tfrac", r"\frac")
35 .replace(r"\dfrac", r"\frac")
36 .replace(r"\cfrac", r"\frac");
37
38 input = convert_plain_tex_font_scopes(&input);
39 input = convert_over_fractions(&input);
40 input = convert_choose(&input);
41 input = input.replace(r"\mbox{", r"\text{");
42 input
43}
44
45fn convert_plain_tex_font_scopes(input: &str) -> String {
46 let map = [
47 (r"\rm", r"\mathrm"),
48 (r"\bf", r"\mathbf"),
49 (r"\cal", r"\mathcal"),
50 (r"\it", r"\mathit"),
51 (r"\sf", r"\mathsf"),
52 (r"\tt", r"\mathtt"),
53 ];
54 let mut result = input.to_string();
55 for (old, new) in map {
56 let mut search = 0;
57 while let Some(rel) = result[search..].find(old) {
58 let abs = search + rel;
59 let body = abs + old.len();
60 if abs > 0 && result.as_bytes()[abs - 1] == b'{' {
61 if let Some(close) = result[abs..].find('}') {
62 let end = abs + close;
63 let content = result[body..end].trim();
64 let rep = format!("{new}{{{content}}}");
65 result.replace_range((abs - 1)..=end, &rep);
66 search = abs - 1 + rep.len();
67 continue;
68 }
69 }
70 search = abs + old.len();
71 }
72 }
73 result
74}
75
76fn convert_over_fractions(input: &str) -> String {
77 let mut result = input.to_string();
78 while let Some(over) = result.find(r"\over") {
79 let mut open = None;
80 let mut depth = 0;
81 for (i, ch) in result[..over].char_indices().rev() {
82 match ch {
83 '}' => depth += 1,
84 '{' => {
85 if depth == 0 {
86 open = Some(i);
87 break;
88 }
89 depth -= 1;
90 }
91 _ => {}
92 }
93 }
94 let mut close = None;
95 depth = 0;
96 for (i, ch) in result[over + 5..].char_indices() {
97 match ch {
98 '{' => depth += 1,
99 '}' => {
100 if depth == 0 {
101 close = Some(over + 5 + i);
102 break;
103 }
104 depth -= 1;
105 }
106 _ => {}
107 }
108 }
109 if let (Some(s), Some(e)) = (open, close) {
110 let num = result[s + 1..over].trim();
111 let den = result[over + 5..e].trim();
112 let rep = format!(r"\frac{{{num}}}{{{den}}}");
113 result.replace_range(s..=e, &rep);
114 } else {
115 break;
116 }
117 }
118 result
119}
120
121fn convert_choose(input: &str) -> String {
123 let mut result = input.to_string();
124 while let Some(ch) = result.find(r"\choose") {
125 let mut open = None;
126 let mut depth = 0;
127 for (i, c) in result[..ch].char_indices().rev() {
128 match c {
129 '}' => depth += 1,
130 '{' => {
131 if depth == 0 {
132 open = Some(i);
133 break;
134 }
135 depth -= 1;
136 }
137 _ => {}
138 }
139 }
140 let mut close = None;
141 depth = 0;
142 for (i, c) in result[ch + 7..].char_indices() {
143 match c {
144 '{' => depth += 1,
145 '}' => {
146 if depth == 0 {
147 close = Some(ch + 7 + i);
148 break;
149 }
150 depth -= 1;
151 }
152 _ => {}
153 }
154 }
155 if let (Some(s), Some(e)) = (open, close) {
156 let n = result[s + 1..ch].trim();
157 let k = result[ch + 7..e].trim();
158 let rep = format!(r"\binom{{{n}}}{{{k}}}");
159 result.replace_range(s..=e, &rep);
160 } else {
161 break;
162 }
163 }
164 result
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn left_right_kept() {
173 let s = preprocess(r"\left(\frac{1}{2}\right)");
174 assert!(s.contains(r"\left"));
175 assert!(s.contains(r"\right"));
176 }
177
178 #[test]
179 fn tfrac_becomes_frac() {
180 assert_eq!(preprocess(r"\tfrac{a}{b}"), r"\frac{a}{b}");
181 }
182
183 #[test]
184 fn over_becomes_frac() {
185 assert_eq!(preprocess(r"{a \over b}"), r"\frac{a}{b}");
186 }
187
188 #[test]
189 fn trailing_period_kept() {
190 assert_eq!(preprocess("x^2."), "x^2.");
191 }
192
193 #[test]
194 fn empty() {
195 assert_eq!(preprocess(" "), "");
196 }
197
198 #[test]
199 fn overline_kept() {
200 assert_eq!(preprocess(r"\overline{z}"), r"\overline{z}");
201 }
202
203 #[test]
204 fn choose_becomes_binom() {
205 assert_eq!(preprocess(r"{n \choose k}"), r"\binom{n}{k}");
206 }
207
208 #[test]
209 fn mbox_becomes_text() {
210 assert_eq!(preprocess(r"\mbox{Diagonal}"), r"\text{Diagonal}");
211 }
212
213 #[test]
214 fn spacing_kept() {
215 let s = preprocess(r"a\,b");
216 assert!(s.contains(r"\,"));
217 }
218}