Skip to main content

term_maths/
layout.rs

1use rust_latex_parser::{AccentKind, EqNode, MathFontKind, MatrixKind};
2
3use crate::mathfont;
4use crate::rendered_block::RenderedBlock;
5
6/// Render an `EqNode` AST into a `RenderedBlock`.
7pub fn layout(node: &EqNode) -> RenderedBlock {
8    match node {
9        EqNode::Text(s) => layout_text(s),
10        EqNode::Space(pts) => layout_space(*pts),
11        EqNode::Seq(children) => layout_seq(children),
12        EqNode::Frac(num, den) => layout_frac(num, den),
13        EqNode::Sup(base, sup) => layout_sup(base, sup),
14        EqNode::Sub(base, sub) => layout_sub(base, sub),
15        EqNode::SupSub(base, sup, sub) => layout_supsub(base, sup, sub),
16        EqNode::Sqrt(body) => layout_sqrt(body),
17        EqNode::BigOp {
18            symbol,
19            lower,
20            upper,
21        } => layout_bigop(symbol, lower, upper),
22        EqNode::Accent(body, kind) => layout_accent(body, kind),
23        EqNode::Limit { name, lower } => layout_limit(name, lower),
24        EqNode::TextBlock(s) => RenderedBlock::from_text(s),
25        EqNode::MathFont { kind, content } => layout_mathfont(kind, content),
26        EqNode::Delimited {
27            left,
28            right,
29            content,
30        } => layout_delimited(left, right, content),
31        EqNode::Matrix { kind, rows } => layout_matrix(kind, rows),
32        EqNode::Cases { rows } => layout_cases(rows),
33        EqNode::Binom(top, bottom) => layout_binom(top, bottom),
34        EqNode::Brace {
35            content,
36            label,
37            over,
38        } => layout_brace(content, label, over),
39        EqNode::StackRel {
40            base,
41            annotation,
42            over,
43        } => layout_stackrel(base, annotation, over),
44    }
45}
46
47fn layout_text(s: &str) -> RenderedBlock {
48    RenderedBlock::from_text(s)
49}
50
51/// Map a character to its Unicode superscript equivalent, if one exists.
52fn to_superscript_char(ch: char) -> Option<char> {
53    match ch {
54        '0' => Some('⁰'),
55        '1' => Some('¹'),
56        '2' => Some('²'),
57        '3' => Some('³'),
58        '4' => Some('⁴'),
59        '5' => Some('⁵'),
60        '6' => Some('⁶'),
61        '7' => Some('⁷'),
62        '8' => Some('⁸'),
63        '9' => Some('⁹'),
64        '+' => Some('⁺'),
65        '-' => Some('⁻'),
66        '=' => Some('⁼'),
67        '(' => Some('⁽'),
68        ')' => Some('⁾'),
69        'n' => Some('ⁿ'),
70        'i' => Some('ⁱ'),
71        _ => None,
72    }
73}
74
75/// Map a character to its Unicode subscript equivalent, if one exists.
76fn to_subscript_char(ch: char) -> Option<char> {
77    match ch {
78        '0' => Some('₀'),
79        '1' => Some('₁'),
80        '2' => Some('₂'),
81        '3' => Some('₃'),
82        '4' => Some('₄'),
83        '5' => Some('₅'),
84        '6' => Some('₆'),
85        '7' => Some('₇'),
86        '8' => Some('₈'),
87        '9' => Some('₉'),
88        '+' => Some('₊'),
89        '-' => Some('₋'),
90        '=' => Some('₌'),
91        '(' => Some('₍'),
92        ')' => Some('₎'),
93        'a' => Some('ₐ'),
94        'e' => Some('ₑ'),
95        'h' => Some('ₕ'),
96        'i' => Some('ᵢ'),
97        'j' => Some('ⱼ'),
98        'k' => Some('ₖ'),
99        'l' => Some('ₗ'),
100        'm' => Some('ₘ'),
101        'n' => Some('ₙ'),
102        'o' => Some('ₒ'),
103        'p' => Some('ₚ'),
104        'r' => Some('ᵣ'),
105        's' => Some('ₛ'),
106        't' => Some('ₜ'),
107        'u' => Some('ᵤ'),
108        'v' => Some('ᵥ'),
109        'x' => Some('ₓ'),
110        _ => None,
111    }
112}
113
114/// Try to convert a node's text content to Unicode superscript characters.
115/// Returns None if any character lacks a superscript form.
116fn try_unicode_superscript(node: &EqNode) -> Option<String> {
117    let text = extract_flat_text(node)?;
118    text.chars().map(to_superscript_char).collect()
119}
120
121/// Try to convert a node's text content to Unicode subscript characters.
122fn try_unicode_subscript(node: &EqNode) -> Option<String> {
123    let text = extract_flat_text(node)?;
124    text.chars().map(to_subscript_char).collect()
125}
126
127/// Extract flat text from simple nodes (Text, Seq of Text).
128fn extract_flat_text(node: &EqNode) -> Option<String> {
129    match node {
130        EqNode::Text(s) => Some(s.clone()),
131        EqNode::Seq(children) => {
132            let mut result = String::new();
133            for child in children {
134                match child {
135                    EqNode::Text(s) => result.push_str(s),
136                    EqNode::Space(_) => {} // skip spaces in scripts
137                    _ => return None,
138                }
139            }
140            if result.is_empty() {
141                None
142            } else {
143                Some(result)
144            }
145        }
146        _ => None,
147    }
148}
149
150/// Render a Space node. The parser auto-inserts Space nodes around operators.
151/// Negative and very small spaces collapse. Standard operator spaces (3–5pt)
152/// become a single space. Larger explicit spaces (\quad etc.) grow accordingly.
153fn layout_space(pts: f32) -> RenderedBlock {
154    if pts <= 0.0 || pts < 2.0 {
155        RenderedBlock::empty()
156    } else if pts >= 18.0 {
157        // \quad or larger
158        RenderedBlock::from_text("  ")
159    } else {
160        RenderedBlock::from_char(' ')
161    }
162}
163
164/// Check if a node is whitespace-like (Space node or Text containing only spaces).
165fn is_space_like(node: &EqNode) -> bool {
166    match node {
167        EqNode::Space(_) => true,
168        EqNode::Text(s) => s.chars().all(|c| c == ' '),
169        _ => false,
170    }
171}
172
173fn layout_seq(children: &[EqNode]) -> RenderedBlock {
174    // Flatten nested Seqs so we can handle spacing uniformly.
175    let flat = flatten_seq(children);
176    // Collapse consecutive whitespace-like nodes into a single space.
177    let mut result = RenderedBlock::empty();
178    let mut prev_was_space = false;
179    for child in &flat {
180        if is_space_like(child) {
181            if !prev_was_space {
182                prev_was_space = true;
183                result = result.beside(&RenderedBlock::from_char(' '));
184            }
185            continue;
186        }
187        prev_was_space = false;
188        let block = layout(child);
189        result = result.beside(&block);
190    }
191    result
192}
193
194/// Trim leading/trailing whitespace from a node.
195/// Strips Space nodes and whitespace-only Text nodes from Seq boundaries.
196fn trim_node(node: &EqNode) -> EqNode {
197    match node {
198        EqNode::Seq(children) => {
199            let trimmed: Vec<EqNode> = children
200                .iter()
201                .map(|c| match c {
202                    EqNode::Text(s) => EqNode::Text(s.trim().to_string()),
203                    other => other.clone(),
204                })
205                .filter(|c| !is_space_like(c) || !matches!(c, EqNode::Text(s) if s.is_empty()))
206                .collect();
207            // Remove leading/trailing space-like nodes
208            let start = trimmed.iter().position(|c| !is_space_like(c)).unwrap_or(0);
209            let end = trimmed
210                .iter()
211                .rposition(|c| !is_space_like(c))
212                .map_or(0, |i| i + 1);
213            if start >= end {
214                return EqNode::Seq(vec![]);
215            }
216            EqNode::Seq(trimmed[start..end].to_vec())
217        }
218        EqNode::Text(s) => EqNode::Text(s.trim().to_string()),
219        other => other.clone(),
220    }
221}
222
223/// Recursively flatten nested Seq nodes into a single flat list.
224fn flatten_seq(children: &[EqNode]) -> Vec<&EqNode> {
225    let mut result = Vec::new();
226    for child in children {
227        if let EqNode::Seq(inner) = child {
228            result.extend(flatten_seq(inner));
229        } else {
230            result.push(child);
231        }
232    }
233    result
234}
235
236fn layout_frac(num: &EqNode, den: &EqNode) -> RenderedBlock {
237    let num_block = layout(num);
238    let den_block = layout(den);
239
240    let bar_width = num_block.width().max(den_block.width()) + 2; // +2 for padding
241    let bar = RenderedBlock::hline('─', bar_width);
242
243    let num_centered = num_block.center_in(bar_width);
244    let den_centered = den_block.center_in(bar_width);
245
246    // Stack: numerator, bar, denominator. Baseline is the bar row.
247    let top = RenderedBlock::above(&num_centered, &bar, 0);
248    let baseline_row = top.height() - 1; // bar is the last row of 'top'
249    RenderedBlock::above(&top, &den_centered, baseline_row)
250}
251
252fn layout_sup(base: &EqNode, sup: &EqNode) -> RenderedBlock {
253    // Try inline Unicode superscript first
254    if let Some(sup_text) = try_unicode_superscript(sup) {
255        let base_block = layout(base);
256        let sup_block = RenderedBlock::from_text(&sup_text);
257        return base_block.beside(&sup_block);
258    }
259
260    let base_block = layout(base);
261    let sup_block = layout(sup);
262
263    let can_overlap = base_block.height() > 1;
264    let sup_above = if can_overlap {
265        sup_block.height().saturating_sub(1)
266    } else {
267        sup_block.height()
268    };
269
270    let rows = build_sup_sub_grid(
271        base_block.cells(),
272        base_block.width(),
273        base_block.baseline(),
274        sup_block.cells(),
275        sup_block.width(),
276        None,
277        0,
278    );
279
280    let total_height = rows.len();
281    let baseline = sup_above + base_block.baseline();
282
283    RenderedBlock::new(rows, baseline.min(total_height.saturating_sub(1)))
284}
285
286fn layout_sub(base: &EqNode, sub: &EqNode) -> RenderedBlock {
287    // Try inline Unicode subscript first
288    if let Some(sub_text) = try_unicode_subscript(sub) {
289        let base_block = layout(base);
290        let sub_block = RenderedBlock::from_text(&sub_text);
291        return base_block.beside(&sub_block);
292    }
293
294    let base_block = layout(base);
295    let sub_block = layout(sub);
296
297    let rows = build_sup_sub_grid(
298        base_block.cells(),
299        base_block.width(),
300        base_block.baseline(),
301        &[],
302        0,
303        Some((sub_block.cells(), sub_block.width())),
304        0,
305    );
306
307    let baseline = base_block.baseline();
308    let total_height = rows.len();
309
310    RenderedBlock::new(rows, baseline.min(total_height.saturating_sub(1)))
311}
312
313fn layout_supsub(base: &EqNode, sup: &EqNode, sub: &EqNode) -> RenderedBlock {
314    // Try inline Unicode for both scripts
315    let sup_inline = try_unicode_superscript(sup);
316    let sub_inline = try_unicode_subscript(sub);
317
318    if let (Some(sup_text), Some(sub_text)) = (&sup_inline, &sub_inline) {
319        let base_block = layout(base);
320        let scripts = format!("{}{}", sup_text, sub_text);
321        // Subscript chars go right after superscript chars, all inline
322        // Actually stack them: sup on same line, sub on same line
323        // For compactness: base followed by sup_text on top row, sub_text on bottom
324        // Simplest: just append both inline
325        return base_block.beside(&RenderedBlock::from_text(&scripts));
326    }
327
328    // Fall back to multi-row layout
329    let base_block = layout(base);
330    let sup_block = layout(sup);
331    let sub_block = layout(sub);
332
333    let can_overlap_sup = base_block.height() > 1;
334    let sup_above = if can_overlap_sup {
335        sup_block.height().saturating_sub(1)
336    } else {
337        sup_block.height()
338    };
339
340    let rows = build_sup_sub_grid(
341        base_block.cells(),
342        base_block.width(),
343        base_block.baseline(),
344        sup_block.cells(),
345        sup_block.width(),
346        Some((sub_block.cells(), sub_block.width())),
347        0,
348    );
349
350    let total_height = rows.len();
351    let baseline = sup_above + base_block.baseline();
352
353    RenderedBlock::new(rows, baseline.min(total_height.saturating_sub(1)))
354}
355
356/// Build a grid for base with optional superscript above-right and subscript below-right.
357///
358/// Layout:
359/// ```text
360///          [sup rows]
361///   [base] [overlap ]
362///          [sub rows]
363/// ```
364///
365/// The superscript's last row overlaps with the base's first row (right side).
366/// The subscript's first row overlaps with the base's last row (right side).
367/// Build a grid for base with optional superscript above-right and subscript below-right.
368///
369/// For a single-row base like `x`:
370/// - `x^2`   renders as:  ` 2`  /  `x `
371/// - `x_i`   renders as:  `x `  /  ` i`
372/// - `x_i^2` renders as:  ` 2`  /  `x `  /  ` i`
373///
374/// For multi-row bases, sup overlaps with the top row and sub with the bottom row.
375fn build_sup_sub_grid(
376    base_cells: &[Vec<String>],
377    base_width: usize,
378    _base_baseline: usize,
379    sup_cells: &[Vec<String>],
380    sup_width: usize,
381    sub: Option<(&[Vec<String>], usize)>,
382    _sub_baseline: usize,
383) -> Vec<Vec<String>> {
384    let base_height = base_cells.len();
385    let sup_height = sup_cells.len();
386    let (sub_cells, sub_width) = sub.unwrap_or((&[], 0));
387    let sub_height = sub_cells.len();
388    let has_sup = sup_height > 0;
389    let has_sub = sub_height > 0;
390
391    let script_width = sup_width.max(sub_width);
392
393    // For single-row bases with both sup and sub, don't overlap — stack all three.
394    // For multi-row bases or single script, allow 1 row of overlap.
395    let can_overlap_sup = has_sup && base_height > 1;
396    let can_overlap_sub = has_sub && base_height > 1 && !(has_sup && base_height <= 2);
397
398    let sup_above = if can_overlap_sup {
399        sup_height.saturating_sub(1)
400    } else {
401        sup_height
402    };
403
404    let sub_below = if can_overlap_sub {
405        sub_height.saturating_sub(1)
406    } else {
407        sub_height
408    };
409
410    let total_height = sup_above + base_height + sub_below;
411    let mut rows = Vec::with_capacity(total_height);
412
413    let empty_script = || std::iter::repeat_n(" ".to_string(), script_width);
414
415    // Helper to append a script row (or padding) to a row
416    fn append_script_row(
417        row: &mut Vec<String>,
418        cells: &[Vec<String>],
419        idx: usize,
420        script_width: usize,
421    ) {
422        if idx < cells.len() {
423            row.extend(cells[idx].iter().cloned());
424            let used = cells[idx].len();
425            row.extend(std::iter::repeat_n(
426                " ".to_string(),
427                script_width.saturating_sub(used),
428            ));
429        } else {
430            row.extend(std::iter::repeat_n(" ".to_string(), script_width));
431        }
432    }
433
434    // Phase 1: sup-only rows above the base
435    for r in 0..sup_above {
436        let mut row = vec![" ".to_string(); base_width];
437        append_script_row(&mut row, sup_cells, r, script_width);
438        rows.push(row);
439    }
440
441    // Phase 2: base rows (with possible script overlap)
442    for (r, base_row) in base_cells.iter().enumerate().take(base_height) {
443        let mut row = base_row.clone();
444
445        // Check if a sup row overlaps here
446        let sup_idx = if can_overlap_sup {
447            sup_above + r
448        } else {
449            usize::MAX
450        };
451        // Check if a sub row overlaps here
452        let sub_overlap_start = if can_overlap_sub {
453            base_height.saturating_sub(sub_height)
454        } else {
455            usize::MAX
456        };
457        let sub_idx = if r >= sub_overlap_start && can_overlap_sub {
458            r - sub_overlap_start
459        } else {
460            usize::MAX
461        };
462
463        if sup_idx < sup_height {
464            append_script_row(&mut row, sup_cells, sup_idx, script_width);
465        } else if sub_idx < sub_height {
466            append_script_row(&mut row, sub_cells, sub_idx, script_width);
467        } else {
468            row.extend(empty_script());
469        }
470
471        rows.push(row);
472    }
473
474    // Phase 3: sub-only rows below the base
475    let sub_start = if can_overlap_sub {
476        sub_height.min(base_height)
477    } else {
478        0
479    };
480    for r in sub_start..sub_height {
481        let mut row = vec![" ".to_string(); base_width];
482        append_script_row(&mut row, sub_cells, r, script_width);
483        rows.push(row);
484    }
485
486    rows
487}
488
489fn layout_sqrt(body: &EqNode) -> RenderedBlock {
490    let body_block = layout(body);
491    let body_h = body_block.height();
492    let body_w = body_block.width();
493
494    // Single-row body:   ___
495    //                   √abc
496    //
497    // Multi-row body:    ________
498    //                   ╱  num
499    //                  ╱  ─────
500    //                 √   den
501
502    if body_h == 1 {
503        // Simple case: √ prefix with overline above
504        let mut rows = Vec::with_capacity(2);
505        // Overline row
506        let mut top = vec![" ".to_string()];
507        top.extend(std::iter::repeat_n("─".to_string(), body_w));
508        rows.push(top);
509        // Body row with √
510        let mut bot = vec!["√".to_string()];
511        bot.extend(body_block.cells()[0].iter().cloned());
512        rows.push(bot);
513        RenderedBlock::new(rows, 1) // baseline at body row
514    } else {
515        // Multi-row: radical extends upward
516        let mut rows = Vec::with_capacity(body_h + 1);
517
518        // Overline row
519        let mut top = vec![" ".to_string()];
520        top.extend(std::iter::repeat_n("─".to_string(), body_w));
521        rows.push(top);
522
523        // Body rows with radical on the left
524        for r in 0..body_h {
525            let radical_char = if r == body_h - 1 { "√" } else { "│" };
526            let mut row = vec![radical_char.to_string()];
527            row.extend(body_block.cells()[r].iter().cloned());
528            rows.push(row);
529        }
530
531        let baseline = 1 + body_block.baseline();
532        RenderedBlock::new(rows, baseline)
533    }
534}
535
536/// Build a multi-row operator symbol for integrals (⌠⎮⌡) and large Σ/∏.
537fn build_bigop_symbol(symbol: &str) -> RenderedBlock {
538    match symbol {
539        "∫" => {
540            // 3-row integral using bracket pieces
541            let rows = vec![
542                vec!["⌠".to_string()],
543                vec!["⎮".to_string()],
544                vec!["⌡".to_string()],
545            ];
546            RenderedBlock::new(rows, 1) // baseline at middle
547        }
548        "∬" => {
549            let rows = vec![
550                vec!["⌠".to_string(), "⌠".to_string()],
551                vec!["⎮".to_string(), "⎮".to_string()],
552                vec!["⌡".to_string(), "⌡".to_string()],
553            ];
554            RenderedBlock::new(rows, 1)
555        }
556        "∮" => {
557            // Contour integral — use single char since no multi-row form exists
558            let rows = vec![
559                vec!["⌠".to_string()],
560                vec!["⎮".to_string()],
561                vec!["⌡".to_string()],
562            ];
563            RenderedBlock::new(rows, 1)
564        }
565        _ => {
566            // Σ, ∏, etc. — single character is fine, they're already wide enough
567            RenderedBlock::from_text(symbol)
568        }
569    }
570}
571
572fn layout_bigop(
573    symbol: &str,
574    lower: &Option<Box<EqNode>>,
575    upper: &Option<Box<EqNode>>,
576) -> RenderedBlock {
577    let op_block = build_bigop_symbol(symbol);
578
579    let upper_block = upper.as_ref().map(|u| layout(u));
580    let lower_block = lower.as_ref().map(|l| layout(l));
581
582    let max_width = [
583        op_block.width(),
584        upper_block.as_ref().map_or(0, |b| b.width()),
585        lower_block.as_ref().map_or(0, |b| b.width()),
586    ]
587    .into_iter()
588    .max()
589    .unwrap_or(1);
590
591    let op_centered = op_block.center_in(max_width);
592
593    let mut result = if let Some(ub) = &upper_block {
594        let ub_centered = ub.center_in(max_width);
595        let baseline = ub_centered.height(); // op starts after upper limit
596        RenderedBlock::above(&ub_centered, &op_centered, baseline)
597    } else {
598        op_centered.clone()
599    };
600
601    // Baseline at the middle of the operator symbol
602    let op_mid = upper_block.as_ref().map_or(0, |b| b.height()) + op_block.height() / 2;
603
604    if let Some(lb) = &lower_block {
605        let lb_centered = lb.center_in(max_width);
606        result = RenderedBlock::above(&result, &lb_centered, op_mid);
607    }
608
609    RenderedBlock::new(result.cells().to_vec(), op_mid)
610}
611
612fn layout_accent(body: &EqNode, kind: &AccentKind) -> RenderedBlock {
613    let body_block = layout(body);
614    let w = body_block.width();
615
616    let accent_block = match kind {
617        AccentKind::Bar => {
618            // Overline: use ‾ repeated across full width
619            RenderedBlock::hline('‾', w)
620        }
621        AccentKind::Hat => {
622            if w <= 1 {
623                RenderedBlock::from_char('^')
624            } else if w <= 3 {
625                RenderedBlock::from_text("/\\").center_in(w)
626            } else {
627                // Wide hat: /‾‾‾\ shape
628                let inner = w.saturating_sub(2);
629                let hat_str: String = std::iter::once('/')
630                    .chain(std::iter::repeat_n('‾', inner))
631                    .chain(std::iter::once('\\'))
632                    .collect();
633                RenderedBlock::from_text(&hat_str)
634            }
635        }
636        AccentKind::Tilde => {
637            if w <= 1 {
638                RenderedBlock::from_char('~')
639            } else {
640                // Wide tilde using ˜ repeated or ~ centered
641                RenderedBlock::hline('~', w)
642            }
643        }
644        AccentKind::Vec => {
645            if w <= 1 {
646                RenderedBlock::from_char('→')
647            } else {
648                // Arrow spanning width: ──→
649                let shaft = w.saturating_sub(1);
650                let arrow_str: String = std::iter::repeat_n('─', shaft)
651                    .chain(std::iter::once('→'))
652                    .collect();
653                RenderedBlock::from_text(&arrow_str)
654            }
655        }
656        AccentKind::Dot => RenderedBlock::from_char('˙').center_in(w),
657        AccentKind::DoubleDot => RenderedBlock::from_text("¨").center_in(w),
658    };
659
660    let baseline = accent_block.height() + body_block.baseline();
661    RenderedBlock::above(&accent_block, &body_block, baseline)
662}
663
664fn layout_limit(name: &str, lower: &Option<Box<EqNode>>) -> RenderedBlock {
665    let name_block = RenderedBlock::from_text(name);
666
667    if let Some(low) = lower {
668        let low_block = layout(low);
669        let max_width = name_block.width().max(low_block.width());
670        let name_centered = name_block.center_in(max_width);
671        let low_centered = low_block.center_in(max_width);
672        let baseline = name_centered.height() - 1;
673        RenderedBlock::above(&name_centered, &low_centered, baseline)
674    } else {
675        name_block
676    }
677}
678
679fn layout_mathfont(kind: &MathFontKind, content: &EqNode) -> RenderedBlock {
680    // Extract text and apply Unicode math font mapping
681    if let Some(text) = extract_flat_text(content) {
682        let mapped = mathfont::map_str(kind, &text);
683        RenderedBlock::from_text(&mapped)
684    } else {
685        // Complex content inside font command — render normally
686        layout(content)
687    }
688}
689
690fn layout_delimited(left: &str, right: &str, content: &EqNode) -> RenderedBlock {
691    let content_block = layout(content);
692    let h = content_block.height();
693
694    let left_block = build_delimiter(left, h);
695    let right_block = build_delimiter(right, h);
696
697    left_block.beside(&content_block).beside(&right_block)
698}
699
700/// Build a vertically-scaled delimiter.
701fn build_delimiter(delim: &str, height: usize) -> RenderedBlock {
702    if delim == "." || delim.is_empty() {
703        // Invisible delimiter
704        return RenderedBlock::new(vec![vec![" ".to_string()]; height], height / 2);
705    }
706
707    if height <= 1 {
708        return RenderedBlock::new(vec![vec![delim.to_string()]], 0);
709    }
710
711    let (top, mid, bot) = match delim {
712        "(" => ("⎛", "⎜", "⎝"),
713        ")" => ("⎞", "⎟", "⎠"),
714        "[" => ("⎡", "⎢", "⎣"),
715        "]" => ("⎤", "⎥", "⎦"),
716        "{" => ("⎧", "⎨", "⎩"),
717        "}" => ("⎫", "⎬", "⎭"),
718        "|" => ("│", "│", "│"),
719        "‖" => ("‖", "‖", "‖"),
720        _ => (delim, delim, delim),
721    };
722
723    let mut rows = Vec::with_capacity(height);
724    rows.push(vec![top.to_string()]);
725    for _ in 1..height.saturating_sub(1) {
726        rows.push(vec![mid.to_string()]);
727    }
728    if height > 1 {
729        rows.push(vec![bot.to_string()]);
730    }
731
732    RenderedBlock::new(rows, height / 2)
733}
734
735fn layout_matrix(kind: &MatrixKind, matrix_rows: &[Vec<EqNode>]) -> RenderedBlock {
736    if matrix_rows.is_empty() {
737        return RenderedBlock::empty();
738    }
739
740    // Render all cells, trimming whitespace from cell content
741    let rendered: Vec<Vec<RenderedBlock>> = matrix_rows
742        .iter()
743        .map(|row| row.iter().map(|cell| layout(&trim_node(cell))).collect())
744        .collect();
745
746    let num_cols = rendered.iter().map(|r| r.len()).max().unwrap_or(0);
747
748    // Compute column widths
749    let mut col_widths = vec![0usize; num_cols];
750    for row in &rendered {
751        for (c, cell) in row.iter().enumerate() {
752            col_widths[c] = col_widths[c].max(cell.width());
753        }
754    }
755
756    // Build each matrix row using beside() for proper baseline alignment.
757    // Then stack rows vertically with a separator gap.
758    let col_sep = 2; // spaces between columns
759    let separator = RenderedBlock::from_text(&" ".repeat(col_sep));
760
761    let mut row_blocks: Vec<RenderedBlock> = Vec::new();
762
763    for row in &rendered {
764        let mut row_block = RenderedBlock::empty();
765        for (c, cell) in row.iter().enumerate() {
766            let padded = cell.center_in(col_widths[c]);
767            if !row_block.is_empty() {
768                row_block = row_block.beside(&separator);
769            }
770            row_block = row_block.beside(&padded);
771        }
772        // Pad to fill missing columns
773        for w in col_widths.iter().take(num_cols).skip(row.len()) {
774            row_block = row_block.beside(&separator);
775            row_block = row_block.beside(&RenderedBlock::from_text(&" ".repeat(*w)));
776        }
777        row_blocks.push(row_block);
778    }
779
780    // Stack rows vertically. Each row_block already has correct baselines from beside().
781    let grid_width = row_blocks.iter().map(|r| r.width()).max().unwrap_or(0);
782
783    let mut grid = RenderedBlock::empty();
784    for row_block in &row_blocks {
785        let padded = row_block.center_in(grid_width);
786        if grid.is_empty() {
787            grid = padded;
788        } else {
789            let baseline = grid.height() / 2; // intermediate baseline
790            grid = RenderedBlock::above(&grid, &padded, baseline);
791        }
792    }
793
794    // Set baseline to middle of entire grid
795    let total_height = grid.height();
796    let grid = RenderedBlock::new(grid.cells().to_vec(), total_height / 2);
797
798    // Wrap with delimiters based on matrix kind
799    let (left, right) = match kind {
800        MatrixKind::Paren => ("(", ")"),
801        MatrixKind::Bracket => ("[", "]"),
802        MatrixKind::Brace => ("{", "}"),
803        MatrixKind::VBar => ("|", "|"),
804        MatrixKind::DoubleVBar => ("‖", "‖"),
805        MatrixKind::Plain => ("", ""),
806    };
807
808    if left.is_empty() {
809        grid
810    } else {
811        let left_d = build_delimiter(left, total_height);
812        let right_d = build_delimiter(right, total_height);
813        left_d.beside(&grid).beside(&right_d)
814    }
815}
816
817fn layout_cases(rows: &[(EqNode, Option<EqNode>)]) -> RenderedBlock {
818    // Render as a left-brace delimited set of rows
819    let rendered: Vec<RenderedBlock> = rows
820        .iter()
821        .map(|(val, cond)| {
822            let val_block = layout(val);
823            if let Some(c) = cond {
824                let cond_block = layout(c);
825                val_block
826                    .beside(&RenderedBlock::from_text("  if "))
827                    .beside(&cond_block)
828            } else {
829                val_block
830            }
831        })
832        .collect();
833
834    let max_width = rendered.iter().map(|b| b.width()).max().unwrap_or(0);
835    let mut grid = RenderedBlock::empty();
836    for row_block in &rendered {
837        let padded = RenderedBlock::new(row_block.cells().to_vec(), row_block.baseline());
838        // Pad to max width
839        let full_row = RenderedBlock::new(
840            padded
841                .cells()
842                .iter()
843                .map(|r| {
844                    let mut r = r.clone();
845                    r.extend(std::iter::repeat_n(
846                        " ".to_string(),
847                        max_width.saturating_sub(r.len()),
848                    ));
849                    r
850                })
851                .collect(),
852            padded.baseline(),
853        );
854        if grid.is_empty() {
855            grid = full_row;
856        } else {
857            grid = RenderedBlock::above(&grid, &full_row, grid.height() / 2);
858        }
859    }
860
861    let total_height = grid.height();
862    let grid = RenderedBlock::new(grid.cells().to_vec(), total_height / 2);
863    let left_brace = build_delimiter("{", total_height);
864    left_brace.beside(&grid)
865}
866
867fn layout_binom(top: &EqNode, bottom: &EqNode) -> RenderedBlock {
868    // Render as a fraction with parentheses instead of a bar
869    let top_block = layout(top);
870    let bot_block = layout(bottom);
871
872    let inner_width = top_block.width().max(bot_block.width());
873    let top_centered = top_block.center_in(inner_width);
874    let bot_centered = bot_block.center_in(inner_width);
875
876    let baseline = top_centered.height();
877    let stacked = RenderedBlock::above(&top_centered, &bot_centered, baseline - 1);
878
879    let h = stacked.height();
880    let left = build_delimiter("(", h);
881    let right = build_delimiter(")", h);
882    left.beside(&stacked).beside(&right)
883}
884
885fn layout_brace(content: &EqNode, label: &Option<Box<EqNode>>, over: &bool) -> RenderedBlock {
886    let content_block = layout(content);
887    let w = content_block.width();
888
889    // Build a horizontal brace
890    let brace_str = if *over { "⏞" } else { "⏟" };
891    let brace_block = RenderedBlock::hline(brace_str.chars().next().unwrap(), w);
892
893    if let Some(lbl) = label {
894        let label_block = layout(lbl).center_in(w);
895        if *over {
896            let top = RenderedBlock::above(&label_block, &brace_block, label_block.height());
897            let baseline = top.height() + content_block.baseline();
898            RenderedBlock::above(&top, &content_block, baseline)
899        } else {
900            let bottom = RenderedBlock::above(&brace_block, &label_block, 0);
901            let baseline = content_block.baseline();
902            RenderedBlock::above(&content_block, &bottom, baseline)
903        }
904    } else if *over {
905        let baseline = brace_block.height() + content_block.baseline();
906        RenderedBlock::above(&brace_block, &content_block, baseline)
907    } else {
908        let baseline = content_block.baseline();
909        RenderedBlock::above(&content_block, &brace_block, baseline)
910    }
911}
912
913fn layout_stackrel(base: &EqNode, annotation: &EqNode, over: &bool) -> RenderedBlock {
914    let base_block = layout(base);
915    let ann_block = layout(annotation);
916    let w = base_block.width().max(ann_block.width());
917    let base_centered = base_block.center_in(w);
918    let ann_centered = ann_block.center_in(w);
919
920    if *over {
921        let baseline = ann_centered.height() + base_block.baseline();
922        RenderedBlock::above(&ann_centered, &base_centered, baseline)
923    } else {
924        let baseline = base_block.baseline();
925        RenderedBlock::above(&base_centered, &ann_centered, baseline)
926    }
927}