1use std::path::Path;
4
5use quick_xml::Reader;
6use quick_xml::events::Event;
7
8use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
9use crate::error::{Error, Result};
10use crate::ir::{
11 IDENTITY, LineCap, LineJoin, Node, Page, Paint, SourceMeta, Stroke, TextAnchor, TextRun,
12};
13
14#[derive(Clone, Debug)]
15pub enum MathNode {
16 Text(String),
17 Superscript(Box<MathNode>),
18 Subscript(Box<MathNode>),
19 Fraction {
20 numerator: Box<MathNode>,
21 denominator: Box<MathNode>,
22 },
23 Sqrt(Box<MathNode>),
24 Matrix {
25 rows: Vec<Vec<MathNode>>,
26 delimiters: Option<(char, char)>,
27 },
28 Row(Vec<MathNode>),
29}
30
31pub(crate) fn convert(
32 path: &Path,
33 options: &ConvertOptions,
34 sink: &mut dyn PageConsumer,
35) -> Result<Vec<String>> {
36 let bytes = read_limited_file(path, options.max_input_bytes, "LaTeX math input")?;
37 let source = String::from_utf8(bytes)
38 .map_err(|e| Error::InvalidInput(format!("math file is not valid UTF-8: {e}")))?;
39
40 if crate::document::latex::looks_like_document(&source) {
41 return crate::document::latex::convert_source(path, &source, options, sink);
42 }
43
44 let mut clean_math = source
45 .trim()
46 .trim_start_matches('$')
47 .trim_end_matches('$')
48 .trim_start_matches("\\[")
49 .trim_end_matches("\\]")
50 .trim();
51
52 if clean_math.starts_with("\\begin{equation}") && clean_math.ends_with("\\end{equation}") {
53 clean_math = clean_math
54 .strip_prefix("\\begin{equation}")
55 .unwrap()
56 .strip_suffix("\\end{equation}")
57 .unwrap()
58 .trim();
59 } else if clean_math.starts_with("\\begin{equation*}")
60 && clean_math.ends_with("\\end{equation*}")
61 {
62 clean_math = clean_math
63 .strip_prefix("\\begin{equation*}")
64 .unwrap()
65 .strip_suffix("\\end{equation*}")
66 .unwrap()
67 .trim();
68 }
69
70 let ast = parse_math_expression(clean_math, 0)?;
71 let page = layout_and_render_math(&ast, clean_math, options)?;
72 sink.consume(page)?;
73 Ok(Vec::new())
74}
75
76pub fn parse_math_expression(input: &str, depth: usize) -> Result<MathNode> {
77 if depth > 256 {
78 return Err(Error::LimitExceeded(
79 "math recursion depth limit exceeded".into(),
80 ));
81 }
82 let mut nodes = Vec::new();
83 let mut chars = input.chars().peekable();
84
85 while let Some(&c) = chars.peek() {
86 if c.is_whitespace() {
87 chars.next();
88 } else if c == '\\' {
89 chars.next();
90 if let Some(&next_ch) = chars.peek()
91 && !next_ch.is_alphabetic()
92 {
93 chars.next();
94 match next_ch {
95 '{' => nodes.push(MathNode::Text("{".to_string())),
96 '}' => nodes.push(MathNode::Text("}".to_string())),
97 '\\' => nodes.push(MathNode::Text(" ".to_string())),
98 ',' | ';' | ':' | ' ' => nodes.push(MathNode::Text(" ".to_string())),
99 '!' => {} '%' => nodes.push(MathNode::Text("%".to_string())),
101 '&' => nodes.push(MathNode::Text("&".to_string())),
102 '_' => nodes.push(MathNode::Text("_".to_string())),
103 '$' => nodes.push(MathNode::Text("$".to_string())),
104 _ => nodes.push(MathNode::Text(next_ch.to_string())),
105 }
106 continue;
107 }
108 let mut command = String::new();
109 while let Some(&cmd_ch) = chars.peek() {
110 if cmd_ch.is_alphabetic() {
111 command.push(cmd_ch);
112 chars.next();
113 } else {
114 break;
115 }
116 }
117 match command.as_str() {
118 "frac" => {
119 let num = parse_group(&mut chars);
120 let den = parse_group(&mut chars);
121 nodes.push(MathNode::Fraction {
122 numerator: Box::new(parse_math_expression(&num, depth + 1)?),
123 denominator: Box::new(parse_math_expression(&den, depth + 1)?),
124 });
125 }
126 "sqrt" => {
127 while let Some(&c) = chars.peek() {
128 if c.is_whitespace() {
129 chars.next();
130 } else {
131 break;
132 }
133 }
134 if chars.peek() == Some(&'[') {
135 chars.next();
136 for c in chars.by_ref() {
137 if c == ']' {
138 break;
139 }
140 }
141 }
142 let inner = parse_group(&mut chars);
143 nodes.push(MathNode::Sqrt(Box::new(parse_math_expression(
144 &inner,
145 depth + 1,
146 )?)));
147 }
148 "vec" => {
149 let inner = parse_group(&mut chars);
150 nodes.push(MathNode::Text(format!("{inner}\u{20d7}")));
151 }
152 "hat" => {
153 let inner = parse_group(&mut chars);
154 nodes.push(MathNode::Text(format!("{inner}\u{0302}")));
155 }
156 "bar" | "overline" => {
157 let inner = parse_group(&mut chars);
158 nodes.push(MathNode::Text(format!("{inner}\u{0304}")));
159 }
160 "tilde" => {
161 let inner = parse_group(&mut chars);
162 nodes.push(MathNode::Text(format!("{inner}\u{0303}")));
163 }
164 "dot" => {
165 let inner = parse_group(&mut chars);
166 nodes.push(MathNode::Text(format!("{inner}\u{0307}")));
167 }
168 "ddot" => {
169 let inner = parse_group(&mut chars);
170 nodes.push(MathNode::Text(format!("{inner}\u{0308}")));
171 }
172 "mathbb" => {
173 let inner = parse_group(&mut chars);
174 let bb = match inner.as_str() {
175 "R" => "ℝ",
176 "C" => "ℂ",
177 "N" => "ℕ",
178 "Z" => "ℤ",
179 "Q" => "ℚ",
180 "P" => "ℙ",
181 "H" => "ℍ",
182 _ => &inner,
183 };
184 nodes.push(MathNode::Text(bb.to_string()));
185 }
186 "text" | "mathrm" | "mathbf" | "mathit" | "operatorname" | "bm" => {
187 let inner = parse_group(&mut chars);
188 nodes.push(MathNode::Text(inner));
189 }
190 "mathcal" | "mathscr" => {
191 let inner = parse_group(&mut chars);
192 let mapped: String = inner
193 .chars()
194 .map(|ch| match ch {
195 'A' => '𝒜',
196 'B' => 'ℬ',
197 'C' => '𝒞',
198 'D' => '𝒟',
199 'E' => 'ℰ',
200 'F' => 'ℱ',
201 'G' => '𝒢',
202 'H' => 'ℋ',
203 'I' => 'ℐ',
204 'J' => '𝒥',
205 'K' => '𝒦',
206 'L' => 'ℒ',
207 'M' => 'ℳ',
208 'N' => '𝒩',
209 'O' => '𝒪',
210 'P' => '𝒫',
211 'Q' => '𝒬',
212 'R' => 'ℛ',
213 'S' => '𝒮',
214 'T' => '𝒯',
215 'U' => '𝒰',
216 'V' => '𝒱',
217 'W' => '𝒲',
218 'X' => '𝒳',
219 'Y' => '𝒴',
220 'Z' => '𝒵',
221 'a' => '𝒶',
222 'b' => '𝒷',
223 'c' => '𝒸',
224 'd' => '𝒹',
225 'e' => 'ℯ',
226 'f' => '𝒻',
227 'g' => 'ℊ',
228 'h' => '𝒽',
229 'i' => '𝒾',
230 'j' => '𝒿',
231 'k' => '𝓀',
232 'l' => '𝓁',
233 'm' => '𝓂',
234 'n' => '𝓃',
235 'o' => 'ℴ',
236 'p' => '𝓅',
237 'q' => '𝓆',
238 'r' => '𝓇',
239 's' => '𝓈',
240 't' => '𝓉',
241 'u' => '𝓊',
242 'v' => '𝓋',
243 'w' => '𝓌',
244 'x' => '𝓍',
245 'y' => '𝓎',
246 'z' => '𝓏',
247 other => other,
248 })
249 .collect();
250 nodes.push(MathNode::Text(mapped));
251 }
252 "mathsf" | "mathtt" | "mathfrak" | "frak" => {
253 let inner = parse_group(&mut chars);
254 nodes.push(MathNode::Text(inner));
255 }
256 "left" | "right" => {
257 while let Some(&nc) = chars.peek() {
258 if nc.is_whitespace() {
259 chars.next();
260 } else {
261 break;
262 }
263 }
264 if chars.peek() == Some(&'.') {
265 chars.next(); }
267 }
268 "sum" => nodes.push(MathNode::Text("∑".to_string())),
269 "int" => nodes.push(MathNode::Text("∫".to_string())),
270 "times" => nodes.push(MathNode::Text("×".to_string())),
271 "div" => nodes.push(MathNode::Text("÷".to_string())),
272 "pm" => nodes.push(MathNode::Text("±".to_string())),
273 "mp" => nodes.push(MathNode::Text("∓".to_string())),
274 "alpha" => nodes.push(MathNode::Text("α".to_string())),
275 "beta" => nodes.push(MathNode::Text("β".to_string())),
276 "gamma" => nodes.push(MathNode::Text("γ".to_string())),
277 "Gamma" => nodes.push(MathNode::Text("Γ".to_string())),
278 "delta" => nodes.push(MathNode::Text("δ".to_string())),
279 "Delta" => nodes.push(MathNode::Text("Δ".to_string())),
280 "epsilon" | "varepsilon" => nodes.push(MathNode::Text("ε".to_string())),
281 "zeta" => nodes.push(MathNode::Text("ζ".to_string())),
282 "eta" => nodes.push(MathNode::Text("η".to_string())),
283 "theta" => nodes.push(MathNode::Text("θ".to_string())),
284 "Theta" => nodes.push(MathNode::Text("Θ".to_string())),
285 "iota" => nodes.push(MathNode::Text("ι".to_string())),
286 "kappa" => nodes.push(MathNode::Text("κ".to_string())),
287 "lambda" => nodes.push(MathNode::Text("λ".to_string())),
288 "Lambda" => nodes.push(MathNode::Text("Λ".to_string())),
289 "mu" => nodes.push(MathNode::Text("μ".to_string())),
290 "nu" => nodes.push(MathNode::Text("ν".to_string())),
291 "xi" => nodes.push(MathNode::Text("ξ".to_string())),
292 "Xi" => nodes.push(MathNode::Text("Ξ".to_string())),
293 "pi" => nodes.push(MathNode::Text("π".to_string())),
294 "Pi" => nodes.push(MathNode::Text("Π".to_string())),
295 "rho" | "varrho" => nodes.push(MathNode::Text("ρ".to_string())),
296 "sigma" => nodes.push(MathNode::Text("σ".to_string())),
297 "Sigma" => nodes.push(MathNode::Text("Σ".to_string())),
298 "tau" => nodes.push(MathNode::Text("τ".to_string())),
299 "upsilon" => nodes.push(MathNode::Text("υ".to_string())),
300 "Upsilon" => nodes.push(MathNode::Text("Υ".to_string())),
301 "phi" | "varphi" => nodes.push(MathNode::Text("φ".to_string())),
302 "Phi" => nodes.push(MathNode::Text("Φ".to_string())),
303 "chi" => nodes.push(MathNode::Text("χ".to_string())),
304 "psi" => nodes.push(MathNode::Text("ψ".to_string())),
305 "Psi" => nodes.push(MathNode::Text("Ψ".to_string())),
306 "omega" => nodes.push(MathNode::Text("ω".to_string())),
307 "Omega" => nodes.push(MathNode::Text("Ω".to_string())),
308 "partial" => nodes.push(MathNode::Text("∂".to_string())),
309 "nabla" => nodes.push(MathNode::Text("∇".to_string())),
310 "approx" => nodes.push(MathNode::Text("≈".to_string())),
311 "neq" | "ne" => nodes.push(MathNode::Text("≠".to_string())),
312 "leq" | "le" => nodes.push(MathNode::Text("≤".to_string())),
313 "geq" | "ge" => nodes.push(MathNode::Text("≥".to_string())),
314 "cdot" => nodes.push(MathNode::Text("·".to_string())),
315 "infty" => nodes.push(MathNode::Text("∞".to_string())),
316 "in" => nodes.push(MathNode::Text("∈".to_string())),
317 "notin" => nodes.push(MathNode::Text("∉".to_string())),
318 "subset" => nodes.push(MathNode::Text("⊂".to_string())),
319 "subseteq" => nodes.push(MathNode::Text("⊆".to_string())),
320 "supset" => nodes.push(MathNode::Text("⊃".to_string())),
321 "supseteq" => nodes.push(MathNode::Text("⊇".to_string())),
322 "equiv" => nodes.push(MathNode::Text("≡".to_string())),
323 "sim" => nodes.push(MathNode::Text("∼".to_string())),
324 "propto" => nodes.push(MathNode::Text("∝".to_string())),
325 "land" | "wedge" => nodes.push(MathNode::Text("∧".to_string())),
326 "lor" | "vee" => nodes.push(MathNode::Text("∨".to_string())),
327 "oplus" => nodes.push(MathNode::Text("⊕".to_string())),
328 "otimes" => nodes.push(MathNode::Text("⊗".to_string())),
329 "odot" => nodes.push(MathNode::Text("⊙".to_string())),
330 "circ" => nodes.push(MathNode::Text("∘".to_string())),
331 "bullet" => nodes.push(MathNode::Text("•".to_string())),
332 "dots" | "ldots" | "cdots" => nodes.push(MathNode::Text("…".to_string())),
333 "degree" => nodes.push(MathNode::Text("°".to_string())),
334 "cap" => nodes.push(MathNode::Text("∩".to_string())),
335 "cup" => nodes.push(MathNode::Text("∪".to_string())),
336 "forall" => nodes.push(MathNode::Text("∀".to_string())),
337 "exists" => nodes.push(MathNode::Text("∃".to_string())),
338 "neg" => nodes.push(MathNode::Text("¬".to_string())),
339 "to" | "rightarrow" => nodes.push(MathNode::Text("→".to_string())),
340 "leftarrow" | "gets" => nodes.push(MathNode::Text("←".to_string())),
341 "leftrightarrow" => nodes.push(MathNode::Text("↔".to_string())),
342 "uparrow" => nodes.push(MathNode::Text("↑".to_string())),
343 "downarrow" => nodes.push(MathNode::Text("↓".to_string())),
344 "parallel" => nodes.push(MathNode::Text("∥".to_string())),
345 "perp" => nodes.push(MathNode::Text("⊥".to_string())),
346 "quad" => nodes.push(MathNode::Text(" ".to_string())),
347 "qquad" => nodes.push(MathNode::Text(" ".to_string())),
348 "Rightarrow" | "implies" => nodes.push(MathNode::Text("⇒".to_string())),
349 "Leftarrow" | "impliedby" => nodes.push(MathNode::Text("⇐".to_string())),
350 "iff" | "Leftrightarrow" => nodes.push(MathNode::Text("⇔".to_string())),
351 "emptyset" | "varnothing" => nodes.push(MathNode::Text("∅".to_string())),
352 "nexists" => nodes.push(MathNode::Text("∄".to_string())),
353 "ni" | "owns" => nodes.push(MathNode::Text("∋".to_string())),
354 "top" => nodes.push(MathNode::Text("⊤".to_string())),
355 "bot" => nodes.push(MathNode::Text("⊥".to_string())),
356 "ominus" => nodes.push(MathNode::Text("⊖".to_string())),
357 "oslash" => nodes.push(MathNode::Text("⊘".to_string())),
358 "setminus" => nodes.push(MathNode::Text("∖".to_string())),
359 "mid" => nodes.push(MathNode::Text("∣".to_string())),
360 "nmid" => nodes.push(MathNode::Text("∤".to_string())),
361 "simeq" => nodes.push(MathNode::Text("≃".to_string())),
362 "cong" => nodes.push(MathNode::Text("≅".to_string())),
363 "asymp" => nodes.push(MathNode::Text("≍".to_string())),
364 "doteq" => nodes.push(MathNode::Text("≐".to_string())),
365 "aleph" => nodes.push(MathNode::Text("ℵ".to_string())),
366 "wp" => nodes.push(MathNode::Text("℘".to_string())),
367 "prod" => nodes.push(MathNode::Text("∏".to_string())),
368 "coprod" => nodes.push(MathNode::Text("∐".to_string())),
369 "oint" => nodes.push(MathNode::Text("∮".to_string())),
370 "iint" => nodes.push(MathNode::Text("∬".to_string())),
371 "iiint" => nodes.push(MathNode::Text("∭".to_string())),
372 "hbar" => nodes.push(MathNode::Text("ħ".to_string())),
373 "ell" => nodes.push(MathNode::Text("ℓ".to_string())),
374 "Re" => nodes.push(MathNode::Text("ℜ".to_string())),
375 "Im" => nodes.push(MathNode::Text("ℑ".to_string())),
376 "prime" => nodes.push(MathNode::Text("′".to_string())),
377 "dagger" => nodes.push(MathNode::Text("†".to_string())),
378 "ddagger" => nodes.push(MathNode::Text("‡".to_string())),
379 "angle" => nodes.push(MathNode::Text("∠".to_string())),
380 "longrightarrow" => nodes.push(MathNode::Text("⟶".to_string())),
381 "longleftarrow" => nodes.push(MathNode::Text("⟵".to_string())),
382 "Longrightarrow" => nodes.push(MathNode::Text("⟹".to_string())),
383 "Longleftrightarrow" => nodes.push(MathNode::Text("⟺".to_string())),
384 "sin" | "cos" | "tan" | "log" | "ln" | "exp" | "lim" | "max" | "min" | "det"
385 | "arcsin" | "arccos" | "arctan" | "sinh" | "cosh" | "tanh" | "dim" | "ker"
386 | "gcd" | "deg" | "sup" | "inf" | "sec" | "csc" | "cot" | "hom" | "arg" => {
387 nodes.push(MathNode::Text(command));
388 }
389 "begin" => {
390 let env_name = parse_group(&mut chars);
391 if env_name == "array" {
392 let _ = parse_group(&mut chars);
393 }
394 let body = parse_environment_body(&mut chars, &env_name);
395 let delimiters = match env_name.as_str() {
396 "pmatrix" => Some(('(', ')')),
397 "bmatrix" => Some(('[', ']')),
398 "Bmatrix" => Some(('{', '}')),
399 "vmatrix" => Some(('|', '|')),
400 "Vmatrix" => Some(('‖', '‖')),
401 "cases" => Some(('{', '.')),
402 "aligned" | "align" | "align*" | "split" | "gather" | "gather*" => {
403 Some(('.', '.'))
404 }
405 _ => None,
406 };
407 let matrix_rows = parse_matrix_body(&body, depth + 1)?;
408 nodes.push(MathNode::Matrix {
409 rows: matrix_rows,
410 delimiters,
411 });
412 }
413 "end" => {
414 let _ = parse_group(&mut chars);
415 }
416 "binom" => {
417 let top = parse_group(&mut chars);
418 let bot = parse_group(&mut chars);
419 let r1 = vec![parse_math_expression(&top, depth + 1)?];
420 let r2 = vec![parse_math_expression(&bot, depth + 1)?];
421 nodes.push(MathNode::Matrix {
422 rows: vec![r1, r2],
423 delimiters: Some(('(', ')')),
424 });
425 }
426 "limits" | "nolimits" | "displaystyle" | "textstyle" | "notag" | "hline"
427 | "nonumber" => {}
428 "label" => {
429 let _ = parse_group(&mut chars);
430 }
431 "vdots" => nodes.push(MathNode::Text("⋮".to_string())),
432 "ddots" => nodes.push(MathNode::Text("⋱".to_string())),
433 "hdots" => nodes.push(MathNode::Text("…".to_string())),
434 _ => nodes.push(MathNode::Text(command)),
435 }
436 } else if c == '{' {
437 let inner = parse_group(&mut chars);
438 nodes.push(parse_math_expression(&inner, depth + 1)?);
439 } else if c == '}' {
440 chars.next();
441 } else if c == '^' {
442 chars.next();
443 let sup_text = parse_single_or_group(&mut chars);
444 nodes.push(MathNode::Superscript(Box::new(parse_math_expression(
445 &sup_text,
446 depth + 1,
447 )?)));
448 } else if c == '_' {
449 chars.next();
450 let sub_text = parse_single_or_group(&mut chars);
451 nodes.push(MathNode::Subscript(Box::new(parse_math_expression(
452 &sub_text,
453 depth + 1,
454 )?)));
455 } else {
456 nodes.push(MathNode::Text(c.to_string()));
457 chars.next();
458 }
459 }
460
461 if nodes.len() == 1 {
462 Ok(nodes.remove(0))
463 } else {
464 Ok(MathNode::Row(nodes))
465 }
466}
467
468fn parse_group(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
469 while let Some(&c) = chars.peek() {
470 if c.is_whitespace() {
471 chars.next();
472 } else {
473 break;
474 }
475 }
476 if chars.peek() == Some(&'{') {
477 chars.next();
478 let mut depth = 1;
479 let mut content = String::new();
480 for c in chars.by_ref() {
481 if c == '{' {
482 depth += 1;
483 content.push(c);
484 } else if c == '}' {
485 depth -= 1;
486 if depth == 0 {
487 break;
488 }
489 content.push(c);
490 } else {
491 content.push(c);
492 }
493 }
494 content
495 } else if let Some(c) = chars.next() {
496 c.to_string()
497 } else {
498 String::new()
499 }
500}
501
502fn parse_single_or_group(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
503 while let Some(&c) = chars.peek() {
504 if c.is_whitespace() {
505 chars.next();
506 } else {
507 break;
508 }
509 }
510 if chars.peek() == Some(&'{') {
511 parse_group(chars)
512 } else if chars.peek() == Some(&'\\') {
513 chars.next();
514 let mut cmd = String::from('\\');
515 while let Some(&nc) = chars.peek() {
516 if nc.is_alphabetic() {
517 cmd.push(nc);
518 chars.next();
519 } else {
520 break;
521 }
522 }
523 cmd
524 } else if let Some(c) = chars.next() {
525 c.to_string()
526 } else {
527 String::new()
528 }
529}
530
531fn parse_environment_body(
532 chars: &mut std::iter::Peekable<std::str::Chars>,
533 env_name: &str,
534) -> String {
535 let mut body = String::new();
536 let mut depth = 1;
537
538 while let Some(c) = chars.next() {
539 if c == '\\' {
540 let mut cmd = String::new();
541 while let Some(&nc) = chars.peek() {
542 if nc.is_alphabetic() {
543 cmd.push(nc);
544 chars.next();
545 } else {
546 break;
547 }
548 }
549 if cmd == "begin" {
550 let name = parse_group(chars);
551 if name == env_name {
552 depth += 1;
553 }
554 body.push_str(&format!("\\begin{{{name}}}"));
555 } else if cmd == "end" {
556 let name = parse_group(chars);
557 if name == env_name {
558 depth -= 1;
559 if depth == 0 {
560 break;
561 }
562 }
563 body.push_str(&format!("\\end{{{name}}}"));
564 } else {
565 body.push('\\');
566 body.push_str(&cmd);
567 }
568 } else {
569 body.push(c);
570 }
571 }
572 body
573}
574
575fn parse_matrix_body(body: &str, depth: usize) -> Result<Vec<Vec<MathNode>>> {
576 let mut rows = Vec::new();
577 let mut current_row_str = String::new();
578 let mut chars = body.chars().peekable();
579
580 let mut row_strings = Vec::new();
581 let mut brace_depth: usize = 0;
582 let mut env_depth: usize = 0;
583
584 while let Some(c) = chars.next() {
585 if c == '{' {
586 brace_depth += 1;
587 current_row_str.push(c);
588 } else if c == '}' {
589 brace_depth = brace_depth.saturating_sub(1);
590 current_row_str.push(c);
591 } else if c == '\\' {
592 if brace_depth == 0 && env_depth == 0 && chars.peek() == Some(&'\\') {
593 chars.next(); row_strings.push(std::mem::take(&mut current_row_str));
595 continue;
596 }
597 current_row_str.push(c);
598 let mut cmd = String::new();
599 while let Some(&nc) = chars.peek() {
600 if nc.is_alphabetic() || nc == '*' {
601 cmd.push(nc);
602 current_row_str.push(nc);
603 chars.next();
604 } else {
605 break;
606 }
607 }
608 if cmd == "begin" {
609 env_depth += 1;
610 } else if cmd == "end" {
611 env_depth = env_depth.saturating_sub(1);
612 }
613 } else {
614 current_row_str.push(c);
615 }
616 }
617 if !current_row_str.trim().is_empty() || row_strings.is_empty() {
618 row_strings.push(current_row_str);
619 }
620
621 for r_str in row_strings {
622 let trimmed_r = r_str.trim();
623 if trimmed_r.is_empty() {
624 continue;
625 }
626 let mut cells = Vec::new();
627 let mut cell_str = String::new();
628 let mut b_depth: usize = 0;
629 let mut e_depth: usize = 0;
630 let mut chars_r = trimmed_r.chars().peekable();
631 while let Some(c) = chars_r.next() {
632 if c == '{' {
633 b_depth += 1;
634 cell_str.push(c);
635 } else if c == '}' {
636 b_depth = b_depth.saturating_sub(1);
637 cell_str.push(c);
638 } else if c == '\\' {
639 cell_str.push(c);
640 let mut cmd = String::new();
641 while let Some(&nc) = chars_r.peek() {
642 if nc.is_alphabetic() || nc == '*' {
643 cmd.push(nc);
644 cell_str.push(nc);
645 chars_r.next();
646 } else {
647 break;
648 }
649 }
650 if cmd == "begin" {
651 e_depth += 1;
652 } else if cmd == "end" {
653 e_depth = e_depth.saturating_sub(1);
654 }
655 } else if c == '&' && b_depth == 0 && e_depth == 0 {
656 let parsed = parse_math_expression(cell_str.trim(), depth)?;
657 cells.push(parsed);
658 cell_str.clear();
659 } else {
660 cell_str.push(c);
661 }
662 }
663 let parsed = parse_math_expression(cell_str.trim(), depth)?;
664 cells.push(parsed);
665 rows.push(cells);
666 }
667
668 Ok(rows)
669}
670
671struct LayoutBox {
672 width: f64,
673 height_above: f64,
674 height_below: f64,
675}
676
677pub fn layout_and_render_math(
678 ast: &MathNode,
679 raw_tex: &str,
680 _options: &ConvertOptions,
681) -> Result<Page> {
682 let mut page_nodes = Vec::new();
683 let base_font_size = 22.0;
684
685 let base_y = 60.0;
686 let base_x = 40.0;
687
688 let box_dims = render_node_recursive(ast, base_x, base_y, base_font_size, &mut page_nodes);
689
690 let page_width = (base_x + box_dims.width + 40.0).max(160.0);
691 let page_height = (box_dims.height_above + box_dims.height_below + 80.0).max(100.0);
692
693 let mut page = Page::new(1, page_width, page_height, "latex_math");
694 page.embedded_source = Some(raw_tex.to_string());
695 page.nodes = page_nodes;
696
697 Ok(page)
698}
699
700fn render_node_recursive(
701 node: &MathNode,
702 x: f64,
703 y: f64,
704 font_size: f64,
705 out_nodes: &mut Vec<Node>,
706) -> LayoutBox {
707 match node {
708 MathNode::Text(txt) => {
709 let width = txt.chars().count() as f64 * (font_size * 0.60);
710 let is_fn_name = matches!(
711 txt.as_str(),
712 "sin"
713 | "cos"
714 | "tan"
715 | "log"
716 | "ln"
717 | "exp"
718 | "lim"
719 | "max"
720 | "min"
721 | "det"
722 | "arcsin"
723 | "arccos"
724 | "arctan"
725 | "sinh"
726 | "cosh"
727 | "tanh"
728 | "dim"
729 | "ker"
730 | "gcd"
731 | "deg"
732 );
733 let is_var = !is_fn_name && txt.chars().all(|c| c.is_ascii_alphabetic());
734 let font_family = if is_var {
735 "Cambria Math, 'Times New Roman', serif"
736 } else {
737 "Helvetica, Arial, sans-serif"
738 };
739 out_nodes.push(Node::Text {
740 id: String::new(),
741 x,
742 y,
743 runs: vec![TextRun {
744 text: txt.clone(),
745 font_size,
746 font_family: font_family.to_string(),
747 italic: is_var,
748 fill: Paint::solid("#0f172a"),
749 ..Default::default()
750 }],
751 anchor: TextAnchor::Start,
752 transform: IDENTITY,
753 opacity: 1.0,
754 stroke: Stroke::default(),
755 clip_id: None,
756 meta: SourceMeta::default(),
757 });
758 LayoutBox {
759 width,
760 height_above: font_size * 0.8,
761 height_below: font_size * 0.2,
762 }
763 }
764 MathNode::Fraction {
765 numerator,
766 denominator,
767 } => {
768 let num_fs = font_size * 0.85;
769 let den_fs = font_size * 0.85;
770
771 let num_box = measure_box(numerator, num_fs);
772 let den_box = measure_box(denominator, den_fs);
773
774 let frac_width = num_box.width.max(den_box.width) + 12.0;
775 let bar_y = y - font_size * 0.28;
776
777 let num_x = x + (frac_width - num_box.width) / 2.0;
778 let num_y = bar_y - num_box.height_below - 4.0;
779 render_node_recursive(numerator, num_x, num_y, num_fs, out_nodes);
780
781 let den_x = x + (frac_width - den_box.width) / 2.0;
782 let den_y = bar_y + den_box.height_above + 4.0;
783 render_node_recursive(denominator, den_x, den_y, den_fs, out_nodes);
784
785 let bar_d = format!(
787 "M {:.2},{:.2} L {:.2},{:.2}",
788 x + 2.0,
789 bar_y,
790 x + frac_width - 2.0,
791 bar_y
792 );
793 out_nodes.push(Node::Path {
794 id: String::new(),
795 d: bar_d,
796 fill_rule: String::new(),
797 fill: Paint::None,
798 stroke: Stroke {
799 paint: Paint::solid("#0f172a"),
800 width: 1.5,
801 line_cap: LineCap::Round,
802 line_join: LineJoin::Round,
803 ..Default::default()
804 },
805 transform: IDENTITY,
806 clip_id: None,
807 meta: SourceMeta::default(),
808 });
809
810 LayoutBox {
811 width: frac_width,
812 height_above: (bar_y - (num_y - num_box.height_above)).abs() + 6.0,
813 height_below: (den_y + den_box.height_below - bar_y).abs() + 6.0,
814 }
815 }
816 MathNode::Superscript(inner) => {
817 let sup_fs = font_size * 0.70;
818 let sup_y = y - font_size * 0.45;
819 let b = render_node_recursive(inner, x, sup_y, sup_fs, out_nodes);
820 LayoutBox {
821 width: b.width,
822 height_above: b.height_above + font_size * 0.45,
823 height_below: 0.0,
824 }
825 }
826 MathNode::Subscript(inner) => {
827 let sub_fs = font_size * 0.70;
828 let sub_y = y + font_size * 0.30;
829 let b = render_node_recursive(inner, x, sub_y, sub_fs, out_nodes);
830 LayoutBox {
831 width: b.width,
832 height_above: 0.0,
833 height_below: b.height_below + font_size * 0.30,
834 }
835 }
836 MathNode::Sqrt(inner) => {
837 let inner_box = measure_box(inner, font_size);
838 let sqrt_w = inner_box.width + 16.0;
839
840 let tick_x = x;
841 let tick_y = y - font_size * 0.1;
842 let low_y = y + font_size * 0.35;
843 let top_y = y - inner_box.height_above - 4.0;
844
845 let sqrt_d = format!(
846 "M {:.2},{:.2} L {:.2},{:.2} L {:.2},{:.2} L {:.2},{:.2} L {:.2},{:.2}",
847 tick_x,
848 tick_y,
849 tick_x + 3.0,
850 tick_y - 2.0,
851 tick_x + 6.0,
852 low_y,
853 tick_x + 10.0,
854 top_y,
855 tick_x + sqrt_w,
856 top_y
857 );
858 out_nodes.push(Node::Path {
859 id: String::new(),
860 d: sqrt_d,
861 fill_rule: String::new(),
862 fill: Paint::None,
863 stroke: Stroke {
864 paint: Paint::solid("#0f172a"),
865 width: 1.5,
866 line_cap: LineCap::Round,
867 line_join: LineJoin::Round,
868 ..Default::default()
869 },
870 transform: IDENTITY,
871 clip_id: None,
872 meta: SourceMeta::default(),
873 });
874
875 render_node_recursive(inner, x + 12.0, y, font_size, out_nodes);
876
877 LayoutBox {
878 width: sqrt_w,
879 height_above: inner_box.height_above + 6.0,
880 height_below: inner_box.height_below,
881 }
882 }
883 MathNode::Matrix { rows, delimiters } => {
884 let row_count = rows.len();
885 let col_count = rows.iter().map(|r| r.len()).max().unwrap_or(0);
886 if row_count == 0 || col_count == 0 {
887 return LayoutBox {
888 width: 0.0,
889 height_above: font_size * 0.8,
890 height_below: font_size * 0.2,
891 };
892 }
893
894 let cell_font_size = font_size * 0.90;
895 let col_gap = 16.0;
896 let row_gap = 8.0;
897
898 let mut cell_boxes: Vec<Vec<LayoutBox>> = Vec::with_capacity(row_count);
900 for r in rows {
901 let mut r_boxes = Vec::with_capacity(r.len());
902 for cell in r {
903 r_boxes.push(measure_box(cell, cell_font_size));
904 }
905 cell_boxes.push(r_boxes);
906 }
907
908 let mut col_widths = vec![0.0f64; col_count];
910 for r_boxes in &cell_boxes {
911 for (c_idx, b) in r_boxes.iter().enumerate() {
912 if b.width > col_widths[c_idx] {
913 col_widths[c_idx] = b.width;
914 }
915 }
916 }
917
918 let mut row_aboves = vec![cell_font_size * 0.8; row_count];
920 let mut row_belows = vec![cell_font_size * 0.2; row_count];
921 for (r_idx, r_boxes) in cell_boxes.iter().enumerate() {
922 for b in r_boxes {
923 if b.height_above > row_aboves[r_idx] {
924 row_aboves[r_idx] = b.height_above;
925 }
926 if b.height_below > row_belows[r_idx] {
927 row_belows[r_idx] = b.height_below;
928 }
929 }
930 }
931
932 let mut row_heights = Vec::with_capacity(row_count);
933 for i in 0..row_count {
934 row_heights.push(row_aboves[i] + row_belows[i] + row_gap);
935 }
936 let total_matrix_height: f64 = row_heights.iter().sum::<f64>() - row_gap;
937 let inner_width: f64 = col_widths.iter().sum::<f64>()
938 + if col_count > 1 {
939 (col_count - 1) as f64 * col_gap
940 } else {
941 0.0
942 };
943
944 let has_visible_delims = matches!(*delimiters, Some((l, r)) if l != '.' || r != '.');
945 let delim_pad = if has_visible_delims { 12.0 } else { 0.0 };
946 let total_width = inner_width + delim_pad * 2.0;
947
948 let matrix_top = y - total_matrix_height / 2.0;
950 let matrix_bottom = matrix_top + total_matrix_height;
951
952 if let Some((ld, rd)) = delimiters {
954 let left_x = x + 3.0;
955 let right_x = x + total_width - 3.0;
956 match ld {
957 '(' => {
958 let d = format!(
959 "M {:.2},{:.2} C {:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
960 left_x + 5.0,
961 matrix_top - 2.0,
962 left_x - 3.0,
963 y - total_matrix_height * 0.2,
964 left_x - 3.0,
965 y + total_matrix_height * 0.2,
966 left_x + 5.0,
967 matrix_bottom + 2.0
968 );
969 out_nodes.push(Node::Path {
970 id: String::new(),
971 d,
972 fill_rule: String::new(),
973 fill: Paint::None,
974 stroke: Stroke {
975 paint: Paint::solid("#0f172a"),
976 width: 1.5,
977 line_cap: LineCap::Round,
978 line_join: LineJoin::Round,
979 ..Default::default()
980 },
981 transform: IDENTITY,
982 clip_id: None,
983 meta: SourceMeta::default(),
984 });
985 }
986 '[' => {
987 let d = format!(
988 "M {:.2},{:.2} L {:.2},{:.2} L {:.2},{:.2} L {:.2},{:.2}",
989 left_x + 6.0,
990 matrix_top - 2.0,
991 left_x,
992 matrix_top - 2.0,
993 left_x,
994 matrix_bottom + 2.0,
995 left_x + 6.0,
996 matrix_bottom + 2.0
997 );
998 out_nodes.push(Node::Path {
999 id: String::new(),
1000 d,
1001 fill_rule: String::new(),
1002 fill: Paint::None,
1003 stroke: Stroke {
1004 paint: Paint::solid("#0f172a"),
1005 width: 1.5,
1006 line_cap: LineCap::Round,
1007 line_join: LineJoin::Round,
1008 ..Default::default()
1009 },
1010 transform: IDENTITY,
1011 clip_id: None,
1012 meta: SourceMeta::default(),
1013 });
1014 }
1015 '|' => {
1016 let d = format!(
1017 "M {:.2},{:.2} L {:.2},{:.2}",
1018 left_x + 2.0,
1019 matrix_top - 2.0,
1020 left_x + 2.0,
1021 matrix_bottom + 2.0
1022 );
1023 out_nodes.push(Node::Path {
1024 id: String::new(),
1025 d,
1026 fill_rule: String::new(),
1027 fill: Paint::None,
1028 stroke: Stroke {
1029 paint: Paint::solid("#0f172a"),
1030 width: 1.5,
1031 line_cap: LineCap::Round,
1032 ..Default::default()
1033 },
1034 transform: IDENTITY,
1035 clip_id: None,
1036 meta: SourceMeta::default(),
1037 });
1038 }
1039 '‖' => {
1040 let d = format!(
1041 "M {:.2},{:.2} L {:.2},{:.2} M {:.2},{:.2} L {:.2},{:.2}",
1042 left_x + 1.0,
1043 matrix_top - 2.0,
1044 left_x + 1.0,
1045 matrix_bottom + 2.0,
1046 left_x + 4.5,
1047 matrix_top - 2.0,
1048 left_x + 4.5,
1049 matrix_bottom + 2.0
1050 );
1051 out_nodes.push(Node::Path {
1052 id: String::new(),
1053 d,
1054 fill_rule: String::new(),
1055 fill: Paint::None,
1056 stroke: Stroke {
1057 paint: Paint::solid("#0f172a"),
1058 width: 1.5,
1059 line_cap: LineCap::Round,
1060 ..Default::default()
1061 },
1062 transform: IDENTITY,
1063 clip_id: None,
1064 meta: SourceMeta::default(),
1065 });
1066 }
1067 '{' => {
1068 let mid_y = y;
1069 let d = format!(
1070 "M {:.2},{:.2} Q {:.2},{:.2} {:.2},{:.2} Q {:.2},{:.2} {:.2},{:.2} Q {:.2},{:.2} {:.2},{:.2} Q {:.2},{:.2} {:.2},{:.2}",
1071 left_x + 6.0,
1072 matrix_top - 2.0,
1073 left_x + 1.0,
1074 matrix_top - 2.0,
1075 left_x + 1.0,
1076 matrix_top + 10.0,
1077 left_x + 1.0,
1078 mid_y - 2.0,
1079 left_x - 4.0,
1080 mid_y,
1081 left_x + 1.0,
1082 mid_y + 2.0,
1083 left_x + 1.0,
1084 matrix_bottom - 10.0,
1085 left_x + 1.0,
1086 matrix_bottom + 2.0,
1087 left_x + 6.0,
1088 matrix_bottom + 2.0
1089 );
1090 out_nodes.push(Node::Path {
1091 id: String::new(),
1092 d,
1093 fill_rule: String::new(),
1094 fill: Paint::None,
1095 stroke: Stroke {
1096 paint: Paint::solid("#0f172a"),
1097 width: 1.5,
1098 line_cap: LineCap::Round,
1099 line_join: LineJoin::Round,
1100 ..Default::default()
1101 },
1102 transform: IDENTITY,
1103 clip_id: None,
1104 meta: SourceMeta::default(),
1105 });
1106 }
1107 _ => {}
1108 }
1109
1110 match rd {
1111 ')' => {
1112 let d = format!(
1113 "M {:.2},{:.2} C {:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
1114 right_x - 5.0,
1115 matrix_top - 2.0,
1116 right_x + 3.0,
1117 y - total_matrix_height * 0.2,
1118 right_x + 3.0,
1119 y + total_matrix_height * 0.2,
1120 right_x - 5.0,
1121 matrix_bottom + 2.0
1122 );
1123 out_nodes.push(Node::Path {
1124 id: String::new(),
1125 d,
1126 fill_rule: String::new(),
1127 fill: Paint::None,
1128 stroke: Stroke {
1129 paint: Paint::solid("#0f172a"),
1130 width: 1.5,
1131 line_cap: LineCap::Round,
1132 line_join: LineJoin::Round,
1133 ..Default::default()
1134 },
1135 transform: IDENTITY,
1136 clip_id: None,
1137 meta: SourceMeta::default(),
1138 });
1139 }
1140 ']' => {
1141 let d = format!(
1142 "M {:.2},{:.2} L {:.2},{:.2} L {:.2},{:.2} L {:.2},{:.2}",
1143 right_x - 6.0,
1144 matrix_top - 2.0,
1145 right_x,
1146 matrix_top - 2.0,
1147 right_x,
1148 matrix_bottom + 2.0,
1149 right_x - 6.0,
1150 matrix_bottom + 2.0
1151 );
1152 out_nodes.push(Node::Path {
1153 id: String::new(),
1154 d,
1155 fill_rule: String::new(),
1156 fill: Paint::None,
1157 stroke: Stroke {
1158 paint: Paint::solid("#0f172a"),
1159 width: 1.5,
1160 line_cap: LineCap::Round,
1161 line_join: LineJoin::Round,
1162 ..Default::default()
1163 },
1164 transform: IDENTITY,
1165 clip_id: None,
1166 meta: SourceMeta::default(),
1167 });
1168 }
1169 '|' => {
1170 let d = format!(
1171 "M {:.2},{:.2} L {:.2},{:.2}",
1172 right_x - 2.0,
1173 matrix_top - 2.0,
1174 right_x - 2.0,
1175 matrix_bottom + 2.0
1176 );
1177 out_nodes.push(Node::Path {
1178 id: String::new(),
1179 d,
1180 fill_rule: String::new(),
1181 fill: Paint::None,
1182 stroke: Stroke {
1183 paint: Paint::solid("#0f172a"),
1184 width: 1.5,
1185 line_cap: LineCap::Round,
1186 ..Default::default()
1187 },
1188 transform: IDENTITY,
1189 clip_id: None,
1190 meta: SourceMeta::default(),
1191 });
1192 }
1193 '‖' => {
1194 let d = format!(
1195 "M {:.2},{:.2} L {:.2},{:.2} M {:.2},{:.2} L {:.2},{:.2}",
1196 right_x - 4.5,
1197 matrix_top - 2.0,
1198 right_x - 4.5,
1199 matrix_bottom + 2.0,
1200 right_x - 1.0,
1201 matrix_top - 2.0,
1202 right_x - 1.0,
1203 matrix_bottom + 2.0
1204 );
1205 out_nodes.push(Node::Path {
1206 id: String::new(),
1207 d,
1208 fill_rule: String::new(),
1209 fill: Paint::None,
1210 stroke: Stroke {
1211 paint: Paint::solid("#0f172a"),
1212 width: 1.5,
1213 line_cap: LineCap::Round,
1214 ..Default::default()
1215 },
1216 transform: IDENTITY,
1217 clip_id: None,
1218 meta: SourceMeta::default(),
1219 });
1220 }
1221 '}' => {
1222 let mid_y = y;
1223 let d = format!(
1224 "M {:.2},{:.2} Q {:.2},{:.2} {:.2},{:.2} Q {:.2},{:.2} {:.2},{:.2} Q {:.2},{:.2} {:.2},{:.2} Q {:.2},{:.2} {:.2},{:.2}",
1225 right_x - 6.0,
1226 matrix_top - 2.0,
1227 right_x - 1.0,
1228 matrix_top - 2.0,
1229 right_x - 1.0,
1230 matrix_top + 10.0,
1231 right_x - 1.0,
1232 mid_y - 2.0,
1233 right_x + 4.0,
1234 mid_y,
1235 right_x - 1.0,
1236 mid_y + 2.0,
1237 right_x - 1.0,
1238 matrix_bottom - 10.0,
1239 right_x - 1.0,
1240 matrix_bottom + 2.0,
1241 right_x - 6.0,
1242 matrix_bottom + 2.0
1243 );
1244 out_nodes.push(Node::Path {
1245 id: String::new(),
1246 d,
1247 fill_rule: String::new(),
1248 fill: Paint::None,
1249 stroke: Stroke {
1250 paint: Paint::solid("#0f172a"),
1251 width: 1.5,
1252 line_cap: LineCap::Round,
1253 line_join: LineJoin::Round,
1254 ..Default::default()
1255 },
1256 transform: IDENTITY,
1257 clip_id: None,
1258 meta: SourceMeta::default(),
1259 });
1260 }
1261 _ => {}
1262 }
1263 }
1264
1265 let mut cur_y = matrix_top;
1267 for (r_idx, row) in rows.iter().enumerate() {
1268 let row_baseline = cur_y + row_aboves[r_idx];
1269 let mut cur_x = x + delim_pad;
1270 for (c_idx, cell) in row.iter().enumerate() {
1271 let cell_w = cell_boxes[r_idx].get(c_idx).map(|b| b.width).unwrap_or(0.0);
1272 let target_col_w = col_widths.get(c_idx).copied().unwrap_or(cell_w);
1273 let is_left_aligned =
1274 matches!(*delimiters, Some(('{', '.')) | Some(('.', '.')));
1275 let cell_x = if is_left_aligned {
1276 cur_x
1277 } else {
1278 cur_x + (target_col_w - cell_w) / 2.0
1279 };
1280 render_node_recursive(cell, cell_x, row_baseline, cell_font_size, out_nodes);
1281 cur_x += target_col_w + col_gap;
1282 }
1283 cur_y += row_aboves[r_idx] + row_belows[r_idx] + row_gap;
1284 }
1285
1286 LayoutBox {
1287 width: total_width,
1288 height_above: (y - matrix_top).max(cell_font_size * 0.8) + 4.0,
1289 height_below: (matrix_bottom - y).max(cell_font_size * 0.2) + 4.0,
1290 }
1291 }
1292 MathNode::Row(children) => {
1293 let mut cur_x = x;
1294 let mut max_above = font_size * 0.8;
1295 let mut max_below = font_size * 0.2;
1296
1297 for child in children {
1298 let b = render_node_recursive(child, cur_x, y, font_size, out_nodes);
1299 cur_x += b.width + 2.0;
1300 if b.height_above > max_above {
1301 max_above = b.height_above;
1302 }
1303 if b.height_below > max_below {
1304 max_below = b.height_below;
1305 }
1306 }
1307
1308 LayoutBox {
1309 width: cur_x - x,
1310 height_above: max_above,
1311 height_below: max_below,
1312 }
1313 }
1314 }
1315}
1316
1317fn measure_box(node: &MathNode, font_size: f64) -> LayoutBox {
1318 let mut dummy = Vec::new();
1319 render_node_recursive(node, 0.0, 0.0, font_size, &mut dummy)
1320}
1321
1322pub fn extract_latex_from_svg(svg_bytes: &[u8]) -> Result<String> {
1324 if let Some(embedded) = crate::cad::svg_reader::extract_embedded_source(svg_bytes) {
1325 return Ok(embedded);
1326 }
1327
1328 let svg_text = std::str::from_utf8(svg_bytes)
1329 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
1330
1331 let mut reader = Reader::from_str(svg_text);
1332 reader.config_mut().trim_text(true);
1333
1334 let mut texts = Vec::new();
1335 let mut in_text = false;
1336 let mut current_text = String::new();
1337
1338 while let Ok(event) = reader.read_event() {
1339 match event {
1340 Event::Start(e) if e.name().as_ref() == b"text" => {
1341 in_text = true;
1342 current_text.clear();
1343 }
1344 Event::Text(e) if in_text => {
1345 let bytes = e.as_ref();
1346 if let Ok(s) = std::str::from_utf8(bytes) {
1347 current_text.push_str(s);
1348 }
1349 }
1350 Event::End(e) if e.name().as_ref() == b"text" => {
1351 in_text = false;
1352 let trimmed = current_text.trim();
1353 if !trimmed.is_empty() {
1354 texts.push(trimmed.to_string());
1355 }
1356 }
1357 Event::Eof => break,
1358 _ => {}
1359 }
1360 }
1361
1362 Ok(texts.join(" "))
1363}
1364
1365#[cfg(test)]
1366mod tests {
1367 use super::*;
1368
1369 #[test]
1370 fn parses_and_renders_matrices_and_environments() {
1371 let pmatrix = r"\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}";
1372 let ast = parse_math_expression(pmatrix, 0).expect("parse pmatrix");
1373 match &ast {
1374 MathNode::Matrix { rows, delimiters } => {
1375 assert_eq!(*delimiters, Some(('(', ')')));
1376 assert_eq!(rows.len(), 2);
1377 assert_eq!(rows[0].len(), 2);
1378 assert_eq!(rows[1].len(), 2);
1379 }
1380 _ => panic!("expected Matrix"),
1381 }
1382
1383 let opts = ConvertOptions::default();
1384 let page = layout_and_render_math(&ast, pmatrix, &opts).expect("render pmatrix");
1385 assert!(page.width > 0.0);
1386 assert!(page.height > 0.0);
1387 assert!(!page.nodes.is_empty());
1388 }
1389
1390 #[test]
1391 fn parses_nested_matrices_without_leaking_separators() {
1392 let nested = r"\begin{pmatrix} \begin{matrix} a & b \\ c & d \end{matrix} & 0 \\ 0 & 1 \end{pmatrix}";
1393 let ast = parse_math_expression(nested, 0).expect("parse nested");
1394 match &ast {
1395 MathNode::Matrix { rows, delimiters } => {
1396 assert_eq!(*delimiters, Some(('(', ')')));
1397 assert_eq!(rows.len(), 2);
1398 assert_eq!(rows[0].len(), 2);
1399 match &rows[0][0] {
1401 MathNode::Matrix {
1402 rows: inner_rows,
1403 delimiters: inner_delims,
1404 } => {
1405 assert_eq!(*inner_delims, None);
1406 assert_eq!(inner_rows.len(), 2);
1407 }
1408 _ => panic!("expected inner Matrix"),
1409 }
1410 }
1411 _ => panic!("expected Matrix"),
1412 }
1413 }
1414
1415 #[test]
1416 fn parses_cases_and_aligned() {
1417 let cases_tex = r"\begin{cases} x + 1 & x \ge 0 \\ -x & x < 0 \end{cases}";
1418 let ast = parse_math_expression(cases_tex, 0).expect("parse cases");
1419 match &ast {
1420 MathNode::Matrix { rows, delimiters } => {
1421 assert_eq!(*delimiters, Some(('{', '.')));
1422 assert_eq!(rows.len(), 2);
1423 }
1424 _ => panic!("expected Matrix"),
1425 }
1426
1427 let aligned_tex = r"\begin{aligned} a &= b + c \\ d &= e \end{aligned}";
1428 let ast_aligned = parse_math_expression(aligned_tex, 0).expect("parse aligned");
1429 match &ast_aligned {
1430 MathNode::Matrix { rows, delimiters } => {
1431 assert_eq!(*delimiters, Some(('.', '.')));
1432 assert_eq!(rows.len(), 2);
1433 }
1434 _ => panic!("expected Matrix"),
1435 }
1436 }
1437
1438 #[test]
1439 fn parses_array_with_column_spec() {
1440 let array_tex = r"\begin{array}{lcr} 1 & 2 & 3 \\ 4 & 5 & 6 \end{array}";
1441 let ast = parse_math_expression(array_tex, 0).expect("parse array");
1442 match &ast {
1443 MathNode::Matrix { rows, delimiters } => {
1444 assert_eq!(*delimiters, None);
1445 assert_eq!(rows.len(), 2);
1446 assert_eq!(rows[0].len(), 3);
1447 }
1448 _ => panic!("expected Matrix"),
1449 }
1450 }
1451
1452 #[test]
1453 fn parses_sqrt_with_optional_index() {
1454 let sqrt_tex = r"\sqrt[3]{x + 1}";
1455 let ast = parse_math_expression(sqrt_tex, 0).expect("parse sqrt");
1456 match &ast {
1457 MathNode::Sqrt(_) => {}
1458 _ => panic!("expected Sqrt"),
1459 }
1460 }
1461}