Skip to main content

latex_rust/layout/
engine.rs

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