Skip to main content

document_svg/math/
mod.rs

1//! LaTeX math formula parsing, box model layout, SVG rendering, and reverse extraction.
2
3use 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                    '!' => {} // negative space
100                    '%' => 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(); // consume invisible delimiter
266                    }
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(); // consume second '\'
594                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            // Fraction bar
786            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            // Measure each cell
899            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            // Calculate max width for each column
909            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            // Calculate height for each row
919            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            // Matrix vertical center aligned with baseline y
949            let matrix_top = y - total_matrix_height / 2.0;
950            let matrix_bottom = matrix_top + total_matrix_height;
951
952            // Render delimiters
953            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            // Render cells
1266            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
1322/// Reverse extraction: restores LaTeX formula from SVG.
1323pub 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                // First cell is inner matrix
1400                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}