Skip to main content

latex_rust/layout/
engine.rs

1//! `MathNode` → `MathBox`. All sizes are [`Dim`](crate::Dim) (zenith-float 1.0).
2
3use core::cell::Cell;
4use core::cmp::Ordering;
5
6use crate::atoms::symbol_atom_kind;
7use crate::color::Color;
8use crate::dim::Dim;
9use crate::error::Error;
10use crate::font::MathFont;
11use crate::layout::metrics::MathParams;
12use crate::layout::numbering::NumberingState;
13use crate::layout::space::{atom_space_mu, convert_bin, space_width};
14use crate::layout::style::MathStyle;
15use crate::layout::{BoxContent, MathBox};
16use crate::parser::{
17    AccentKind, AtomKind, ColSpec, DelimSize, Delimiter, EnvRow, IntegralKind, MathNode,
18    MatrixStyle, PhantomKind, SpaceKind, TextStyle,
19};
20use crate::style_map::styled_char;
21use crate::symbols::lookup;
22
23/// Lay out `node` in `style` using STIX Two Math metrics.
24///
25/// Every dimension on the returned [`MathBox`] is a [`Dim`](crate::Dim). Missing
26/// glyphs and unsupported constructs are errors — never a substitute glyph.
27///
28/// # Arguments
29///
30/// * `node` — parsed math tree.
31/// * `font` — face providing MATH constants and glyph metrics.
32/// * `style` — TeX math style (`Display`, `Text`, scripts).
33///
34/// # Returns
35///
36/// A box tree ready for SVG, PNG, or egui emission.
37///
38/// # Errors
39///
40/// * [`Error::Font`] — glyph missing from the face.
41/// * [`Error::Unsupported`] — construct or MATH table the engine will not fake.
42/// * [`Error::Malformed`] — invalid structure discovered during layout.
43///
44/// # Examples
45///
46/// ```
47/// use latex_rust::{layout, parse, MathFont, MathStyle};
48///
49/// let ast = parse(r"\frac{1}{2}").unwrap();
50/// let font = MathFont::stix_two_math().unwrap();
51/// let boxed = layout(&ast, &font, MathStyle::Text).unwrap();
52/// assert!(!boxed.width.is_zero());
53/// ```
54pub fn layout(node: &MathNode, font: &MathFont, style: MathStyle) -> Result<MathBox, Error> {
55    let mut state = NumberingState::default();
56    layout_with_numbering(node, font, style, &mut state)
57}
58
59/// Lay out with a caller-owned equation counter and `\label` / `\ref` table.
60///
61/// # Arguments
62///
63/// * `node` — parsed math tree.
64/// * `font` — face providing MATH constants and glyph metrics.
65/// * `style` — TeX math style.
66/// * `state` — counter and label map; survives across calls.
67///
68/// # Returns
69///
70/// A box tree. Numbers assigned for this tree are recorded in `state`.
71///
72/// # Errors
73///
74/// Same as [`layout`].
75///
76/// # Examples
77///
78/// ```
79/// use latex_rust::{layout_with_numbering, parse, MathFont, MathStyle, NumberingState};
80///
81/// let ast = parse(r"\begin{equation}x\end{equation}").unwrap();
82/// let font = MathFont::stix_two_math().unwrap();
83/// let mut state = NumberingState::default();
84/// let boxed = layout_with_numbering(&ast, &font, MathStyle::Display, &mut state).unwrap();
85/// assert!(!boxed.width.is_zero());
86/// ```
87pub fn layout_with_numbering(
88    node: &MathNode,
89    font: &MathFont,
90    style: MathStyle,
91    state: &mut NumberingState,
92) -> Result<MathBox, Error> {
93    let params = MathParams::from_font(font)?;
94    let start = state.collect(node);
95    Engine {
96        font,
97        params,
98        numbers: state,
99        idx: Cell::new(start),
100    }
101    .layout(node, style)
102}
103
104struct Engine<'a> {
105    font: &'a MathFont,
106    params: MathParams,
107    numbers: &'a NumberingState,
108    idx: Cell<usize>,
109}
110
111struct Item {
112    bx: MathBox,
113    class: Option<AtomKind>,
114}
115
116impl Engine<'_> {
117    fn layout(&self, node: &MathNode, style: MathStyle) -> Result<MathBox, Error> {
118        Ok(self.item(node, style)?.bx)
119    }
120
121    fn item(&self, node: &MathNode, style: MathStyle) -> Result<Item, Error> {
122        match node {
123            MathNode::Atom(c, k) => {
124                let bx = self.glyph(*c, style)?;
125                Ok(Item {
126                    bx,
127                    class: Some(*k),
128                })
129            }
130            MathNode::Symbol(name) => {
131                let ch = symbol_char(name)?;
132                let bx = self.glyph(ch, style)?;
133                Ok(Item {
134                    bx,
135                    class: Some(symbol_class(name)),
136                })
137            }
138            MathNode::Row(items) => self.row(items, style),
139            MathNode::Fraction(num, den) => self.fraction(num, den, style),
140            MathNode::Radical(deg, rad) => self.radical(deg.as_deref(), rad, style),
141            MathNode::Superscript(base, exp) => self.scripts(base, None, Some(exp), style),
142            MathNode::Subscript(base, sub) => self.scripts(base, Some(sub), None, style),
143            MathNode::SubSup(base, sub, exp) => self.scripts(base, Some(sub), Some(exp), style),
144            MathNode::Delimited(open, body, close) => self.delimited(open, body, close, style),
145            MathNode::SizedDelim(d, size, k) => {
146                let needed = self.explicit_delim_span(*size, style);
147                Ok(Item {
148                    class: Some(*k),
149                    bx: self.delim_box(d, &needed, style)?,
150                })
151            }
152            MathNode::Space(kind) => Ok(Item {
153                bx: MathBox::kern(self.space_dim(kind, style)),
154                class: None,
155            }),
156            MathNode::Strut(h, d) => {
157                let s = self.params.scale(style);
158                Ok(Item {
159                    bx: MathBox {
160                        width: Dim::zero(),
161                        height: h * &s,
162                        depth: d * &s,
163                        italic: Dim::zero(),
164                        shift: Dim::zero(),
165                        content: BoxContent::Empty,
166                    },
167                    class: Some(AtomKind::Ord),
168                })
169            }
170            MathNode::Phantom(kind, inner) => {
171                let mut bx = self.layout(inner, style)?;
172                match kind {
173                    PhantomKind::Full => bx.content = BoxContent::Empty,
174                    PhantomKind::Vertical => {
175                        bx.width = Dim::zero();
176                        bx.content = BoxContent::Empty;
177                    }
178                    PhantomKind::Horizontal => {
179                        bx.height = Dim::zero();
180                        bx.depth = Dim::zero();
181                        bx.content = BoxContent::Empty;
182                    }
183                }
184                Ok(Item {
185                    class: class_of(inner),
186                    bx,
187                })
188            }
189            MathNode::Text(s, ts) => self.text_run(s, *ts, style),
190            MathNode::Operator(name, limits) => self.operator(name, *limits, style),
191            MathNode::Sum(lo, hi) => self.large_op('∑', lo.as_deref(), hi.as_deref(), style, true),
192            MathNode::Product(lo, hi) => {
193                self.large_op('∏', lo.as_deref(), hi.as_deref(), style, true)
194            }
195            MathNode::Integral(k, lo, hi) => {
196                let ch = match k {
197                    IntegralKind::Int => '∫',
198                    IntegralKind::Iint => '∬',
199                    IntegralKind::Iiint => '∭',
200                    IntegralKind::Oint => '∮',
201                    IntegralKind::Oiint => '∯',
202                };
203                self.large_op(ch, lo.as_deref(), hi.as_deref(), style, false)
204            }
205            MathNode::Limit(sub) => {
206                let op = self.text_run("lim", TextStyle::Rm, style)?;
207                if let Some(s) = sub {
208                    self.attach_limits(op.bx, None, Some(s), style, true)
209                } else {
210                    Ok(Item {
211                        bx: op.bx,
212                        class: Some(AtomKind::Op),
213                    })
214                }
215            }
216            MathNode::OverUnder(base, over, under) => {
217                let mut b = self.layout(base, style)?;
218                let mut needed = b.width.clone();
219                if let Some(o) = over {
220                    needed = needed.max(&self.layout(o, style.into_script())?.width);
221                }
222                if let Some(u) = under {
223                    needed = needed.max(&self.layout(u, style.into_script())?.width);
224                }
225                b = self.stretch_h(b, &needed, style)?;
226                self.attach_limits(b, over.as_deref(), under.as_deref(), style, true)
227            }
228            MathNode::Accent(base, kind) => self.accent(base, *kind, style),
229            MathNode::CancelTo(value, expr) => self.cancelto(value, expr, style),
230            MathNode::Matrix(ms, spec, rows) => self.matrix(*ms, spec, rows, style),
231            MathNode::Substack(lines) => self.substack(lines, style),
232            MathNode::Ref(key) => self.reference(key, style),
233            MathNode::Tag { star, body } => self.tag_box(*star, body, style),
234            MathNode::Label(_) | MathNode::NoNumber => Ok(Item {
235                bx: MathBox::empty(),
236                class: None,
237            }),
238            MathNode::Hline => Err(Error::Unsupported {
239                what: "hline outside array".into(),
240            }),
241            MathNode::Intertext(n) => Ok(Item {
242                class: Some(AtomKind::Ord),
243                bx: self.layout(n, MathStyle::Text)?,
244            }),
245            MathNode::Color(c, body) | MathNode::TextColor(c, body) => {
246                let inner = self.layout(body, style)?;
247                Ok(Item {
248                    class: class_of(body),
249                    bx: color_wrap(*c, inner),
250                })
251            }
252            MathNode::ColorBox(c, body) => {
253                let inner = self.layout(body, style)?;
254                let pad = self.params.mu(style) * Dim::from_i64(3);
255                Ok(Item {
256                    class: Some(AtomKind::Inner),
257                    bx: back_color_wrap(*c, pad_box(inner, &pad)),
258                })
259            }
260            MathNode::FColorBox(border, fill, body) => {
261                let inner = self.layout(body, style)?;
262                let pad = self.params.mu(style) * Dim::from_i64(3);
263                let thick = self.params.fraction_rule_thickness.clone() * self.params.scale(style);
264                Ok(Item {
265                    class: Some(AtomKind::Inner),
266                    bx: back_color_wrap(
267                        *fill,
268                        frame_wrap(thick, Some(*border), pad_box(inner, &pad)),
269                    ),
270                })
271            }
272        }
273    }
274
275    fn glyph(&self, ch: char, style: MathStyle) -> Result<MathBox, Error> {
276        let g = self.font.glyph(ch)?;
277        let s = self.params.scale(style);
278        let italic = self.font.italic_correction(g.glyph_id);
279        Ok(MathBox {
280            width: &g.advance * &s,
281            height: &g.height * &s,
282            depth: &g.depth * &s,
283            italic: &italic * &s,
284            shift: Dim::zero(),
285            content: BoxContent::Glyph {
286                ch,
287                glyph_id: g.glyph_id,
288            },
289        })
290    }
291
292    fn glyph_id(&self, ch: char, gid: u16, style: MathStyle) -> Result<MathBox, Error> {
293        let g = self.font.glyph_id(ch, gid)?;
294        let s = self.params.scale(style);
295        let italic = self.font.italic_correction(gid);
296        Ok(MathBox {
297            width: &g.advance * &s,
298            height: &g.height * &s,
299            depth: &g.depth * &s,
300            italic: &italic * &s,
301            shift: Dim::zero(),
302            content: BoxContent::Glyph { ch, glyph_id: gid },
303        })
304    }
305
306    fn space_dim(&self, kind: &SpaceKind, style: MathStyle) -> Dim {
307        let mu = self.params.mu(style);
308        match kind {
309            SpaceKind::Thin => mu * Dim::from_i64(3),
310            SpaceKind::Medium => mu * Dim::from_i64(4),
311            SpaceKind::Thick => mu * Dim::from_i64(5),
312            SpaceKind::NegThin => -(mu * Dim::from_i64(3)),
313            SpaceKind::Quad => self.params.em(style),
314            SpaceKind::Qquad => self.params.em(style) * Dim::from_i64(2),
315            SpaceKind::ControlSpace => self.params.em(style) / Dim::from_i64(3),
316            SpaceKind::Hspace(d) => d * &self.params.scale(style),
317        }
318    }
319
320    fn text_run(&self, s: &str, ts: TextStyle, style: MathStyle) -> Result<Item, Error> {
321        if ts == TextStyle::Pmb {
322            return self.pmb(s, style);
323        }
324        let mut kids = Vec::new();
325        for c in s.chars() {
326            if c == ' ' {
327                kids.push(MathBox::kern(self.params.mu(style) * Dim::from_i64(4)));
328            } else {
329                kids.push(self.glyph(styled_char(c, ts), style)?);
330            }
331        }
332        Ok(Item {
333            bx: MathBox::hpack(kids),
334            class: Some(AtomKind::Ord),
335        })
336    }
337
338    fn pmb(&self, s: &str, style: MathStyle) -> Result<Item, Error> {
339        let base = self.text_run(s, TextStyle::Rm, style)?;
340        let dx = self.params.em(style) / Dim::from_i64(25);
341        let shifted = MathBox::hpack(vec![MathBox::kern(dx.clone()), base.bx.clone()]);
342        Ok(Item {
343            class: Some(AtomKind::Ord),
344            bx: MathBox {
345                width: &base.bx.width + &dx,
346                height: base.bx.height.clone(),
347                depth: base.bx.depth.clone(),
348                italic: Dim::zero(),
349                shift: Dim::zero(),
350                content: BoxContent::Overlap(vec![base.bx, shifted]),
351            },
352        })
353    }
354
355    fn operator(&self, name: &str, limits: bool, style: MathStyle) -> Result<Item, Error> {
356        if let Some(ch) = single_glyph(name) {
357            if !ch.is_ascii_alphabetic() {
358                return self.large_op(ch, None, None, style, limits);
359            }
360        }
361        self.text_run(name, TextStyle::Rm, style).map(|mut it| {
362            it.class = Some(AtomKind::Op);
363            it
364        })
365    }
366
367    fn row(&self, items: &[MathNode], style: MathStyle) -> Result<Item, Error> {
368        if items.is_empty() {
369            return Ok(Item {
370                bx: MathBox::empty(),
371                class: Some(AtomKind::Ord),
372            });
373        }
374        let mut laid = Vec::new();
375        for n in items {
376            laid.push(self.item(n, style)?);
377        }
378        let n = laid.len();
379        let mut classes: Vec<Option<AtomKind>> = Vec::with_capacity(n);
380        for i in 0..n {
381            let prev = if i == 0 { None } else { laid[i - 1].class };
382            let next = if i + 1 < n { laid[i + 1].class } else { None };
383            classes.push(laid[i].class.map(|c| convert_bin(prev, c, next)));
384        }
385        let mut out = Vec::new();
386        for i in 0..n {
387            if i > 0 {
388                if let (Some(l), Some(r)) = (classes[i - 1], classes[i]) {
389                    let mu = atom_space_mu(l, r, style);
390                    let w = space_width(mu, &self.params, style);
391                    if !w.is_zero() {
392                        out.push(MathBox::kern(w));
393                    }
394                }
395            }
396            out.push(laid[i].bx.clone());
397        }
398        let class = if n == 1 {
399            classes[0]
400        } else {
401            Some(AtomKind::Ord)
402        };
403        Ok(Item {
404            bx: MathBox::hpack(out),
405            class,
406        })
407    }
408
409    fn fraction(&self, num: &MathNode, den: &MathNode, style: MathStyle) -> Result<Item, Error> {
410        let num_b = self.layout(num, style.numerator())?;
411        let den_b = self.layout(den, style.denominator())?;
412        let s = self.params.scale(style);
413        let axis = &self.params.axis_height * &s;
414        let thick = &self.params.fraction_rule_thickness * &s;
415        let half = &thick / &Dim::from_i64(2);
416        let (shift_up0, shift_dn0, gap_num, gap_den) = if style.is_display() {
417            (
418                &self.params.fraction_numerator_display_style_shift_up * &s,
419                &self.params.fraction_denominator_display_style_shift_down * &s,
420                &self.params.fraction_num_display_style_gap_min * &s,
421                &self.params.fraction_denom_display_style_gap_min * &s,
422            )
423        } else {
424            (
425                &self.params.fraction_numerator_shift_up * &s,
426                &self.params.fraction_denominator_shift_down * &s,
427                &self.params.fraction_numerator_gap_min * &s,
428                &self.params.fraction_denominator_gap_min * &s,
429            )
430        };
431        let num_shift = shift_up0.max(&(&axis + &half + &gap_num + &num_b.depth));
432        let den_shift = shift_dn0.max(&(&den_b.height + &gap_den + &half - &axis).clamp_nonneg());
433        let width = num_b.width.max(&den_b.width);
434        let num_c = center_in(num_b, &width);
435        let den_c = center_in(den_b, &width);
436        let num_h = num_c.height.clone();
437        let den_d = den_c.depth.clone();
438        let bar =
439            MathBox::rule(width.clone(), thick.clone(), Dim::zero()).with_shift(&axis - &half);
440        Ok(Item {
441            class: Some(AtomKind::Inner),
442            bx: MathBox {
443                width,
444                height: &num_shift + &num_h,
445                depth: &den_shift + &den_d,
446                italic: Dim::zero(),
447                shift: Dim::zero(),
448                content: BoxContent::Overlap(vec![
449                    num_c.with_shift(num_shift),
450                    bar,
451                    den_c.with_shift(-den_shift),
452                ]),
453            },
454        })
455    }
456
457    fn radical(
458        &self,
459        deg: Option<&MathNode>,
460        rad: &MathNode,
461        style: MathStyle,
462    ) -> Result<Item, Error> {
463        let rad_b = self.layout(rad, style.cramp())?;
464        let s = self.params.scale(style);
465        let thick = &self.params.radical_rule_thickness * &s;
466        let extra = &self.params.radical_extra_ascender * &s;
467        let gap = if style.is_display() {
468            &self.params.radical_display_style_vertical_gap * &s
469        } else {
470            &self.params.radical_vertical_gap * &s
471        };
472        let needed = &rad_b.height + &rad_b.depth + &gap + &thick + &extra;
473        let surd = self.sized_glyph('√', &needed, style)?;
474        let bar = MathBox::rule(rad_b.width.clone(), thick.clone(), Dim::zero())
475            .with_shift(&rad_b.height + &gap);
476        let rad_col = MathBox {
477            width: rad_b.width.clone(),
478            height: &rad_b.height + &gap + &thick + &extra,
479            depth: rad_b.depth.clone(),
480            italic: Dim::zero(),
481            shift: Dim::zero(),
482            content: BoxContent::Overlap(vec![bar, rad_b.clone()]),
483        };
484        let mut kids = vec![surd, rad_col];
485        let mut width = MathBox::hpack(vec![kids[0].clone(), kids[1].clone()]).width;
486        let height = (&rad_b.height + &gap + &thick + &extra).max(&kids[0].height);
487        let depth = rad_b.depth.max(&kids[0].depth);
488        if let Some(d) = deg {
489            let db = self.layout(d, MathStyle::ScriptScript)?;
490            let before = &self.params.radical_kern_before_degree * &s;
491            let after = &self.params.radical_kern_after_degree * &s;
492            let pct = Dim::from_i64(i64::from(self.params.radical_degree_bottom_raise_percent))
493                / Dim::from_i64(100);
494            let raise = &height * &pct;
495            let deg_box = db.with_shift(raise);
496            kids = vec![
497                MathBox::kern(before),
498                deg_box,
499                MathBox::kern(after),
500                kids[0].clone(),
501                kids[1].clone(),
502            ];
503            width = MathBox::hpack(kids.clone()).width;
504        }
505        Ok(Item {
506            class: Some(AtomKind::Inner),
507            bx: MathBox {
508                width,
509                height,
510                depth,
511                italic: Dim::zero(),
512                shift: Dim::zero(),
513                content: BoxContent::HList(kids),
514            },
515        })
516    }
517
518    fn scripts(
519        &self,
520        base: &MathNode,
521        sub: Option<&MathNode>,
522        sup: Option<&MathNode>,
523        style: MathStyle,
524    ) -> Result<Item, Error> {
525        let base_it = self.item(base, style)?;
526        self.attach_scripts_to_box(base_it.bx, base_it.class, sub, sup, style)
527    }
528
529    fn attach_scripts_to_box(
530        &self,
531        base: MathBox,
532        class: Option<AtomKind>,
533        sub: Option<&MathNode>,
534        sup: Option<&MathNode>,
535        style: MathStyle,
536    ) -> Result<Item, Error> {
537        if sub.is_none() && sup.is_none() {
538            return Ok(Item { bx: base, class });
539        }
540        let s = self.params.scale(style);
541        let ss = style.into_script();
542        let after = &self.params.space_after_script * &self.params.scale(ss);
543        let mut sup_shift = Dim::zero();
544        let mut sub_shift = Dim::zero();
545        let sup_laid = if let Some(e) = sup {
546            sup_shift = if style.is_cramped() {
547                &self.params.superscript_shift_up_cramped * &s
548            } else {
549                &self.params.superscript_shift_up * &s
550            };
551            Some(self.layout(e, ss.cramp())?)
552        } else {
553            None
554        };
555        let sub_laid = if let Some(u) = sub {
556            sub_shift = &self.params.subscript_shift_down * &s;
557            Some(self.layout(u, ss)?)
558        } else {
559            None
560        };
561        if let (Some(sp), Some(sb)) = (&sup_laid, &sub_laid) {
562            let gap = &sup_shift + &sub_shift - &sp.depth - &sb.height;
563            let min_gap = &self.params.sub_superscript_gap_min * &s;
564            if matches!(gap.cmp(&min_gap), Some(Ordering::Less)) {
565                sub_shift = &sub_shift + (&min_gap - &gap);
566            }
567        }
568        let mut kids = vec![base.clone()];
569        let mut width = base.width.clone();
570        if sup_laid.is_some() && !base.italic.is_zero() {
571            kids.push(MathBox::kern(base.italic.clone()));
572            width = &width + &base.italic;
573        }
574        let mut slot_w = Dim::zero();
575        let mut slot_h = Dim::zero();
576        let mut slot_d = Dim::zero();
577        let mut slot_kids = Vec::new();
578        if let Some(sp) = sup_laid {
579            slot_w = slot_w.max(&sp.width);
580            slot_h = slot_h.max(&(&sp.height + &sup_shift));
581            slot_d = slot_d.max(&(&sp.depth - &sup_shift).clamp_nonneg());
582            slot_kids.push(sp.with_shift(sup_shift));
583        }
584        if let Some(sb) = sub_laid {
585            slot_w = slot_w.max(&sb.width);
586            slot_h = slot_h.max(&(&sb.height - &sub_shift).clamp_nonneg());
587            slot_d = slot_d.max(&(&sb.depth + &sub_shift));
588            slot_kids.push(sb.with_shift(-sub_shift));
589        }
590        kids.push(MathBox {
591            width: slot_w.clone(),
592            height: slot_h.clone(),
593            depth: slot_d.clone(),
594            italic: Dim::zero(),
595            shift: Dim::zero(),
596            content: BoxContent::Overlap(slot_kids),
597        });
598        if !after.is_zero() {
599            kids.push(MathBox::kern(after.clone()));
600        }
601        width = &width + &slot_w + &after;
602        Ok(Item {
603            class,
604            bx: MathBox {
605                width,
606                height: base.height.max(&slot_h),
607                depth: base.depth.max(&slot_d),
608                italic: Dim::zero(),
609                shift: Dim::zero(),
610                content: BoxContent::HList(kids),
611            },
612        })
613    }
614
615    fn delimited(
616        &self,
617        open: &Delimiter,
618        body: &MathNode,
619        close: &Delimiter,
620        style: MathStyle,
621    ) -> Result<Item, Error> {
622        let body_b = self.layout(body, style)?;
623        let s = self.params.scale(style);
624        let axis = &self.params.axis_height * &s;
625        let above = (&body_b.height - &axis).clamp_nonneg();
626        let below = &body_b.depth + &axis;
627        let needed = above.max(&below) * Dim::from_i64(2);
628        let left = self.delim_box(open, &needed, style)?;
629        let right = self.delim_box(close, &needed, style)?;
630        Ok(Item {
631            class: Some(AtomKind::Inner),
632            bx: MathBox::hpack(vec![left, body_b, right]),
633        })
634    }
635
636    fn explicit_delim_span(&self, size: DelimSize, style: MathStyle) -> Dim {
637        let em = self.params.em(style);
638        let n = match size {
639            DelimSize::Big => 12,
640            DelimSize::Big2 => 18,
641            DelimSize::Bigg => 24,
642            DelimSize::Bigg2 => 30,
643        };
644        em * Dim::from_i64(n) / Dim::from_i64(10)
645    }
646
647    fn delim_box(&self, d: &Delimiter, needed: &Dim, style: MathStyle) -> Result<MathBox, Error> {
648        match d {
649            Delimiter::Empty => Ok(MathBox::empty()),
650            Delimiter::Char(c) => self.sized_glyph(*c, needed, style),
651            Delimiter::Named(n) => {
652                let ch = named_delim(n)?;
653                self.sized_glyph(ch, needed, style)
654            }
655        }
656    }
657
658    fn sized_glyph(&self, ch: char, needed: &Dim, style: MathStyle) -> Result<MathBox, Error> {
659        let base = self.font.glyph(ch)?;
660        let mut best = self.glyph(ch, style)?;
661        let mut best_span = &best.height + &best.depth;
662        for gid in self.font.vertical_variants(base.glyph_id) {
663            let cand = self.glyph_id(ch, gid, style)?;
664            let span = &cand.height + &cand.depth;
665            let meets = span.cmp(needed).is_some_and(|o| o != Ordering::Less);
666            let best_short = best_span.cmp(needed).is_some_and(|o| o == Ordering::Less);
667            let tighter = span.cmp(&best_span).is_some_and(|o| o == Ordering::Less);
668            let taller = span.cmp(&best_span).is_some_and(|o| o == Ordering::Greater);
669            if (meets && (best_short || tighter)) || (best_short && taller) {
670                best_span = span;
671                best = cand;
672            }
673        }
674        Ok(best)
675    }
676
677    fn stretch_h(&self, base: MathBox, needed: &Dim, style: MathStyle) -> Result<MathBox, Error> {
678        let BoxContent::Glyph { ch, glyph_id } = base.content else {
679            return Ok(base);
680        };
681        let mut best = base;
682        let mut best_w = best.width.clone();
683        for gid in self.font.horizontal_variants(glyph_id) {
684            let cand = self.glyph_id(ch, gid, style)?;
685            let wider_needed = best_w.cmp(needed).is_some_and(|o| o == Ordering::Less);
686            let fits = cand.width.cmp(needed).is_some_and(|o| o != Ordering::Less);
687            let tighter = cand.width.cmp(&best_w).is_some_and(|o| o == Ordering::Less);
688            let longer = cand
689                .width
690                .cmp(&best_w)
691                .is_some_and(|o| o == Ordering::Greater);
692            if (fits && (wider_needed || tighter)) || (wider_needed && longer) {
693                best_w = cand.width.clone();
694                best = cand;
695            }
696        }
697        if best_w.cmp(needed).is_some_and(|o| o != Ordering::Less) {
698            return Ok(best);
699        }
700        if let Some(asm) = self.assemble_h(ch, glyph_id, needed, style) {
701            if asm
702                .width
703                .cmp(&best_w)
704                .is_some_and(|o| o == Ordering::Greater)
705            {
706                return Ok(asm);
707            }
708        }
709        Ok(best)
710    }
711
712    fn assemble_h(&self, ch: char, gid: u16, needed: &Dim, style: MathStyle) -> Option<MathBox> {
713        let parts = self.font.horizontal_assembly_parts(gid);
714        if parts.is_empty() {
715            return None;
716        }
717        let s = self.params.scale(style);
718        let fu = |n: u16| Dim::from_font_units(i64::from(n), self.params.units_per_em) * &s;
719        let sequence = |copies: u32| {
720            let mut v = Vec::new();
721            for &(id, start, end, adv, ext) in &parts {
722                if ext {
723                    for _ in 0..copies {
724                        v.push((id, start, end, adv));
725                    }
726                } else {
727                    v.push((id, start, end, adv));
728                }
729            }
730            v
731        };
732        let width_of = |seq: &[(u16, u16, u16, u16)]| {
733            if seq.is_empty() {
734                return Dim::zero();
735            }
736            let mut w = fu(seq[0].3);
737            for i in 1..seq.len() {
738                let overlap = seq[i - 1].2.min(seq[i].1);
739                w = &w + &fu(seq[i].3) - &fu(overlap);
740            }
741            w
742        };
743        let mut copies = 0u32;
744        while copies < 64 {
745            if width_of(&sequence(copies))
746                .cmp(needed)
747                .is_some_and(|o| o != Ordering::Less)
748            {
749                break;
750            }
751            copies += 1;
752        }
753        let seq = sequence(copies);
754        if seq.is_empty() {
755            return None;
756        }
757        let mut kids = Vec::new();
758        for (i, &(id, start, _, _)) in seq.iter().enumerate() {
759            if i > 0 {
760                let overlap = seq[i - 1].2.min(start);
761                kids.push(MathBox::kern(-(fu(overlap))));
762            }
763            kids.push(self.glyph_id(ch, id, style).ok()?);
764        }
765        Some(MathBox::hpack(kids))
766    }
767
768    fn large_op(
769        &self,
770        ch: char,
771        lo: Option<&MathNode>,
772        hi: Option<&MathNode>,
773        style: MathStyle,
774        limits_in_display: bool,
775    ) -> Result<Item, Error> {
776        let min_h = if style.is_display() {
777            self.params.display_operator_min_height.clone() * self.params.scale(style)
778        } else {
779            Dim::zero()
780        };
781        let op = self.sized_glyph(ch, &min_h, style)?;
782        let use_limits = limits_in_display && style.is_display();
783        self.attach_limits(op, hi, lo, style, use_limits)
784    }
785
786    fn attach_limits(
787        &self,
788        op: MathBox,
789        over: Option<&MathNode>,
790        under: Option<&MathNode>,
791        style: MathStyle,
792        as_limits: bool,
793    ) -> Result<Item, Error> {
794        if !as_limits {
795            return self.attach_scripts_to_box(op, Some(AtomKind::Op), under, over, style);
796        }
797        let s = self.params.scale(style);
798        let op_h = op.height.clone();
799        let op_d = op.depth.clone();
800        let over_b = match over {
801            Some(o) => Some(self.layout(o, style.into_script())?),
802            None => None,
803        };
804        let under_b = match under {
805            Some(u) => Some(self.layout(u, style.into_script())?),
806            None => None,
807        };
808        let mut width = op.width.clone();
809        if let Some(ref o) = over_b {
810            width = width.max(&o.width);
811        }
812        if let Some(ref u) = under_b {
813            width = width.max(&u.width);
814        }
815        let mut height = op_h.clone();
816        let mut depth = op_d.clone();
817        let mut kids = vec![center_in(op, &width)];
818        if let Some(ob) = over_b {
819            let gap = self.params.upper_limit_gap_min.clone() * &s;
820            let rise = self.params.upper_limit_baseline_rise_min.clone() * &s;
821            let extra = gap.max(&rise);
822            height = &height + &ob.height + &ob.depth + &extra;
823            let sh = &op_h + &extra + &ob.depth;
824            kids.push(center_in(ob, &width).with_shift(sh));
825        }
826        if let Some(ub) = under_b {
827            let gap = self.params.lower_limit_gap_min.clone() * &s;
828            let drop = self.params.lower_limit_baseline_drop_min.clone() * &s;
829            let extra = gap.max(&drop);
830            depth = &depth + &ub.height + &ub.depth + &extra;
831            let sh = -(&op_d + &extra + &ub.height);
832            kids.push(center_in(ub, &width).with_shift(sh));
833        }
834        Ok(Item {
835            class: Some(AtomKind::Op),
836            bx: MathBox {
837                width,
838                height,
839                depth,
840                italic: Dim::zero(),
841                shift: Dim::zero(),
842                content: BoxContent::Overlap(kids),
843            },
844        })
845    }
846
847    fn not_overlay(&self, base: &MathNode, b: MathBox, style: MathStyle) -> Result<Item, Error> {
848        let slash = match self.glyph('\u{0338}', style) {
849            Ok(bx) => bx,
850            Err(_) => self.glyph('/', style)?,
851        };
852        let width = b.width.max(&slash.width);
853        let two = Dim::from_i64(2);
854        let b_axis = &(&b.height - &b.depth) / &two;
855        let s_axis = &(&slash.height - &slash.depth) / &two;
856        let raise = &b_axis - &s_axis;
857        let height = b.height.max(&(&slash.height + &raise).max(&slash.height));
858        let depth = b.depth.max(&(&slash.depth - &raise).clamp_nonneg());
859        Ok(Item {
860            class: class_of(base),
861            bx: MathBox {
862                width: width.clone(),
863                height,
864                depth,
865                italic: Dim::zero(),
866                shift: Dim::zero(),
867                content: BoxContent::Overlap(vec![
868                    center_in(b, &width),
869                    center_in(slash, &width).with_shift(raise),
870                ]),
871            },
872        })
873    }
874
875    fn accent(&self, base: &MathNode, kind: AccentKind, style: MathStyle) -> Result<Item, Error> {
876        let nucleus_style = if is_tex_accent(kind) {
877            style.cramp()
878        } else {
879            style
880        };
881        let b = self.layout(base, nucleus_style)?;
882        if kind == AccentKind::Not {
883            return self.not_overlay(base, b, style);
884        }
885        if kind == AccentKind::Boxed {
886            return Ok(Item {
887                class: Some(AtomKind::Ord),
888                bx: self.boxed_frame(b, style),
889            });
890        }
891        if matches!(
892            kind,
893            AccentKind::Cancel | AccentKind::BCancel | AccentKind::XCancel
894        ) {
895            let mut kids = vec![b.clone()];
896            kids.extend(self.cancel_lines(&b, kind, style));
897            return Ok(Item {
898                class: Some(AtomKind::Ord),
899                bx: MathBox {
900                    width: b.width.clone(),
901                    height: b.height.clone(),
902                    depth: b.depth.clone(),
903                    italic: Dim::zero(),
904                    shift: Dim::zero(),
905                    content: BoxContent::Overlap(kids),
906                },
907            });
908        }
909        if matches!(kind, AccentKind::Overline | AccentKind::Underline) {
910            return self.bar_rule(b, kind == AccentKind::Underline, style);
911        }
912        let mut acc = self.accent_glyph(kind, style)?;
913        let stretchy = is_stretchy_accent(kind);
914        if stretchy {
915            acc = self.stretch_h(acc, &b.width, style)?;
916        }
917        if is_under_accent(kind) {
918            return Ok(Item {
919                class: Some(AtomKind::Ord),
920                bx: self.place_under(b, acc, style),
921            });
922        }
923        let x_off = if stretchy {
924            let extra = &b.width - &acc.width;
925            &extra / &Dim::from_i64(2)
926        } else {
927            self.accent_x_off(&b, &acc, kind)
928        };
929        let raise = self.accent_raise(&b, &acc, style);
930        Ok(Item {
931            class: Some(AtomKind::Ord),
932            bx: overlay_accent(b, acc, x_off, raise),
933        })
934    }
935
936    fn accent_glyph(&self, kind: AccentKind, style: MathStyle) -> Result<MathBox, Error> {
937        for &ch in accent_candidates(kind) {
938            if let Ok(bx) = self.glyph(ch, style) {
939                return Ok(bx);
940            }
941        }
942        Err(Error::Unsupported {
943            what: format!("accent {}", kind.gold()),
944        })
945    }
946
947    fn cancelto(&self, value: &MathNode, expr: &MathNode, style: MathStyle) -> Result<Item, Error> {
948        let b = self.layout(expr, style)?;
949        let val = self.layout(value, style.into_script())?;
950        let mut kids = vec![b.clone()];
951        kids.extend(self.cancel_lines(&b, AccentKind::Cancel, style));
952        let gap = self.params.space_after_script.clone() * self.params.scale(style);
953        let val_x = &b.width + &gap;
954        let val_shift = &b.height + &val.depth;
955        let val_w = val.width.clone();
956        let val_h = val.height.clone();
957        kids.push(shift_x(val, val_x.clone()).with_shift(val_shift.clone()));
958        let width = &val_x + &val_w;
959        let height = b.height.max(&(&val_shift + &val_h));
960        Ok(Item {
961            class: Some(AtomKind::Ord),
962            bx: MathBox {
963                width,
964                height,
965                depth: b.depth.clone(),
966                italic: Dim::zero(),
967                shift: Dim::zero(),
968                content: BoxContent::Overlap(kids),
969            },
970        })
971    }
972
973    fn boxed_frame(&self, inner: MathBox, style: MathStyle) -> MathBox {
974        let pad = self.params.mu(style) * Dim::from_i64(3);
975        let thick = self.params.fraction_rule_thickness.clone() * self.params.scale(style);
976        frame_wrap(thick, None, pad_box(inner, &pad))
977    }
978
979    fn bar_rule(&self, b: MathBox, under: bool, style: MathStyle) -> Result<Item, Error> {
980        let s = self.params.scale(style);
981        let gap = if under {
982            self.params.underbar_vertical_gap.clone() * &s
983        } else {
984            self.params.overbar_vertical_gap.clone() * &s
985        };
986        let thick = if under {
987            self.params.underbar_rule_thickness.clone() * &s
988        } else {
989            self.params.overbar_rule_thickness.clone() * &s
990        };
991        let extra = if under {
992            self.params.underbar_extra_descender.clone() * &s
993        } else {
994            self.params.overbar_extra_ascender.clone() * &s
995        };
996        let mut height = b.height.clone();
997        let mut depth = b.depth.clone();
998        let bar = if under {
999            depth = &depth + &gap + &thick + &extra;
1000            MathBox::rule(b.width.clone(), thick, Dim::zero()).with_shift(-(&b.depth + &gap))
1001        } else {
1002            height = &height + &gap + &thick + &extra;
1003            MathBox::rule(b.width.clone(), thick, Dim::zero()).with_shift(&b.height + &gap)
1004        };
1005        Ok(Item {
1006            class: Some(AtomKind::Ord),
1007            bx: MathBox {
1008                width: b.width.clone(),
1009                height,
1010                depth,
1011                italic: Dim::zero(),
1012                shift: Dim::zero(),
1013                content: BoxContent::Overlap(vec![b, bar]),
1014            },
1015        })
1016    }
1017
1018    fn cancel_lines(&self, b: &MathBox, kind: AccentKind, style: MathStyle) -> Vec<MathBox> {
1019        let t = self.params.fraction_rule_thickness.clone() * self.params.scale(style);
1020        let w = b.width.clone();
1021        let h = b.height.clone();
1022        let d = b.depth.clone();
1023        let mk = |x1: Dim, y1: Dim, x2: Dim, y2: Dim| MathBox {
1024            width: w.clone(),
1025            height: h.clone(),
1026            depth: d.clone(),
1027            italic: Dim::zero(),
1028            shift: Dim::zero(),
1029            content: BoxContent::Line {
1030                x1,
1031                y1,
1032                x2,
1033                y2,
1034                thickness: t.clone(),
1035            },
1036        };
1037        match kind {
1038            AccentKind::Cancel => vec![mk(Dim::zero(), -d.clone(), w.clone(), h.clone())],
1039            AccentKind::BCancel => vec![mk(Dim::zero(), h.clone(), w.clone(), -d.clone())],
1040            AccentKind::XCancel => vec![
1041                mk(Dim::zero(), -d.clone(), w.clone(), h.clone()),
1042                mk(Dim::zero(), h.clone(), w.clone(), -d.clone()),
1043            ],
1044            _ => Vec::new(),
1045        }
1046    }
1047
1048    fn accent_x_off(&self, base: &MathBox, acc: &MathBox, kind: AccentKind) -> Dim {
1049        let two = Dim::from_i64(2);
1050        let base_att = first_glyph_id(base)
1051            .and_then(|id| self.font.top_accent_attachment(id))
1052            .unwrap_or_else(|| (&base.width + &base.italic) / &two);
1053        let acc_att = first_glyph_id(acc)
1054            .and_then(|id| self.font.top_accent_attachment(id))
1055            .unwrap_or_else(|| &acc.width / &two);
1056        match kind {
1057            AccentKind::Vec | AccentKind::Overrightarrow | AccentKind::Underrightarrow => {
1058                &base.width + &base.italic - &acc.width
1059            }
1060            AccentKind::Acute => &base_att - &acc_att + &(&acc.width / &Dim::from_i64(4)),
1061            AccentKind::Grave => &base_att - &acc_att - &(&acc.width / &Dim::from_i64(4)),
1062            _ => &base_att - &acc_att,
1063        }
1064    }
1065
1066    fn accent_raise(&self, base: &MathBox, acc: &MathBox, style: MathStyle) -> Dim {
1067        let s = self.params.scale(style);
1068        let abh = if style.is_cramped() {
1069            &self.params.flattened_accent_base_height * &s
1070        } else {
1071            &self.params.accent_base_height * &s
1072        };
1073        if acc.width.is_zero() {
1074            (&base.height - &abh).clamp_nonneg()
1075        } else {
1076            base.height.max(&abh)
1077        }
1078    }
1079
1080    fn place_under(&self, base: MathBox, acc: MathBox, style: MathStyle) -> MathBox {
1081        let s = self.params.scale(style);
1082        let gap = self.params.underbar_vertical_gap.clone() * &s;
1083        let extra = self.params.underbar_extra_descender.clone() * &s;
1084        let raise = -(&base.depth + &gap + &acc.height);
1085        let width = base.width.max(&acc.width);
1086        let depth = &base.depth + &gap + &acc.height + &acc.depth + &extra;
1087        MathBox {
1088            width: width.clone(),
1089            height: base.height.clone(),
1090            depth,
1091            italic: Dim::zero(),
1092            shift: Dim::zero(),
1093            content: BoxContent::Overlap(vec![
1094                center_in(base, &width),
1095                center_in(acc, &width).with_shift(raise),
1096            ]),
1097        }
1098    }
1099
1100    fn take_number(&self) -> Option<String> {
1101        let i = self.idx.get();
1102        self.idx.set(i + 1);
1103        self.numbers.assigned(i).map(str::to_string)
1104    }
1105
1106    fn number_box(&self, s: &str, style: MathStyle) -> Result<MathBox, Error> {
1107        Ok(self.text_run(s, TextStyle::Rm, style)?.bx)
1108    }
1109
1110    fn attach_number(&self, body: MathBox, num: Option<MathBox>, style: MathStyle) -> MathBox {
1111        let Some(num) = num else {
1112            return body;
1113        };
1114        let gap = self.params.em(style);
1115        MathBox::hpack(vec![body, MathBox::kern(gap), num])
1116    }
1117
1118    fn reference(&self, key: &str, style: MathStyle) -> Result<Item, Error> {
1119        let s = self.numbers.lookup(key).ok_or_else(|| Error::Unsupported {
1120            what: format!("undefined label {key}"),
1121        })?;
1122        self.text_run(s, TextStyle::Rm, style)
1123    }
1124
1125    fn tag_box(&self, star: bool, body: &MathNode, style: MathStyle) -> Result<Item, Error> {
1126        let inner = self.layout(body, MathStyle::Text)?;
1127        let bx = if star {
1128            inner
1129        } else {
1130            let open = self.glyph('(', style)?;
1131            let close = self.glyph(')', style)?;
1132            MathBox::hpack(vec![open, inner, close])
1133        };
1134        Ok(Item {
1135            class: Some(AtomKind::Ord),
1136            bx,
1137        })
1138    }
1139
1140    fn substack(&self, lines: &[MathNode], style: MathStyle) -> Result<Item, Error> {
1141        let ss = style.into_script();
1142        let sep = self.params.em(ss) / Dim::from_i64(5);
1143        let mut laid = Vec::new();
1144        for ln in lines {
1145            laid.push(self.layout(ln, ss)?);
1146        }
1147        let width = laid.iter().fold(Dim::zero(), |w, b| w.max(&b.width));
1148        let mut rows = Vec::new();
1149        for (i, b) in laid.into_iter().enumerate() {
1150            if i > 0 {
1151                rows.push(MathBox {
1152                    width: Dim::zero(),
1153                    height: Dim::zero(),
1154                    depth: sep.clone(),
1155                    italic: Dim::zero(),
1156                    shift: Dim::zero(),
1157                    content: BoxContent::Empty,
1158                });
1159            }
1160            rows.push(align_in(b, &width, ColSpec::Center));
1161        }
1162        Ok(Item {
1163            class: Some(AtomKind::Ord),
1164            bx: MathBox::vpack(rows),
1165        })
1166    }
1167
1168    fn matrix(
1169        &self,
1170        style_m: MatrixStyle,
1171        spec: &[ColSpec],
1172        rows: &[EnvRow],
1173        style: MathStyle,
1174    ) -> Result<Item, Error> {
1175        if rows.is_empty() {
1176            return Ok(Item {
1177                bx: MathBox::empty(),
1178                class: Some(AtomKind::Inner),
1179            });
1180        }
1181        let body_style = if style_m.is_display_env() {
1182            MathStyle::Display
1183        } else {
1184            style
1185        };
1186        match style_m {
1187            MatrixStyle::Align => self.align_env(rows, body_style, true),
1188            MatrixStyle::Aligned | MatrixStyle::Split => self.align_env(rows, body_style, false),
1189            MatrixStyle::Gather => self.gather_env(rows, body_style),
1190            MatrixStyle::Multline => self.multline_env(rows, body_style),
1191            MatrixStyle::Equation => self.equation_env(rows, body_style),
1192            MatrixStyle::Array => self.array_env(spec, rows, body_style),
1193            MatrixStyle::Cases => self.cases_env(rows, body_style),
1194            _ => self.centered_matrix(style_m, rows, body_style),
1195        }
1196    }
1197
1198    fn centered_matrix(
1199        &self,
1200        style_m: MatrixStyle,
1201        rows: &[EnvRow],
1202        style: MathStyle,
1203    ) -> Result<Item, Error> {
1204        let data = data_cells(rows)?;
1205        self.grid(
1206            &data,
1207            style,
1208            None,
1209            self.params.mu(style) * Dim::from_i64(10),
1210            ColSpec::Center,
1211            matrix_delims(style_m),
1212            false,
1213        )
1214    }
1215
1216    fn cases_env(&self, rows: &[EnvRow], style: MathStyle) -> Result<Item, Error> {
1217        let data = data_cells(rows)?;
1218        self.grid(
1219            &data,
1220            style,
1221            None,
1222            self.params.em(style),
1223            ColSpec::Left,
1224            (Some('{'), None),
1225            false,
1226        )
1227    }
1228
1229    fn align_env(&self, rows: &[EnvRow], style: MathStyle, numbered: bool) -> Result<Item, Error> {
1230        let mut math_rows: Vec<Vec<MathBox>> = Vec::new();
1231        let mut nums: Vec<Option<MathBox>> = Vec::new();
1232        let mut extras: Vec<RowKind> = Vec::new();
1233        let mut ncols = 0;
1234        for row in rows {
1235            match row {
1236                EnvRow::Hline => {
1237                    extras.push(RowKind::Hline);
1238                }
1239                EnvRow::Intertext(n) => {
1240                    extras.push(RowKind::Intertext(self.layout(n, MathStyle::Text)?));
1241                }
1242                EnvRow::Cells { cells, .. } => {
1243                    let mut rboxes = Vec::new();
1244                    for c in cells {
1245                        rboxes.push(self.layout(c, style)?);
1246                    }
1247                    ncols = ncols.max(rboxes.len());
1248                    math_rows.push(rboxes);
1249                    extras.push(RowKind::Cells);
1250                    if numbered {
1251                        nums.push(match self.take_number() {
1252                            Some(s) => Some(self.number_box(&s, MathStyle::Text)?),
1253                            None => None,
1254                        });
1255                    }
1256                }
1257            }
1258        }
1259        let mut col_w = vec![Dim::zero(); ncols];
1260        for row in &math_rows {
1261            for (j, cell) in row.iter().enumerate() {
1262                col_w[j] = col_w[j].max(&cell.width);
1263            }
1264        }
1265        let pair_sep = self.params.em(style);
1266        let row_sep = self.params.em(style) / Dim::from_i64(5);
1267        let mut packed = Vec::new();
1268        let mut mi = 0;
1269        for extra in extras {
1270            match extra {
1271                RowKind::Hline => {
1272                    return Err(Error::Unsupported {
1273                        what: "hline in align".into(),
1274                    });
1275                }
1276                RowKind::Intertext(t) => {
1277                    if !packed.is_empty() {
1278                        packed.push(sep_row(row_sep.clone()));
1279                    }
1280                    packed.push(t);
1281                }
1282                RowKind::Cells => {
1283                    if !packed.is_empty() {
1284                        packed.push(sep_row(row_sep.clone()));
1285                    }
1286                    let mut parts = Vec::new();
1287                    let row = pad_row(math_rows[mi].clone(), ncols);
1288                    for (j, cell) in row.into_iter().enumerate() {
1289                        if j > 0 && j % 2 == 0 {
1290                            parts.push(MathBox::kern(pair_sep.clone()));
1291                        }
1292                        let align = if j % 2 == 0 {
1293                            ColSpec::Right
1294                        } else {
1295                            ColSpec::Left
1296                        };
1297                        parts.push(align_in(cell, &col_w[j], align));
1298                    }
1299                    let mut body = MathBox::hpack(parts);
1300                    if numbered {
1301                        body = self.attach_number(body, nums[mi].clone(), style);
1302                    }
1303                    packed.push(body);
1304                    mi += 1;
1305                }
1306            }
1307        }
1308        Ok(Item {
1309            class: Some(AtomKind::Inner),
1310            bx: MathBox::vpack(packed),
1311        })
1312    }
1313
1314    fn gather_env(&self, rows: &[EnvRow], style: MathStyle) -> Result<Item, Error> {
1315        let row_sep = self.params.em(style) / Dim::from_i64(5);
1316        let mut bodies = Vec::new();
1317        let mut kinds = Vec::new();
1318        for row in rows {
1319            match row {
1320                EnvRow::Hline => {
1321                    return Err(Error::Unsupported {
1322                        what: "hline in gather".into(),
1323                    });
1324                }
1325                EnvRow::Intertext(n) => {
1326                    kinds.push(RowKind::Intertext(self.layout(n, MathStyle::Text)?));
1327                }
1328                EnvRow::Cells { cells, .. } => {
1329                    let node = if cells.len() == 1 {
1330                        cells[0].clone()
1331                    } else {
1332                        MathNode::Row(cells.clone())
1333                    };
1334                    bodies.push(self.layout(&node, style)?);
1335                    kinds.push(RowKind::Cells);
1336                }
1337            }
1338        }
1339        let max_w = bodies.iter().fold(Dim::zero(), |w, b| w.max(&b.width));
1340        let mut packed = Vec::new();
1341        let mut bi = 0;
1342        for kind in kinds {
1343            if !packed.is_empty() {
1344                packed.push(sep_row(row_sep.clone()));
1345            }
1346            match kind {
1347                RowKind::Intertext(t) => packed.push(t),
1348                RowKind::Cells => {
1349                    let body = align_in(bodies[bi].clone(), &max_w, ColSpec::Center);
1350                    let num = match self.take_number() {
1351                        Some(s) => Some(self.number_box(&s, MathStyle::Text)?),
1352                        None => None,
1353                    };
1354                    packed.push(self.attach_number(body, num, style));
1355                    bi += 1;
1356                }
1357                RowKind::Hline => {}
1358            }
1359        }
1360        Ok(Item {
1361            class: Some(AtomKind::Inner),
1362            bx: MathBox::vpack(packed),
1363        })
1364    }
1365
1366    fn multline_env(&self, rows: &[EnvRow], style: MathStyle) -> Result<Item, Error> {
1367        let data = data_cells(rows)?;
1368        if data.is_empty() {
1369            let _ = self.take_number();
1370            return Ok(Item {
1371                bx: MathBox::empty(),
1372                class: Some(AtomKind::Inner),
1373            });
1374        }
1375        let mut bodies = Vec::new();
1376        for row in &data {
1377            let node = if row.len() == 1 {
1378                row[0].clone()
1379            } else {
1380                MathNode::Row(row.clone())
1381            };
1382            bodies.push(self.layout(&node, style)?);
1383        }
1384        let max_w = bodies.iter().fold(Dim::zero(), |w, b| w.max(&b.width));
1385        let n = bodies.len();
1386        let row_sep = self.params.em(style) / Dim::from_i64(5);
1387        let mut packed = Vec::new();
1388        for (i, b) in bodies.into_iter().enumerate() {
1389            if i > 0 {
1390                packed.push(sep_row(row_sep.clone()));
1391            }
1392            let align = if i == 0 {
1393                ColSpec::Left
1394            } else if i + 1 == n {
1395                ColSpec::Right
1396            } else {
1397                ColSpec::Center
1398            };
1399            packed.push(align_in(b, &max_w, align));
1400        }
1401        let mut inner = MathBox::vpack(packed);
1402        let num = match self.take_number() {
1403            Some(s) => Some(self.number_box(&s, MathStyle::Text)?),
1404            None => None,
1405        };
1406        inner = self.attach_number(inner, num, style);
1407        Ok(Item {
1408            class: Some(AtomKind::Inner),
1409            bx: inner,
1410        })
1411    }
1412
1413    fn equation_env(&self, rows: &[EnvRow], style: MathStyle) -> Result<Item, Error> {
1414        let data = data_cells(rows)?;
1415        let mut parts = Vec::new();
1416        for row in &data {
1417            for (i, c) in row.iter().enumerate() {
1418                if i > 0 {
1419                    parts.push(MathNode::Space(SpaceKind::Quad));
1420                }
1421                parts.push(c.clone());
1422            }
1423        }
1424        let node = wrap_nodes(parts);
1425        let body = self.layout(&node, style)?;
1426        let num = match self.take_number() {
1427            Some(s) => Some(self.number_box(&s, MathStyle::Text)?),
1428            None => None,
1429        };
1430        Ok(Item {
1431            class: Some(AtomKind::Inner),
1432            bx: self.attach_number(body, num, style),
1433        })
1434    }
1435
1436    fn array_env(
1437        &self,
1438        spec: &[ColSpec],
1439        rows: &[EnvRow],
1440        style: MathStyle,
1441    ) -> Result<Item, Error> {
1442        let thick = self.params.fraction_rule_thickness.clone() * self.params.scale(style);
1443        let col_sep = self.params.mu(style) * Dim::from_i64(10);
1444        let row_sep = self.params.em(style) / Dim::from_i64(5);
1445        let mut kinds = Vec::new();
1446        let mut data: Vec<Vec<MathBox>> = Vec::new();
1447        let mut ncols_data = 0;
1448        for row in rows {
1449            match row {
1450                EnvRow::Hline => kinds.push(RowKind::Hline),
1451                EnvRow::Intertext(n) => {
1452                    kinds.push(RowKind::Intertext(self.layout(n, MathStyle::Text)?));
1453                }
1454                EnvRow::Cells { cells, .. } => {
1455                    let mut rboxes = Vec::new();
1456                    for c in cells {
1457                        rboxes.push(self.layout(c, style)?);
1458                    }
1459                    ncols_data = ncols_data.max(rboxes.len());
1460                    data.push(rboxes);
1461                    kinds.push(RowKind::Cells);
1462                }
1463            }
1464        }
1465        let mut spec: Vec<ColSpec> = if spec.is_empty() {
1466            vec![ColSpec::Center; ncols_data]
1467        } else {
1468            spec.to_vec()
1469        };
1470        let mut ndata = spec.iter().filter(|c| !c.is_rule()).count();
1471        while ndata < ncols_data {
1472            spec.push(ColSpec::Center);
1473            ndata += 1;
1474        }
1475        for row in &mut data {
1476            while row.len() < ndata {
1477                row.push(MathBox::empty());
1478            }
1479        }
1480        let mut col_w: Vec<Dim> = spec
1481            .iter()
1482            .map(|c| {
1483                if c.is_rule() {
1484                    thick.clone()
1485                } else {
1486                    Dim::zero()
1487                }
1488            })
1489            .collect();
1490        for row in &data {
1491            let mut dj = 0;
1492            for (j, sp) in spec.iter().enumerate() {
1493                if sp.is_rule() {
1494                    continue;
1495                }
1496                if dj < row.len() {
1497                    col_w[j] = col_w[j].max(&row[dj].width);
1498                }
1499                dj += 1;
1500            }
1501        }
1502        let mut table_w = Dim::zero();
1503        for (j, _) in spec.iter().enumerate() {
1504            if j > 0 && !spec[j].is_rule() && !spec[j - 1].is_rule() {
1505                table_w = &table_w + &col_sep;
1506            }
1507            table_w = &table_w + &col_w[j];
1508        }
1509        let mut packed = Vec::new();
1510        let mut di = 0;
1511        for kind in kinds {
1512            match kind {
1513                RowKind::Hline => {
1514                    packed.push(MathBox::rule(table_w.clone(), thick.clone(), Dim::zero()));
1515                }
1516                RowKind::Intertext(t) => {
1517                    if !packed.is_empty() {
1518                        packed.push(sep_row(row_sep.clone()));
1519                    }
1520                    packed.push(t);
1521                }
1522                RowKind::Cells => {
1523                    if !packed.is_empty()
1524                        && !matches!(packed.last().map(|b| &b.content), Some(BoxContent::Rule))
1525                    {
1526                        packed.push(sep_row(row_sep.clone()));
1527                    }
1528                    let row = &data[di];
1529                    let mut rh = Dim::zero();
1530                    let mut rd = Dim::zero();
1531                    for c in row {
1532                        rh = rh.max(&c.height);
1533                        rd = rd.max(&c.depth);
1534                    }
1535                    let mut parts = Vec::new();
1536                    let mut dj = 0;
1537                    for (j, sp) in spec.iter().enumerate() {
1538                        if j > 0 && !sp.is_rule() && !spec[j - 1].is_rule() {
1539                            parts.push(MathBox::kern(col_sep.clone()));
1540                        }
1541                        if sp.is_rule() {
1542                            parts.push(MathBox {
1543                                width: thick.clone(),
1544                                height: rh.clone(),
1545                                depth: rd.clone(),
1546                                italic: Dim::zero(),
1547                                shift: Dim::zero(),
1548                                content: BoxContent::Rule,
1549                            });
1550                        } else {
1551                            let cell = row.get(dj).cloned().unwrap_or_else(MathBox::empty);
1552                            parts.push(align_in(cell, &col_w[j], *sp));
1553                            dj += 1;
1554                        }
1555                    }
1556                    packed.push(MathBox::hpack(parts));
1557                    di += 1;
1558                }
1559            }
1560        }
1561        Ok(Item {
1562            class: Some(AtomKind::Inner),
1563            bx: MathBox::vpack(packed),
1564        })
1565    }
1566
1567    #[allow(clippy::too_many_arguments)]
1568    fn grid(
1569        &self,
1570        rows: &[Vec<MathNode>],
1571        style: MathStyle,
1572        spec: Option<&[ColSpec]>,
1573        col_sep: Dim,
1574        default_align: ColSpec,
1575        delims: (Option<char>, Option<char>),
1576        numbered: bool,
1577    ) -> Result<Item, Error> {
1578        if rows.is_empty() {
1579            return Ok(Item {
1580                bx: MathBox::empty(),
1581                class: Some(AtomKind::Inner),
1582            });
1583        }
1584        let ncols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
1585        let mut cells: Vec<Vec<MathBox>> = Vec::new();
1586        for row in rows {
1587            let mut rboxes = Vec::new();
1588            for c in 0..ncols {
1589                if c < row.len() {
1590                    rboxes.push(self.layout(&row[c], style)?);
1591                } else {
1592                    rboxes.push(MathBox::empty());
1593                }
1594            }
1595            cells.push(rboxes);
1596        }
1597        let mut col_w: Vec<Dim> = vec![Dim::zero(); ncols];
1598        for row in &cells {
1599            for (j, cell) in row.iter().enumerate() {
1600                col_w[j] = col_w[j].max(&cell.width);
1601            }
1602        }
1603        let row_sep = self.params.em(style) / Dim::from_i64(5);
1604        let mut row_boxes = Vec::new();
1605        for (ri, row) in cells.into_iter().enumerate() {
1606            let mut parts = Vec::new();
1607            for (j, cell) in row.into_iter().enumerate() {
1608                if j > 0 {
1609                    parts.push(MathBox::kern(col_sep.clone()));
1610                }
1611                let align = spec
1612                    .and_then(|s| s.get(j).copied())
1613                    .unwrap_or(default_align);
1614                parts.push(align_in(cell, &col_w[j], align));
1615            }
1616            if ri > 0 {
1617                row_boxes.push(sep_row(row_sep.clone()));
1618            }
1619            let mut packed = MathBox::hpack(parts);
1620            if numbered {
1621                let num = match self.take_number() {
1622                    Some(s) => Some(self.number_box(&s, MathStyle::Text)?),
1623                    None => None,
1624                };
1625                packed = self.attach_number(packed, num, style);
1626            }
1627            row_boxes.push(packed);
1628        }
1629        let mut inner = MathBox::vpack(row_boxes);
1630        let needed = &inner.height + &inner.depth;
1631        let (ld, rd) = delims;
1632        if let Some(l) = ld {
1633            let left = self.sized_glyph(l, &needed, style)?;
1634            let right = match rd {
1635                Some(r) => self.sized_glyph(r, &needed, style)?,
1636                None => MathBox::empty(),
1637            };
1638            inner = MathBox::hpack(vec![left, inner, right]);
1639        }
1640        Ok(Item {
1641            class: Some(AtomKind::Inner),
1642            bx: inner,
1643        })
1644    }
1645}
1646
1647enum RowKind {
1648    Cells,
1649    Hline,
1650    Intertext(MathBox),
1651}
1652
1653fn data_cells(rows: &[EnvRow]) -> Result<Vec<Vec<MathNode>>, Error> {
1654    let mut out = Vec::new();
1655    for r in rows {
1656        match r {
1657            EnvRow::Cells { cells, .. } => out.push(cells.clone()),
1658            EnvRow::Hline => {
1659                return Err(Error::Unsupported {
1660                    what: "hline in this environment".into(),
1661                });
1662            }
1663            EnvRow::Intertext(_) => {
1664                return Err(Error::Unsupported {
1665                    what: "intertext in this environment".into(),
1666                });
1667            }
1668        }
1669    }
1670    Ok(out)
1671}
1672
1673fn sep_row(depth: Dim) -> MathBox {
1674    MathBox {
1675        width: Dim::zero(),
1676        height: Dim::zero(),
1677        depth,
1678        italic: Dim::zero(),
1679        shift: Dim::zero(),
1680        content: BoxContent::Empty,
1681    }
1682}
1683
1684fn pad_row(mut row: Vec<MathBox>, ncols: usize) -> Vec<MathBox> {
1685    while row.len() < ncols {
1686        row.push(MathBox::empty());
1687    }
1688    row
1689}
1690
1691fn wrap_nodes(items: Vec<MathNode>) -> MathNode {
1692    if items.len() == 1 {
1693        items
1694            .into_iter()
1695            .next()
1696            .unwrap_or(MathNode::Row(Vec::new()))
1697    } else {
1698        MathNode::Row(items)
1699    }
1700}
1701
1702fn align_in(inner: MathBox, width: &Dim, align: ColSpec) -> MathBox {
1703    if matches!(align, ColSpec::VRule) || inner.width.eq_dim(width) {
1704        return inner;
1705    }
1706    let extra = width - &inner.width;
1707    match align {
1708        ColSpec::Center => center_in(inner, width),
1709        ColSpec::Left => {
1710            let h = inner.height.clone();
1711            let d = inner.depth.clone();
1712            let packed = MathBox::hpack(vec![inner, MathBox::kern(extra)]);
1713            MathBox {
1714                width: packed.width,
1715                height: h,
1716                depth: d,
1717                italic: Dim::zero(),
1718                shift: Dim::zero(),
1719                content: packed.content,
1720            }
1721        }
1722        ColSpec::Right => {
1723            let h = inner.height.clone();
1724            let d = inner.depth.clone();
1725            let packed = MathBox::hpack(vec![MathBox::kern(extra), inner]);
1726            MathBox {
1727                width: packed.width,
1728                height: h,
1729                depth: d,
1730                italic: Dim::zero(),
1731                shift: Dim::zero(),
1732                content: packed.content,
1733            }
1734        }
1735        ColSpec::VRule => inner,
1736    }
1737}
1738
1739fn color_wrap(c: Color, inner: MathBox) -> MathBox {
1740    MathBox {
1741        width: inner.width.clone(),
1742        height: inner.height.clone(),
1743        depth: inner.depth.clone(),
1744        italic: inner.italic.clone(),
1745        shift: inner.shift.clone(),
1746        content: BoxContent::Color(c, Box::new(inner)),
1747    }
1748}
1749
1750fn back_color_wrap(c: Color, inner: MathBox) -> MathBox {
1751    MathBox {
1752        width: inner.width.clone(),
1753        height: inner.height.clone(),
1754        depth: inner.depth.clone(),
1755        italic: inner.italic.clone(),
1756        shift: inner.shift.clone(),
1757        content: BoxContent::BackColor(c, Box::new(inner)),
1758    }
1759}
1760
1761fn frame_wrap(thickness: Dim, stroke: Option<Color>, inner: MathBox) -> MathBox {
1762    MathBox {
1763        width: inner.width.clone(),
1764        height: inner.height.clone(),
1765        depth: inner.depth.clone(),
1766        italic: Dim::zero(),
1767        shift: Dim::zero(),
1768        content: BoxContent::Frame {
1769            thickness,
1770            stroke,
1771            inner: Box::new(inner),
1772        },
1773    }
1774}
1775
1776fn pad_box(inner: MathBox, pad: &Dim) -> MathBox {
1777    let w = &inner.width + pad + pad;
1778    let h = &inner.height + pad;
1779    let d = &inner.depth + pad;
1780    MathBox {
1781        width: w,
1782        height: h,
1783        depth: d,
1784        italic: Dim::zero(),
1785        shift: Dim::zero(),
1786        content: BoxContent::HList(vec![
1787            MathBox::kern(pad.clone()),
1788            inner,
1789            MathBox::kern(pad.clone()),
1790        ]),
1791    }
1792}
1793
1794fn center_in(inner: MathBox, width: &Dim) -> MathBox {
1795    if inner.width.eq_dim(width) {
1796        return inner;
1797    }
1798    let extra = width - &inner.width;
1799    let half = &extra / &Dim::from_i64(2);
1800    let h = inner.height.clone();
1801    let d = inner.depth.clone();
1802    let packed = MathBox::hpack(vec![
1803        MathBox::kern(half.clone()),
1804        inner,
1805        MathBox::kern(&extra - &half),
1806    ]);
1807    MathBox {
1808        width: packed.width,
1809        height: h,
1810        depth: d,
1811        italic: Dim::zero(),
1812        shift: Dim::zero(),
1813        content: packed.content,
1814    }
1815}
1816
1817fn shift_x(inner: MathBox, x: Dim) -> MathBox {
1818    if x.is_zero() {
1819        return inner;
1820    }
1821    let h = inner.height.clone();
1822    let d = inner.depth.clone();
1823    let packed = MathBox::hpack(vec![MathBox::kern(x), inner]);
1824    MathBox {
1825        width: packed.width,
1826        height: h,
1827        depth: d,
1828        italic: Dim::zero(),
1829        shift: Dim::zero(),
1830        content: packed.content,
1831    }
1832}
1833
1834fn overlay_accent(base: MathBox, acc: MathBox, x_off: Dim, raise: Dim) -> MathBox {
1835    let origin = (-x_off.clone()).clamp_nonneg();
1836    let base_x = origin.clone();
1837    let acc_x = &origin + &x_off;
1838    let width = (&base_x + &base.width).max(&(&acc_x + &acc.width));
1839    let acc_top = &acc.height + &raise;
1840    let acc_bot = &raise - &acc.depth;
1841    let height = base.height.max(&acc_top);
1842    let depth = base.depth.max(&(-acc_bot).clamp_nonneg());
1843    MathBox {
1844        width,
1845        height,
1846        depth,
1847        italic: Dim::zero(),
1848        shift: Dim::zero(),
1849        content: BoxContent::Overlap(vec![
1850            shift_x(base, base_x),
1851            shift_x(acc, acc_x).with_shift(raise),
1852        ]),
1853    }
1854}
1855
1856fn first_glyph_id(b: &MathBox) -> Option<u16> {
1857    match &b.content {
1858        BoxContent::Glyph { glyph_id, .. } => Some(*glyph_id),
1859        BoxContent::HList(v) | BoxContent::VList(v) | BoxContent::Overlap(v) => {
1860            v.iter().find_map(first_glyph_id)
1861        }
1862        BoxContent::Color(_, inner)
1863        | BoxContent::BackColor(_, inner)
1864        | BoxContent::Frame { inner, .. } => first_glyph_id(inner),
1865        _ => None,
1866    }
1867}
1868
1869fn is_tex_accent(kind: AccentKind) -> bool {
1870    matches!(
1871        kind,
1872        AccentKind::Hat
1873            | AccentKind::Check
1874            | AccentKind::Breve
1875            | AccentKind::Acute
1876            | AccentKind::Grave
1877            | AccentKind::Tilde
1878            | AccentKind::Bar
1879            | AccentKind::Vec
1880            | AccentKind::Dot
1881            | AccentKind::Ddot
1882            | AccentKind::Dddot
1883            | AccentKind::Ddddot
1884            | AccentKind::Ring
1885            | AccentKind::WideHat
1886            | AccentKind::WideTilde
1887            | AccentKind::Overleftarrow
1888            | AccentKind::Overrightarrow
1889            | AccentKind::Overleftrightarrow
1890            | AccentKind::Underleftarrow
1891            | AccentKind::Underrightarrow
1892            | AccentKind::Underleftrightarrow
1893            | AccentKind::Overbrace
1894            | AccentKind::Underbrace
1895    )
1896}
1897
1898fn is_stretchy_accent(kind: AccentKind) -> bool {
1899    matches!(
1900        kind,
1901        AccentKind::WideHat
1902            | AccentKind::WideTilde
1903            | AccentKind::Overleftarrow
1904            | AccentKind::Overrightarrow
1905            | AccentKind::Overleftrightarrow
1906            | AccentKind::Underleftarrow
1907            | AccentKind::Underrightarrow
1908            | AccentKind::Underleftrightarrow
1909            | AccentKind::Overbrace
1910            | AccentKind::Underbrace
1911    )
1912}
1913
1914fn is_under_accent(kind: AccentKind) -> bool {
1915    matches!(
1916        kind,
1917        AccentKind::Underleftarrow
1918            | AccentKind::Underrightarrow
1919            | AccentKind::Underleftrightarrow
1920            | AccentKind::Underbrace
1921    )
1922}
1923
1924fn accent_candidates(kind: AccentKind) -> &'static [char] {
1925    match kind {
1926        AccentKind::Hat | AccentKind::WideHat => &['ˆ', '\u{0302}'],
1927        AccentKind::Check => &['ˇ', '\u{030C}'],
1928        AccentKind::Breve => &['˘', '\u{0306}'],
1929        AccentKind::Acute => &['´', '\u{0301}'],
1930        AccentKind::Grave => &['`', '\u{0300}'],
1931        AccentKind::Tilde | AccentKind::WideTilde => &['˜', '\u{0303}'],
1932        AccentKind::Bar => &['¯', '\u{0304}'],
1933        AccentKind::Vec => &['→', '\u{20D7}'],
1934        AccentKind::Dot => &['˙', '\u{0307}'],
1935        AccentKind::Ddot => &['¨', '\u{0308}'],
1936        AccentKind::Dddot => &['\u{20DB}'],
1937        AccentKind::Ddddot => &['\u{20DC}'],
1938        AccentKind::Ring => &['˚', '\u{030A}'],
1939        AccentKind::Overleftarrow | AccentKind::Underleftarrow => &['←', '\u{27F5}'],
1940        AccentKind::Overrightarrow | AccentKind::Underrightarrow => &['→', '\u{27F6}'],
1941        AccentKind::Overleftrightarrow | AccentKind::Underleftrightarrow => &['↔', '\u{27F7}'],
1942        AccentKind::Overbrace => &['⏞'],
1943        AccentKind::Underbrace => &['⏟'],
1944        AccentKind::Not
1945        | AccentKind::Overline
1946        | AccentKind::Underline
1947        | AccentKind::Cancel
1948        | AccentKind::BCancel
1949        | AccentKind::XCancel
1950        | AccentKind::Boxed => &[],
1951    }
1952}
1953
1954fn class_of(n: &MathNode) -> Option<AtomKind> {
1955    match n {
1956        MathNode::Atom(_, k) => Some(*k),
1957        MathNode::Symbol(s) => Some(symbol_class(s)),
1958        MathNode::Operator(_, _)
1959        | MathNode::Sum(_, _)
1960        | MathNode::Product(_, _)
1961        | MathNode::Integral(_, _, _)
1962        | MathNode::Limit(_) => Some(AtomKind::Op),
1963        MathNode::Fraction(_, _)
1964        | MathNode::Radical(_, _)
1965        | MathNode::Matrix(_, _, _)
1966        | MathNode::Substack(_)
1967        | MathNode::Delimited(_, _, _) => Some(AtomKind::Inner),
1968        MathNode::SizedDelim(_, _, k) => Some(*k),
1969        MathNode::Superscript(b, _)
1970        | MathNode::Subscript(b, _)
1971        | MathNode::SubSup(b, _, _)
1972        | MathNode::Accent(b, _)
1973        | MathNode::OverUnder(b, _, _)
1974        | MathNode::CancelTo(_, b)
1975        | MathNode::Tag { body: b, .. }
1976        | MathNode::Intertext(b) => class_of(b),
1977        MathNode::Text(_, _) | MathNode::Ref(_) => Some(AtomKind::Ord),
1978        MathNode::Color(_, b)
1979        | MathNode::TextColor(_, b)
1980        | MathNode::ColorBox(_, b)
1981        | MathNode::FColorBox(_, _, b)
1982        | MathNode::Phantom(_, b) => class_of(b),
1983        MathNode::Row(v) if v.len() == 1 => class_of(&v[0]),
1984        MathNode::Row(_) => Some(AtomKind::Ord),
1985        MathNode::Space(_)
1986        | MathNode::Strut(_, _)
1987        | MathNode::Label(_)
1988        | MathNode::NoNumber
1989        | MathNode::Hline => None,
1990    }
1991}
1992
1993fn single_glyph(name: &str) -> Option<char> {
1994    let e = lookup(name)?;
1995    let mut chars = e.glyph.chars();
1996    match (chars.next(), chars.next()) {
1997        (Some(c), None) => Some(c),
1998        _ => None,
1999    }
2000}
2001
2002fn symbol_char(name: &str) -> Result<char, Error> {
2003    single_glyph(name).ok_or_else(|| Error::Unsupported {
2004        what: format!("symbol \\{name}"),
2005    })
2006}
2007
2008fn symbol_class(name: &str) -> AtomKind {
2009    symbol_atom_kind(name)
2010}
2011
2012fn named_delim(n: &str) -> Result<char, Error> {
2013    let c = match n {
2014        "{" => '{',
2015        "}" => '}',
2016        "|" | "Vert" | "lVert" | "rVert" => '‖',
2017        "vert" | "lvert" | "rvert" => '|',
2018        "langle" => '⟨',
2019        "rangle" => '⟩',
2020        "lfloor" => '⌊',
2021        "rfloor" => '⌋',
2022        "lceil" => '⌈',
2023        "rceil" => '⌉',
2024        "backslash" => '\\',
2025        "uparrow" => '↑',
2026        "downarrow" => '↓',
2027        "Uparrow" => '⇑',
2028        "Downarrow" => '⇓',
2029        "updownarrow" => '↕',
2030        "Updownarrow" => '⇕',
2031        other => {
2032            return Err(Error::Unsupported {
2033                what: format!("delimiter {other}"),
2034            });
2035        }
2036    };
2037    Ok(c)
2038}
2039
2040fn matrix_delims(s: MatrixStyle) -> (Option<char>, Option<char>) {
2041    match s {
2042        MatrixStyle::Pmatrix => (Some('('), Some(')')),
2043        MatrixStyle::Bmatrix => (Some('['), Some(']')),
2044        MatrixStyle::Vmatrix => (Some('|'), Some('|')),
2045        MatrixStyle::VVmatrix => (Some('‖'), Some('‖')),
2046        MatrixStyle::BBmatrix => (Some('{'), Some('}')),
2047        MatrixStyle::Cases => (Some('{'), None),
2048        _ => (None, None),
2049    }
2050}