Skip to main content

retex_engine/
mlist.rs

1//! Converting math lists to horizontal lists (*TeX: The Program*
2//! §699–§767, with e-TeX's `\middle`) the way LuaTeX does it for
3//! traditional (TFM) fonts.
4//!
5//! LuaTeX keeps TeX82's algorithm but takes the font-dependent values from
6//! the `\Umath…` parameters of the current style (see
7//! [`crate::mathparams`]). Observed differences from TeX82, all
8//! established with black-box tests:
9//!
10//! * glyphs made from math characters are protected (subtype 256), so
11//!   that the node processing of an enclosing box leaves them alone;
12//! * italic corrections are kern nodes of subtype "italic", the other
13//!   kerns made here are font kerns;
14//! * a missing character from an undefined family is reported each time
15//!   it is fetched (the field is not reset to empty);
16//! * a null delimiter of a `\left`/`\right` fence is not shifted to the
17//!   axis.
18
19use crate::Engine;
20use crate::build::PackSpec;
21use crate::fonts::LigOp;
22use crate::math::{Field, cramped_style, denom_style, half, num_style, sub_style, sup_style};
23use crate::mathparams::mp;
24use crate::nodes::{
25    AttrList, DEFAULT_CODE, NodeData, NodeRef, fence_sub, glue_sub, id, kern_sub, list_sub,
26    noad_opt, noad_sub, radical_sub,
27};
28use crate::primitives_gen::{assign_dimen as dp, assign_int as ip};
29
30/// Glyph subtype of characters made by `mlist_to_hlist`.
31pub const PROTECTED_GLYPH: u16 = 256;
32
33/// The TeX82 type of a noad, for the two passes.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum NoadType {
36    /// Ord.
37    Ord = 0,
38    /// Op.
39    Op = 1,
40    /// Bin.
41    Bin = 2,
42    /// Rel.
43    Rel = 3,
44    /// Open.
45    Open = 4,
46    /// Close.
47    Close = 5,
48    /// Punct.
49    Punct = 6,
50    /// Inner.
51    Inner = 7,
52    /// Radical.
53    Radical,
54    /// Fraction.
55    Fraction,
56    /// Underline.
57    Under,
58    /// Overline.
59    Over,
60    /// Math accent.
61    Accent,
62    /// `\vcenter`.
63    Vcenter,
64    /// `\left`.
65    Left,
66    /// `\right` or `\middle`.
67    Right,
68}
69
70/// TeX's inter-atom spacing table (§764): 0 none, 1 conditional thin,
71/// 2 thin, 3 conditional medium, 4 conditional thick.
72const MATH_SPACING: &[u8; 64] = b"0234000122*4000133**3**344*0400400*000000234000111*1111112341011";
73
74impl Engine {
75    /// Whether font `f` is an OpenType math font (it has `MathConstants`),
76    /// which LuaTeX treats differently from TFM fonts.
77    pub fn is_opentype_math_font(&self, f: u32) -> bool {
78        self.font(f).math_constants.is_some()
79    }
80
81    /// The spacing between atoms of classes `l` and `r` in `style`: the
82    /// `\Umath…spacing` parameter when set (plain glue), otherwise TeX's
83    /// table with the current `\thinmuskip`, `\medmuskip` or
84    /// `\thickmuskip` (named glue), or none.
85    pub fn math_spacing(&self, l: u32, r: u32, style: u32) -> Option<(crate::glue::Glue, u16)> {
86        let p = 49 + l * 8 + r;
87        let v = self.math_param_value(p, style);
88        if v != crate::codes::UNDEFINED_MATH_PARAM {
89            return Some((self.math_param_glue(v), 0));
90        }
91        let c = MATH_SPACING[(l * 8 + r) as usize];
92        let x = match c {
93            b'1' if style < 4 => glue_sub::THIN_MU_SKIP,
94            b'2' => glue_sub::THIN_MU_SKIP,
95            b'3' if style < 4 => glue_sub::MED_MU_SKIP,
96            b'4' if style < 4 => glue_sub::THICK_MU_SKIP,
97            _ => return None,
98        };
99        let par = match x {
100            glue_sub::THIN_MU_SKIP => crate::primitives_gen::assign_mu_glue::THINMUSKIP,
101            glue_sub::MED_MU_SKIP => crate::primitives_gen::assign_mu_glue::MEDMUSKIP,
102            _ => crate::primitives_gen::assign_mu_glue::THICKMUSKIP,
103        };
104        Some((self.eqtb.glue(par + crate::eqtb::MU_GLUE_OFFSET), x))
105    }
106}
107
108/// The state of the conversion (`cur_style`, `cur_size`, `cur_mu`).
109#[derive(Clone, Copy, Debug, Default)]
110pub struct MathState {
111    /// The current style.
112    pub style: i32,
113    /// The current size: 0 text, 1 script, 2 scriptscript.
114    pub size: u32,
115    /// The math unit.
116    pub mu: i32,
117    /// A kern to put after the operator box being converted (an OpenType
118    /// operator's italic correction, negated, before its subscript).
119    pub op_kern: i32,
120}
121
122/// The size belonging to a style.
123pub fn style_size(s: i32) -> u32 {
124    if s < 4 { 0 } else { ((s - 2) / 2) as u32 }
125}
126
127impl Engine {
128    /// The TeX82 type of noad `q` (`None` for nodes).
129    pub fn noad_type(&self, q: NodeRef) -> Option<NoadType> {
130        Some(match self.nodes.id(q) {
131            id::NOAD => match self.nodes.subtype(q) {
132                noad_sub::ORD => NoadType::Ord,
133                noad_sub::OP_DISPLAY_LIMITS | noad_sub::OP_LIMITS | noad_sub::OP_NO_LIMITS => {
134                    NoadType::Op
135                }
136                noad_sub::BIN => NoadType::Bin,
137                noad_sub::REL => NoadType::Rel,
138                noad_sub::OPEN => NoadType::Open,
139                noad_sub::CLOSE => NoadType::Close,
140                noad_sub::PUNCT => NoadType::Punct,
141                noad_sub::INNER => NoadType::Inner,
142                noad_sub::UNDER => NoadType::Under,
143                noad_sub::OVER => NoadType::Over,
144                _ => NoadType::Vcenter,
145            },
146            id::RADICAL => NoadType::Radical,
147            id::ACCENT => NoadType::Accent,
148            id::FRACTION => NoadType::Fraction,
149            id::FENCE => {
150                if self.nodes.subtype(q) == fence_sub::LEFT {
151                    NoadType::Left
152                } else {
153                    NoadType::Right
154                }
155            }
156            _ => return None,
157        })
158    }
159
160    /// Sets up `cur_size` and `cur_mu` for `cur_style`.
161    fn set_cur_style(&mut self, s: i32) {
162        self.ts.math.style = s;
163        self.ts.math.size = style_size(s);
164        let q = self.math_par(mp::QUAD);
165        self.ts.math.mu = retex_arith::x_over_n(q, 18).map_or(0, |r| r.quotient);
166    }
167
168    /// Math parameter `p` in the current style (0 when undefined).
169    pub fn math_par(&self, p: u32) -> i32 {
170        let v = self.math_param_value(p, self.ts.math.style as u32);
171        if v == crate::codes::UNDEFINED_MATH_PARAM {
172            0
173        } else {
174            v
175        }
176    }
177
178    /// Math parameter `p` in the current style, `None` when undefined.
179    fn math_par_opt(&self, p: u32) -> Option<i32> {
180        let v = self.math_param_value(p, self.ts.math.style as u32);
181        (v != crate::codes::UNDEFINED_MATH_PARAM).then_some(v)
182    }
183
184    /// Converts mlist `mlist` in style `style` into an hlist, which is
185    /// returned (`mlist_to_hlist`). `penalties` asks for line-break
186    /// penalties after binary operators and relations.
187    pub fn mlist_to_hlist(&mut self, mlist: NodeRef, style: i32, penalties: bool) -> NodeRef {
188        self.fonts.sync_dense();
189        self.init_math_params_once();
190        let saved = self.ts.math;
191        let saved_attr = self.ts.attr_override.clone();
192        let r = self.mlist_to_hlist_rec(mlist, style, penalties);
193        self.ts.math = saved;
194        self.ts.attr_override = saved_attr;
195        r
196    }
197
198    /// The attributes of node `p` (none for a null pointer).
199    fn attr_of(&self, p: NodeRef) -> Option<AttrList> {
200        if p.is_null() {
201            None
202        } else {
203            self.nodes.attr(p).cloned()
204        }
205    }
206
207    /// Makes new nodes take attributes `a`; returns the previous setting.
208    fn attr_set(&mut self, a: Option<AttrList>) -> Option<Option<AttrList>> {
209        self.ts.attr_override.replace(a)
210    }
211
212    /// Makes new nodes take the attributes of `p`; returns the previous
213    /// setting. Observed: LuaTeX gives the nodes it builds for a noad the
214    /// attributes of the noad, or of the kernel or delimiter they come
215    /// from, not the current ones.
216    fn attr_from(&mut self, p: NodeRef) -> Option<Option<AttrList>> {
217        let a = self.attr_of(p);
218        self.attr_set(a)
219    }
220
221    fn attr_restore(&mut self, saved: Option<Option<AttrList>>) {
222        self.ts.attr_override = saved;
223    }
224
225    fn get_new_hlist(&self, q: NodeRef) -> NodeRef {
226        match self.nodes.data(q) {
227            NodeData::Noad(n) => n.new_hlist,
228            NodeData::Fraction(f) => f.new_hlist,
229            NodeData::Fence(f) => f.new_hlist,
230            _ => NodeRef::NULL,
231        }
232    }
233
234    fn set_new_hlist(&mut self, q: NodeRef, p: NodeRef) {
235        match self.nodes.data_mut(q) {
236            NodeData::Noad(n) => n.new_hlist = p,
237            NodeData::Fraction(f) => f.new_hlist = p,
238            NodeData::Fence(f) => f.new_hlist = p,
239            _ => {}
240        }
241    }
242
243    fn mlist_to_hlist_rec(&mut self, mlist: NodeRef, style: i32, penalties: bool) -> NodeRef {
244        let mut q = mlist;
245        let mut r = NodeRef::NULL;
246        let mut r_type = NoadType::Op;
247        let mut max_h = 0i32;
248        let mut max_d = 0i32;
249        self.set_cur_style(style);
250        while !q.is_null() {
251            // reswitch:
252            let mut delta = 0;
253            let Some(mut t) = self.noad_type(q) else {
254                // Nodes that can appear in an mlist.
255                let next = self.first_pass_node(q, &mut max_h, &mut max_d);
256                q = next;
257                continue;
258            };
259            let saved_attr = self.attr_from(q);
260            let mut action = Action::Convert;
261            loop {
262                match t {
263                    NoadType::Bin => {
264                        if matches!(
265                            r_type,
266                            NoadType::Bin
267                                | NoadType::Op
268                                | NoadType::Rel
269                                | NoadType::Open
270                                | NoadType::Punct
271                                | NoadType::Left
272                        ) {
273                            self.nodes.set_subtype(q, noad_sub::ORD);
274                            t = NoadType::Ord;
275                            continue;
276                        }
277                    }
278                    NoadType::Rel | NoadType::Close | NoadType::Punct | NoadType::Right => {
279                        if r_type == NoadType::Bin {
280                            self.nodes.set_subtype(r, noad_sub::ORD);
281                        }
282                        if t == NoadType::Right {
283                            action = Action::DoneWithNoad;
284                        }
285                    }
286                    NoadType::Left => action = Action::DoneWithNoad,
287                    NoadType::Fraction => {
288                        self.make_fraction(q);
289                        action = Action::CheckDimensions;
290                    }
291                    NoadType::Op => {
292                        delta = self.make_op(q);
293                        if self.nodes.subtype(q) == noad_sub::OP_LIMITS {
294                            action = Action::CheckDimensions;
295                        }
296                    }
297                    NoadType::Ord => self.make_ord(q),
298                    NoadType::Open | NoadType::Inner => {}
299                    NoadType::Radical => self.make_radical(q),
300                    NoadType::Over => self.make_over(q),
301                    NoadType::Under => self.make_under(q),
302                    NoadType::Accent => self.make_math_accent(q),
303                    NoadType::Vcenter => self.make_vcenter(q),
304                }
305                break;
306            }
307            if action == Action::Convert {
308                self.convert_nucleus(q, delta);
309            }
310            if action != Action::DoneWithNoad {
311                // check_dimensions:
312                let nh = self.get_new_hlist(q);
313                let (h, d) = self.list_height_depth(nh);
314                if h > max_h {
315                    max_h = h;
316                }
317                if d > max_d {
318                    max_d = d;
319                }
320            }
321            // done_with_noad:
322            r = q;
323            r_type = self.noad_type(r).unwrap_or(NoadType::Ord);
324            if r_type == NoadType::Right {
325                r_type = NoadType::Left;
326                self.set_cur_style(style);
327            }
328            self.attr_restore(saved_attr);
329            q = self.nodes.next(q);
330        }
331        if r_type == NoadType::Bin {
332            self.nodes.set_subtype(r, noad_sub::ORD);
333        }
334        self.second_pass(mlist, style, penalties, max_h, max_d)
335    }
336
337    /// The height and depth an `hpack` of list `p` would have.
338    fn list_height_depth(&mut self, p: NodeRef) -> (i32, i32) {
339        let saved = self.ts.adjust_tail.take();
340        let pre = self.ts.pre_adjust_tail.take();
341        let z = self.hpack_quiet(p);
342        self.ts.adjust_tail = saved;
343        self.ts.pre_adjust_tail = pre;
344        let (h, d) = self
345            .nodes
346            .box_data(z)
347            .map_or((0, 0), |b| (b.height, b.depth));
348        self.nodes.set_list_ptr(z, NodeRef::NULL);
349        self.nodes.free(z);
350        (h, d)
351    }
352
353    /// `hpack(p, natural)` without over/underfull reports (natural size
354    /// never reports anyway).
355    fn hpack_quiet(&mut self, p: NodeRef) -> NodeRef {
356        self.hpack(p, PackSpec::NATURAL)
357    }
358
359    /// First-pass processing of a node in an mlist; returns the next item.
360    fn first_pass_node(&mut self, q: NodeRef, max_h: &mut i32, max_d: &mut i32) -> NodeRef {
361        match self.nodes.data(q) {
362            NodeData::Style { style } => {
363                let s = i32::from(*style);
364                self.set_cur_style(s);
365            }
366            NodeData::Choice(c) => {
367                let c = **c;
368                let k = (self.ts.math.style / 2) as usize;
369                let p = c[k.min(3)];
370                for (i, l) in c.iter().enumerate() {
371                    if i != k.min(3) {
372                        self.nodes.flush_list(*l);
373                    }
374                }
375                let st = self.ts.math.style as u8;
376                *self.nodes.get_mut(q) = crate::nodes::Node {
377                    data: NodeData::Style { style: st },
378                    id: id::STYLE,
379                    subtype: u16::from(st),
380                    ..self.nodes.get(q).clone()
381                };
382                if !p.is_null() {
383                    let z = self.nodes.next(q);
384                    self.nodes.link(q, p);
385                    let t = self.nodes.tail_of(p);
386                    self.nodes.link(t, z);
387                }
388            }
389            NodeData::Rule { height, depth, .. } => {
390                if *height > *max_h {
391                    *max_h = *height;
392                }
393                if *depth > *max_d {
394                    *max_d = *depth;
395                }
396            }
397            NodeData::Glue { spec, .. } => {
398                let spec = *spec;
399                let sub = self.nodes.subtype(q);
400                if sub == glue_sub::MU_GLUE {
401                    let g = self.math_glue(spec, self.ts.math.mu);
402                    if let NodeData::Glue { spec, .. } = self.nodes.data_mut(q) {
403                        *spec = g;
404                    }
405                    self.nodes.set_subtype(q, glue_sub::USER);
406                } else if self.ts.math.size != 0 && sub == glue_sub::COND_MATH_GLUE {
407                    let p = self.nodes.next(q);
408                    if !p.is_null() && matches!(self.nodes.id(p), id::GLUE | id::KERN) {
409                        let n = self.nodes.next(p);
410                        self.nodes.link(q, n);
411                        if n.is_null() {
412                            self.nodes.set_next(q, NodeRef::NULL);
413                        }
414                        self.nodes.set_next(p, NodeRef::NULL);
415                        self.nodes.flush_list(p);
416                    }
417                }
418            }
419            NodeData::Kern { kern, .. } => {
420                if self.nodes.subtype(q) == glue_sub::MU_GLUE {
421                    let w = self.mu_mult(*kern, self.ts.math.mu);
422                    if let NodeData::Kern { kern, .. } = self.nodes.data_mut(q) {
423                        *kern = w;
424                    }
425                    // LuaTeX marks converted \mkern kerns as italic kerns.
426                    self.nodes.set_subtype(q, kern_sub::ITALIC);
427                }
428            }
429            _ => {}
430        }
431        self.nodes.next(q)
432    }
433
434    /// Converts a mu dimension (`mu_mult` of `math_kern`).
435    fn mu_mult(&self, x: i32, m: i32) -> i32 {
436        let r = retex_arith::x_over_n_unchecked(m, 0o200000);
437        let (mut n, mut f) = (r.quotient, r.remainder);
438        if f < 0 {
439            n -= 1;
440            f += 0o200000;
441        }
442        let frac = retex_arith::xn_over_d(x, f, 0o200000).map_or(0, |r| r.quotient);
443        retex_arith::nx_plus_y(n, x, frac).unwrap_or(0)
444    }
445
446    /// Converts mu glue to ordinary glue (`math_glue`).
447    pub fn math_glue(&self, g: crate::glue::Glue, m: i32) -> crate::glue::Glue {
448        let mut r = g;
449        r.width = self.mu_mult(g.width, m);
450        if g.stretch_order == 0 {
451            r.stretch = self.mu_mult(g.stretch, m);
452        }
453        if g.shrink_order == 0 {
454            r.shrink = self.mu_mult(g.shrink, m);
455        }
456        r
457    }
458
459    // ----- fetching characters ------------------------------------------
460
461    /// The font, character and metrics of a `math_char` kernel (`fetch`).
462    /// Reports an undefined family or a missing character.
463    fn fetch(&mut self, k: NodeRef) -> Option<(u32, u32, crate::fonts::CharMetrics)> {
464        let (fam, chr) = match self.nodes.data(k) {
465            NodeData::MathChar { fam, chr } => (*fam, *chr),
466            _ => return None,
467        };
468        let size = self.ts.math.size;
469        let f = self.fam_fnt(fam, size);
470        if f == 0 {
471            self.print_err("");
472            self.print_size(size);
473            self.printer.print_int(i64::from(fam));
474            self.printer.print_str(" is undefined (character ");
475            self.printer.print_int(i64::from(chr));
476            self.printer.print_char(')' as u32);
477            self.help(&[
478                "Somewhere in the math formula just ended, you used the",
479                "stated character from an undefined font family. For example,",
480                "plain TeX doesn't allow \\it or \\sl in subscripts. Proceed,",
481                "and I'll try to forget that I needed that character.",
482            ]);
483            self.error();
484            return None;
485        }
486        match self.fonts.char_metrics(f, chr) {
487            Some(m) => Some((f, chr, *m)),
488            None => {
489                self.char_warning(f, chr);
490                None
491            }
492        }
493    }
494
495    /// Prints `\textfont`, `\scriptfont` or `\scriptscriptfont`
496    /// (`print_size`).
497    pub fn print_size(&mut self, s: u32) {
498        self.print_esc_str(match s {
499            0 => "textfont",
500            1 => "scriptfont",
501            _ => "scriptscriptfont",
502        });
503    }
504
505    /// Reports a missing character (`char_warning`) when
506    /// `\tracinglostchars` is positive.
507    pub fn char_warning(&mut self, f: u32, c: u32) {
508        if self.eqtb.int(ip::TRACINGLOSTCHARS) > 0 {
509            let old = self.eqtb.int(ip::TRACINGONLINE);
510            self.begin_diagnostic();
511            self.printer.print_nl("Missing character: There is no ");
512            self.printer.print_char(c);
513            // Observed: with the code point, as for text characters.
514            self.printer.print_str(&format!(" (U+{c:04X}) in font "));
515            let name = self.font(f).name.clone();
516            self.printer.print_bytes(&name);
517            self.printer.print_char('!' as u32);
518            self.end_diagnostic(false);
519            let _ = old;
520        }
521    }
522
523    /// A protected glyph node for a math character.
524    fn new_math_glyph(&mut self, f: u32, c: u32) -> NodeRef {
525        let g = self.new_char_node(f, c);
526        self.nodes.set_subtype(g, PROTECTED_GLYPH);
527        g
528    }
529
530    /// A box holding one character, whose width includes the italic
531    /// correction (`char_box`).
532    fn char_box(&mut self, f: u32, c: u32) -> NodeRef {
533        let m = self.fonts.char_metrics(f, c).copied().unwrap_or_default();
534        // OpenType math boxes leave out the italic correction (observed).
535        let italic = if self.is_opentype_math_font(f) {
536            0
537        } else {
538            m.italic
539        };
540        let b = self.new_null_box_node(true);
541        let g = self.new_math_glyph(f, c);
542        if let Some(bd) = self.nodes.box_data_mut(b) {
543            bd.width = m.width + italic;
544            bd.height = m.height;
545            bd.depth = m.depth;
546            bd.head = g;
547        }
548        self.nodes.set_subtype(b, list_sub::MATH_CHAR);
549        b
550    }
551
552    /// The TFM tag and remainder of character `c` in font `f`
553    /// (0 none, 1 lig/kern, 2 list, 3 extensible).
554    fn char_tag(&self, f: u32, c: u32) -> (u8, u32) {
555        if let Some(t) = self.tfm_of(f)
556            && let Some(ci) = t.char_info(c)
557        {
558            return (ci.tag, u32::from(ci.remainder));
559        }
560        if let Some(m) = self.math_char(f, c) {
561            if let Some(n) = m.next {
562                return (2, n);
563            }
564            if m.vert.is_some() {
565                return (3, c);
566            }
567        }
568        (0, 0)
569    }
570
571    // ----- boxes ----------------------------------------------------------
572
573    /// Puts a noad field into a box of subtype `sub` in style `s`
574    /// (`clean_box`).
575    fn clean_box(&mut self, k: NodeRef, s: i32, sub: u16) -> NodeRef {
576        let mut q;
577        let kid = if k.is_null() { 0 } else { self.nodes.id(k) };
578        match kid {
579            id::MATH_CHAR | id::MATH_TEXT_CHAR => {
580                let n = self.new_noad(noad_sub::ORD);
581                let kc = self.nodes.copy_node(k);
582                self.set_noad_field(n, Field::Nucleus, kc);
583                q = self.clean_box_mlist(n, s);
584            }
585            id::SUB_BOX => {
586                q = self.nodes.list_ptr_kernel(k);
587                self.nodes.set_kernel_list(k, NodeRef::NULL);
588            }
589            id::SUB_MLIST => {
590                let l = self.nodes.list_ptr_kernel(k);
591                self.nodes.set_kernel_list(k, NodeRef::NULL);
592                q = self.clean_box_mlist(l, s);
593            }
594            _ => {
595                q = self.new_null_box_node(true);
596            }
597        }
598        // found: a new box takes the attributes of its first node
599        // (observed).
600        let x = if q.is_null() {
601            self.hpack_quiet(q)
602        } else if !self.nodes.is_glyph(q)
603            && self.nodes.next(q).is_null()
604            && matches!(self.nodes.id(q), id::HLIST | id::VLIST)
605            && self.nodes.box_data(q).is_some_and(|b| b.shift == 0)
606        {
607            q
608        } else {
609            let saved = self.attr_from(q);
610            let x = self.hpack_quiet(q);
611            self.attr_restore(saved);
612            x
613        };
614        self.nodes.set_subtype(x, sub);
615        // Simplify a trivial box.
616        q = self.nodes.list_ptr(x);
617        if !q.is_null() && self.nodes.is_glyph(q) {
618            let r = self.nodes.next(q);
619            if !r.is_null() && self.nodes.next(r).is_null() && self.nodes.id(r) == id::KERN {
620                self.nodes.free(r);
621                self.nodes.set_next(q, NodeRef::NULL);
622            }
623        }
624        x
625    }
626
627    fn clean_box_mlist(&mut self, l: NodeRef, s: i32) -> NodeRef {
628        let saved = self.ts.math;
629        let r = self.mlist_to_hlist_rec(l, s, false);
630        self.ts.math = saved;
631        r
632    }
633
634    /// Centers box `b` in a box of width `w` (`rebox`).
635    fn rebox(&mut self, b: NodeRef, w: i32) -> NodeRef {
636        let bw = self.nodes.box_data(b).map_or(0, |x| x.width);
637        let lp = self.nodes.list_ptr(b);
638        if bw != w && !lp.is_null() {
639            // The new nodes take the attributes of the box (observed).
640            let saved = self.attr_from(b);
641            let mut b = b;
642            if self.nodes.id(b) == id::VLIST {
643                b = self.hpack_quiet(b);
644            }
645            let mut p = self.nodes.list_ptr(b);
646            let bw = self.nodes.box_data(b).map_or(0, |x| x.width);
647            if self.nodes.is_glyph(p) && self.nodes.next(p).is_null() {
648                let (f, c) = {
649                    let g = self.nodes.glyph(p).expect("glyph");
650                    (g.font, g.chr)
651                };
652                let v = self.fonts.char_metrics(f, c).map_or(0, |m| m.width);
653                if v != bw {
654                    let k = self.new_kern_node(bw - v, kern_sub::FONT);
655                    self.nodes.link(p, k);
656                }
657            }
658            self.nodes.set_list_ptr(b, NodeRef::NULL);
659            self.nodes.free(b);
660            let ss = crate::glue::Glue {
661                stretch: retex_arith::UNITY,
662                stretch_order: crate::glue::FIL,
663                shrink: retex_arith::UNITY,
664                shrink_order: crate::glue::FIL,
665                ..crate::glue::Glue::ZERO
666            };
667            let g1 = self.new_glue_node(ss, glue_sub::USER);
668            self.nodes.link(g1, p);
669            while !self.nodes.next(p).is_null() {
670                p = self.nodes.next(p);
671            }
672            let g2 = self.new_glue_node(ss, glue_sub::USER);
673            self.nodes.link(p, g2);
674            let r = self.hpack(g1, PackSpec::Exactly(w));
675            self.attr_restore(saved);
676            r
677        } else {
678            if let Some(x) = self.nodes.box_data_mut(b) {
679                x.width = w;
680            }
681            b
682        }
683    }
684
685    /// A rule of thickness `t` (`fraction_rule`).
686    fn fraction_rule(&mut self, t: i32) -> NodeRef {
687        self.new_node(
688            id::RULE,
689            0,
690            NodeData::Rule {
691                width: crate::typeset::NULL_FLAG,
692                depth: 0,
693                height: t,
694                dir: 0,
695                index: 0,
696                left: 0,
697                right: 0,
698            },
699        )
700    }
701
702    /// Box `b` under a kern `k`, a rule `t` and a kern `top` (`overbar`).
703    fn overbar(&mut self, b: NodeRef, k: i32, t: i32, top: i32) -> NodeRef {
704        let p = self.new_kern_node(k, kern_sub::FONT);
705        self.nodes.link(p, b);
706        let q = self.fraction_rule(t);
707        self.nodes.link(q, p);
708        let p = self.new_kern_node(top, kern_sub::FONT);
709        self.nodes.link(p, q);
710        self.vpack(p, PackSpec::NATURAL)
711    }
712
713    // ----- delimiters -----------------------------------------------------
714
715    /// A box with the smallest variant of delimiter `d` whose height plus
716    /// depth is at least `v`, built from extensible pieces if needed,
717    /// centered on the axis of size `s` (`var_delimiter`).
718    /// Its nodes take the attributes of the delimiter (none for a null
719    /// one; observed).
720    pub fn var_delimiter(&mut self, d: NodeRef, s: u32, v: i32, shift: bool) -> NodeRef {
721        let saved = self.attr_from(d);
722        let b = self.var_delimiter_box(d, s, v, shift);
723        self.attr_restore(saved);
724        b
725    }
726
727    fn var_delimiter_box(&mut self, d: NodeRef, s: u32, v: i32, shift: bool) -> NodeRef {
728        let (sf, sc, lf, lc) = self.delim_fields(d);
729        let mut best: Option<(u32, u32, bool)> = None;
730        let mut w = 0i32;
731        'search: for (z0, x0) in [(sf, sc), (lf, lc)] {
732            if z0 == 0 && x0 == 0 {
733                continue;
734            }
735            let mut z = s as i32;
736            while z >= 0 {
737                let g = self.fam_fnt(z0, z as u32);
738                if g != 0 {
739                    let mut y = x0;
740                    let mut guard = 0;
741                    loop {
742                        guard += 1;
743                        if guard > 256 {
744                            break;
745                        }
746                        let Some(m) = self.fonts.char_metrics(g, y).copied() else {
747                            break;
748                        };
749                        let (tag, rem) = self.char_tag(g, y);
750                        // An OpenType variant is used when it is large
751                        // enough; the assembly only after the last one.
752                        let opentype = self.is_opentype_math_font(g);
753                        if tag == 3 && !opentype {
754                            best = Some((g, y, true));
755                            break 'search;
756                        }
757                        let u = m.height + m.depth;
758                        if u > w {
759                            best = Some((g, y, false));
760                            w = u;
761                            if u >= v {
762                                break 'search;
763                            }
764                        }
765                        if opentype && self.vert_parts(g, y).is_some() {
766                            best = Some((g, y, true));
767                            break 'search;
768                        }
769                        if tag == 2 {
770                            y = rem;
771                        } else {
772                            break;
773                        }
774                    }
775                }
776                z -= 1;
777            }
778        }
779        let b = match best {
780            Some((f, c, true)) if self.is_opentype_math_font(f) => {
781                let saved = self.ts.math;
782                self.set_cur_style(if s == 0 { 2 } else { 2 * s as i32 + 2 });
783                let overlap = self.math_par(mp::CONNECTOR_OVERLAP_MIN);
784                self.ts.math = saved;
785                self.ot_extensible_box(f, c, v, overlap)
786            }
787            Some((f, c, true)) => self.extensible_box(f, c, v),
788            Some((f, c, false)) => self.char_box(f, c),
789            None => {
790                let b = self.new_null_box_node(true);
791                let nds = self.eqtb.dimen(dp::NULLDELIMITERSPACE);
792                if let Some(bd) = self.nodes.box_data_mut(b) {
793                    bd.width = nds;
794                }
795                self.nodes.set_subtype(b, list_sub::V_DELIMITER);
796                if !shift {
797                    return b;
798                }
799                b
800            }
801        };
802        let saved = self.ts.math;
803        self.set_cur_style(if s == 0 { 2 } else { 2 * s as i32 + 2 });
804        let axis = self.math_par(mp::AXIS);
805        self.ts.math = saved;
806        if let Some(bd) = self.nodes.box_data_mut(b) {
807            bd.shift = half(bd.height - bd.depth) - axis;
808        }
809        b
810    }
811
812    /// For a nucleus that is a list of one accent noad on a character
813    /// (possibly accented itself), that character's font and code.
814    fn nested_accent_char(&mut self, k: NodeRef) -> Option<(u32, u32)> {
815        if k.is_null() || self.nodes.id(k) != id::SUB_MLIST {
816            return None;
817        }
818        let l = self.nodes.list_ptr_kernel(k);
819        if l.is_null()
820            || !self.nodes.next(l).is_null()
821            || self.noad_type(l) != Some(NoadType::Accent)
822        {
823            return None;
824        }
825        let n = self.noad_field(l, Field::Nucleus);
826        if n.is_null() {
827            return None;
828        }
829        if self.nodes.id(n) == id::MATH_CHAR {
830            return self.fetch(n).map(|(f, c, _)| (f, c));
831        }
832        self.nested_accent_char(n)
833    }
834
835    /// The vertical extensible parts of OpenType math character `c`.
836    fn vert_parts(&self, f: u32, c: u32) -> Option<std::rc::Rc<[crate::fonts::ExtPart]>> {
837        self.math_char(f, c)?.vert.clone()
838    }
839
840    /// Builds the vertical assembly of OpenType math character `c` with
841    /// height plus depth at least `v`, as LuaTeX does (observed with Latin
842    /// Modern Math): each extender is repeated the smallest number of
843    /// times that lets the parts, overlapping by at least `overlap`, reach
844    /// `v`; the parts are boxes as high as their glyphs, separated by
845    /// glue that removes the largest overlap the connectors allow and
846    /// stretches back to the smallest; the vbox is set to `v`.
847    fn ot_extensible_box(&mut self, f: u32, c: u32, v: i32, overlap: i32) -> NodeRef {
848        let parts = self
849            .vert_parts(f, c)
850            .unwrap_or_else(|| std::rc::Rc::from(Vec::new()));
851        let expand = |n: usize| -> Vec<crate::fonts::ExtPart> {
852            let mut out = Vec::new();
853            for p in parts.iter() {
854                let k = if p.extender { n } else { 1 };
855                for _ in 0..k {
856                    out.push(*p);
857                }
858            }
859            out
860        };
861        let joint = |a: &crate::fonts::ExtPart, b: &crate::fonts::ExtPart| {
862            let max = a.end.min(b.start).max(0);
863            (max, overlap.min(max))
864        };
865        let largest = |seq: &[crate::fonts::ExtPart]| -> i64 {
866            let mut t: i64 = seq.iter().map(|p| i64::from(p.advance)).sum();
867            for w in seq.windows(2) {
868                t -= i64::from(joint(&w[0], &w[1]).1);
869            }
870            t
871        };
872        let has_extender = parts.iter().any(|p| p.extender);
873        let mut n = 0;
874        let mut seq = expand(n);
875        while has_extender && largest(&seq) < i64::from(v) && n < 1000 {
876            n += 1;
877            seq = expand(n);
878        }
879        // Build the vbox from the top down.
880        let mut head = NodeRef::NULL;
881        let mut tail = NodeRef::NULL;
882        let mut width = 0;
883        for (i, p) in seq.iter().enumerate().rev() {
884            if i + 1 < seq.len() {
885                let (max, min) = joint(p, &seq[i + 1]);
886                let g = crate::glue::Glue {
887                    width: -max,
888                    stretch: max - min,
889                    ..crate::glue::Glue::ZERO
890                };
891                let gn = self.new_glue_node(g, 0);
892                self.nodes.link(tail, gn);
893                tail = gn;
894            }
895            let m = self
896                .fonts
897                .char_metrics(f, p.code)
898                .copied()
899                .unwrap_or_default();
900            let b = self.new_null_box_node(true);
901            self.nodes.set_subtype(b, list_sub::MATH_CHAR);
902            let gl = self.new_math_glyph(f, p.code);
903            // Observed: the box is as high as the glyph (height plus
904            // depth), not as its advance in the assembly, where they differ
905            // (Libertinus Math).
906            if let Some(bd) = self.nodes.box_data_mut(b) {
907                bd.width = m.width;
908                bd.height = m.height + m.depth;
909                bd.depth = 0;
910                bd.head = gl;
911            }
912            width = width.max(m.width);
913            if head.is_null() {
914                head = b;
915            } else {
916                self.nodes.link(tail, b);
917            }
918            tail = b;
919        }
920        let natural = largest(&seq) as i32;
921        let target = v.max(natural.min(v));
922        let b = self.vpack(head, crate::build::PackSpec::Exactly(target));
923        self.nodes.set_subtype(b, list_sub::V_EXTENSIBLE);
924        if let Some(bd) = self.nodes.box_data_mut(b) {
925            bd.width = width;
926        }
927        b
928    }
929
930    /// Builds an extensible character from the recipe of `c` in font `f`
931    /// (§713–§714).
932    fn extensible_box(&mut self, f: u32, c: u32, v: i32) -> NodeRef {
933        let recipe = self
934            .tfm_of(f)
935            .and_then(|t| {
936                let (_, rem) = self.char_tag(f, c);
937                t.exten.get(rem as usize).copied()
938            })
939            .unwrap_or([0, 0, 0, 0]);
940        let [top, mid, bot, rep] = recipe.map(u32::from);
941        let hd = |e: &Engine, ch: u32| {
942            e.fonts
943                .char_metrics(f, ch)
944                .map_or(0, |m| m.height + m.depth)
945        };
946        let b = self.new_null_box_node(false);
947        let u = hd(self, rep);
948        let mut w = 0;
949        let rm = self.fonts.char_metrics(f, rep).copied().unwrap_or_default();
950        let width = rm.width + rm.italic;
951        if bot != 0 {
952            w += hd(self, bot);
953        }
954        if mid != 0 {
955            w += hd(self, mid);
956        }
957        if top != 0 {
958            w += hd(self, top);
959        }
960        let mut n = 0;
961        if u > 0 {
962            while w < v {
963                w += u;
964                n += 1;
965                if mid != 0 {
966                    w += u;
967                }
968            }
969        }
970        if let Some(bd) = self.nodes.box_data_mut(b) {
971            bd.width = width;
972        }
973        if bot != 0 {
974            self.stack_into_box(b, f, bot);
975        }
976        for _ in 0..n {
977            self.stack_into_box(b, f, rep);
978        }
979        if mid != 0 {
980            self.stack_into_box(b, f, mid);
981            for _ in 0..n {
982                self.stack_into_box(b, f, rep);
983            }
984        }
985        if top != 0 {
986            self.stack_into_box(b, f, top);
987        }
988        // LuaTeX gives the whole height to the box (TeX82 keeps the
989        // height of the topmost piece and puts the rest in the depth).
990        if let Some(bd) = self.nodes.box_data_mut(b) {
991            bd.height = w;
992            bd.depth = 0;
993        }
994        self.nodes.set_subtype(b, list_sub::V_EXTENSIBLE);
995        b
996    }
997
998    fn stack_into_box(&mut self, b: NodeRef, f: u32, c: u32) {
999        let p = self.char_box(f, c);
1000        let head = self.nodes.list_ptr(b);
1001        self.nodes.link(p, head);
1002        self.nodes.set_list_ptr(b, p);
1003        let h = self.nodes.box_data(p).map_or(0, |x| x.height);
1004        if let Some(bd) = self.nodes.box_data_mut(b) {
1005            bd.height = h;
1006        }
1007    }
1008
1009    // ----- the make_… procedures -----------------------------------------
1010
1011    fn take_field(&mut self, q: NodeRef, f: Field) -> NodeRef {
1012        let k = self.noad_field(q, f);
1013        self.set_noad_field(q, f, NodeRef::NULL);
1014        k
1015    }
1016
1017    /// Replaces the nucleus of `q` by a `sub_box` holding `b`.
1018    fn set_nucleus_box(&mut self, q: NodeRef, b: NodeRef) {
1019        let old = self.take_field(q, Field::Nucleus);
1020        self.nodes.flush_list(old);
1021        let k = self.new_sub_list(false, b);
1022        self.set_noad_field(q, Field::Nucleus, k);
1023    }
1024
1025    fn make_over(&mut self, q: NodeRef) {
1026        let k = self.noad_field(q, Field::Nucleus);
1027        let st = cramped_style(self.ts.math.style);
1028        let x = self.clean_box(k, st, list_sub::NUCLEUS);
1029        // The bar takes the attributes of the nucleus (observed).
1030        let saved = self.attr_from(k);
1031        let b = self.overbar(
1032            x,
1033            self.math_par(mp::OVERBAR_VGAP),
1034            self.math_par(mp::OVERBAR_RULE),
1035            self.math_par(mp::OVERBAR_KERN),
1036        );
1037        self.attr_restore(saved);
1038        self.nodes.set_subtype(b, list_sub::OVER);
1039        self.set_nucleus_box(q, b);
1040    }
1041
1042    fn make_under(&mut self, q: NodeRef) {
1043        let k = self.noad_field(q, Field::Nucleus);
1044        let x = self.clean_box(k, self.ts.math.style, list_sub::NUCLEUS);
1045        let p = self.new_kern_node(self.math_par(mp::UNDERBAR_VGAP), kern_sub::FONT);
1046        self.nodes.link(x, p);
1047        let r = self.fraction_rule(self.math_par(mp::UNDERBAR_RULE));
1048        self.nodes.link(p, r);
1049        let y = self.vpack(x, PackSpec::NATURAL);
1050        self.nodes.set_subtype(y, list_sub::UNDER);
1051        let (yh, yd) = self
1052            .nodes
1053            .box_data(y)
1054            .map_or((0, 0), |b| (b.height, b.depth));
1055        let xh = self.nodes.box_data(x).map_or(0, |b| b.height);
1056        let delta = yh + yd + self.math_par(mp::UNDERBAR_KERN);
1057        if let Some(b) = self.nodes.box_data_mut(y) {
1058            b.height = xh;
1059            b.depth = delta - xh;
1060        }
1061        self.set_nucleus_box(q, y);
1062    }
1063
1064    fn make_vcenter(&mut self, q: NodeRef) {
1065        let k = self.noad_field(q, Field::Nucleus);
1066        let v = self.nodes.list_ptr_kernel(k);
1067        if v.is_null() || self.nodes.id(v) != id::VLIST {
1068            self.confusion("vcenter");
1069        }
1070        let axis = self.math_par(mp::AXIS);
1071        if let Some(b) = self.nodes.box_data_mut(v) {
1072            let delta = b.height + b.depth;
1073            b.height = axis + half(delta);
1074            b.depth = delta - b.height;
1075        }
1076    }
1077
1078    fn make_radical(&mut self, q: NodeRef) {
1079        let sub = self.nodes.subtype(q);
1080        match sub {
1081            radical_sub::UUNDERDELIMITER
1082            | radical_sub::UOVERDELIMITER
1083            | radical_sub::UDELIMITERUNDER
1084            | radical_sub::UDELIMITEROVER
1085            | radical_sub::UHEXTENSIBLE => {
1086                self.make_delimiter_radical(q, sub);
1087                return;
1088            }
1089            _ => {}
1090        }
1091        let k = self.noad_field(q, Field::Nucleus);
1092        let x = self.clean_box(k, cramped_style(self.ts.math.style), list_sub::NUCLEUS);
1093        let (xh, xd) = self
1094            .nodes
1095            .box_data(x)
1096            .map_or((0, 0), |b| (b.height, b.depth));
1097        let mut clr = self.math_par(mp::RADICAL_VGAP);
1098        let theta = self.math_par_opt(mp::RADICAL_RULE);
1099        let kern = self.math_par(mp::RADICAL_KERN);
1100        let t = theta.unwrap_or(self.math_par(mp::FRACTION_RULE));
1101        let left = self.nodes.noad(q).map_or(NodeRef::NULL, |n| n.left);
1102        let size = self.ts.math.size;
1103        let y = self.var_delimiter(left, size, xh + xd + clr + t, true);
1104        let (yh, yd) = self
1105            .nodes
1106            .box_data(y)
1107            .map_or((0, 0), |b| (b.height, b.depth));
1108        let theta = if let Some(th) = theta {
1109            // With \Umathradicalrule set (OpenType fonts): the sign's whole
1110            // size counts, and its top is put at the top of the rule.
1111            let delta = yh + yd - th - (xh + xd + clr);
1112            if delta > 0 {
1113                clr += half(delta);
1114            }
1115            if let Some(b) = self.nodes.box_data_mut(y) {
1116                b.shift = yh - (xh + clr + th);
1117            }
1118            th
1119        } else {
1120            // TeX82 assumes the sign's height is the rule thickness. LuaTeX
1121            // gives an extensible sign its whole size as height, so measure
1122            // it from its topmost piece and convert.
1123            let top_h = if self.nodes.id(y) == id::VLIST {
1124                let h = self.nodes.list_ptr(y);
1125                self.nodes.box_data(h).map_or(yh, |b| b.height)
1126            } else {
1127                yh
1128            };
1129            let eq_depth = yh + yd - top_h;
1130            let delta = eq_depth - (xh + xd + clr);
1131            if delta > 0 {
1132                clr += half(delta);
1133            }
1134            if let Some(b) = self.nodes.box_data_mut(y) {
1135                b.shift = -(xh + clr) + (yh - top_h);
1136            }
1137            top_h
1138        };
1139        // The kern above the rule is \Umathradicalkern, the default rule
1140        // thickness for traditional fonts, even where the sign's height
1141        // (the rule) differs from it (observed with newtxmath).
1142        // The bar takes the attributes of the delimiter (observed).
1143        let saved = self.attr_from(left);
1144        let ob = self.overbar(x, clr, theta, kern);
1145        self.attr_restore(saved);
1146        self.nodes.set_subtype(ob, list_sub::RADICAL);
1147        self.nodes.link(y, ob);
1148        let deg = self.noad_field(q, Field::Degree);
1149        let list = if deg.is_null() {
1150            y
1151        } else {
1152            // \Uroot: the degree in scriptscript style.
1153            let r = self.clean_box(deg, 6, list_sub::DEGREE);
1154            let (rw, rh, rd) = self
1155                .nodes
1156                .box_data(r)
1157                .map_or((0, 0, 0), |b| (b.width, b.height, b.depth));
1158            let _ = rd;
1159            let before = self.math_par(mp::RADICAL_DEGREE_BEFORE);
1160            let after = self.math_par(mp::RADICAL_DEGREE_AFTER);
1161            let raise = self.math_par(mp::RADICAL_DEGREE_RAISE);
1162            let (yh2, yd2, ys) = self
1163                .nodes
1164                .box_data(y)
1165                .map_or((0, 0, 0), |b| (b.height, b.depth, b.shift));
1166            let ytot = yh2 + yd2;
1167            let shift = retex_arith::xn_over_d(ytot, raise, 100).map_or(0, |r| r.quotient);
1168            // The degree's baseline is `raise` percent of the sign's size
1169            // above the sign's bottom.
1170            if self.math_par_opt(mp::RADICAL_RULE).is_some() {
1171                if let Some(b) = self.nodes.box_data_mut(r) {
1172                    b.shift = -(shift - (yd2 + ys));
1173                }
1174            } else if let Some(b) = self.nodes.box_data_mut(r) {
1175                b.shift = -(shift - yd2 + b.shift.min(0));
1176            }
1177            let _ = rh;
1178            // The kerns take the attributes of the degree (observed).
1179            let saved = self.attr_from(r);
1180            let k1 = self.new_kern_node(before, kern_sub::FONT);
1181            let k2 = self.new_kern_node(after, kern_sub::FONT);
1182            self.attr_restore(saved);
1183            let _ = rw;
1184            self.nodes.link(k1, r);
1185            self.nodes.link(r, k2);
1186            self.nodes.link(k2, y);
1187            k1
1188        };
1189        let b = self.hpack_quiet(list);
1190        self.set_nucleus_box(q, b);
1191    }
1192
1193    /// `\Uoverdelimiter` and friends: a delimiter above or below the
1194    /// nucleus, as observed in LuaTeX. `\Uoverdelimiter` puts the nucleus,
1195    /// in superscript style, over the delimiter and `\Uunderdelimiter`
1196    /// puts it, in subscript style, under it; `\Udelimiterover` and
1197    /// `\Udelimiterunder` put the delimiter over or under a nucleus in the
1198    /// current style, measuring the gap from the delimiter's baseline.
1199    /// Both boxes take the larger width; the baseline is the delimiter's
1200    /// for the first two, the nucleus's for the others.
1201    fn make_delimiter_radical(&mut self, q: NodeRef, sub: u16) {
1202        let k = self.noad_field(q, Field::Nucleus);
1203        let cs = self.ts.math.style;
1204        let left = self.nodes.noad(q).map_or(NodeRef::NULL, |n| n.left);
1205        let size = self.ts.math.size;
1206        if sub == radical_sub::UHEXTENSIBLE {
1207            let width = self.nodes.noad(q).map_or(0, |n| n.width);
1208            let y = self.h_delimiter(left, size, width);
1209            let b = self.hpack_quiet(y);
1210            self.set_nucleus_box(q, b);
1211            return;
1212        }
1213        let st = match sub {
1214            radical_sub::UOVERDELIMITER => sup_style(cs),
1215            radical_sub::UUNDERDELIMITER => sub_style(cs),
1216            _ => cs,
1217        };
1218        let x = if k.is_null() {
1219            self.new_null_box_node(true)
1220        } else {
1221            self.clean_box(k, st, list_sub::NUCLEUS)
1222        };
1223        // Observed: \Udelimiterover and \Udelimiterunder take the
1224        // delimiter in the script size (like limits), the others in the
1225        // current size; it is made as wide as the nucleus.
1226        let dsize = match sub {
1227            radical_sub::UDELIMITEROVER | radical_sub::UDELIMITERUNDER => (size + 1).min(2),
1228            _ => size,
1229        };
1230        let xw0 = self.nodes.box_data(x).map_or(0, |b| b.width);
1231        let y = self.h_delimiter(left, dsize, xw0);
1232        let dim = |e: &Self, b: NodeRef| {
1233            e.nodes
1234                .box_data(b)
1235                .map_or((0, 0, 0), |b| (b.width, b.height, b.depth))
1236        };
1237        let (xw, xh, xd) = dim(self, x);
1238        let (yw, yh, yd) = dim(self, y);
1239        let w = xw.max(yw);
1240        for b in [x, y] {
1241            if let Some(bd) = self.nodes.box_data_mut(b) {
1242                bd.width = w;
1243            }
1244        }
1245        let over = matches!(
1246            sub,
1247            radical_sub::UOVERDELIMITER | radical_sub::UDELIMITEROVER
1248        );
1249        let (vgap, bgap) = if over {
1250            (
1251                self.math_par(mp::OVER_DELIMITER_VGAP),
1252                self.math_par(mp::OVER_DELIMITER_BGAP),
1253            )
1254        } else {
1255            (
1256                self.math_par(mp::UNDER_DELIMITER_VGAP),
1257                self.math_par(mp::UNDER_DELIMITER_BGAP),
1258            )
1259        };
1260        let (top, bottom, kern) = match sub {
1261            radical_sub::UOVERDELIMITER => (x, y, vgap.max(bgap - yh - xd)),
1262            radical_sub::UUNDERDELIMITER => (y, x, vgap.max(bgap - xh - yd)),
1263            radical_sub::UDELIMITEROVER => (y, x, vgap.max(bgap - xh - (yh + yd)) - yd),
1264            _ => (x, y, vgap.max(bgap - xd - (yh + yd)) - yh),
1265        };
1266        let kk = self.new_kern_node(kern, kern_sub::FONT);
1267        self.nodes.link(top, kk);
1268        self.nodes.link(kk, bottom);
1269        let v = self.vpack(top, PackSpec::NATURAL);
1270        self.nodes.set_subtype(
1271            v,
1272            if over {
1273                list_sub::OVER_DELIMITER
1274            } else {
1275                list_sub::UNDER_DELIMITER
1276            },
1277        );
1278        // Under the nucleus or the delimiter on top, the baseline is that
1279        // of the top box.
1280        if matches!(
1281            sub,
1282            radical_sub::UUNDERDELIMITER | radical_sub::UDELIMITERUNDER
1283        ) {
1284            let th = self.nodes.box_data(top).map_or(0, |b| b.height);
1285            if let Some(b) = self.nodes.box_data_mut(v) {
1286                let tot = b.height + b.depth;
1287                b.height = th;
1288                b.depth = tot - th;
1289            }
1290        }
1291        self.set_nucleus_box(q, v);
1292    }
1293
1294    /// A horizontal delimiter of size `s` at least `w` wide: for an
1295    /// OpenType font the first variant that wide, else the assembly of
1296    /// the last one stretched to `w` (observed: as the vertical one, with
1297    /// the parts in boxes as wide as their glyphs and the connector
1298    /// overlap of the current style); for other fonts the character.
1299    /// Its nodes take the delimiter's attributes.
1300    fn h_delimiter(&mut self, d: NodeRef, s: u32, w: i32) -> NodeRef {
1301        let saved = self.attr_from(d);
1302        let (sf, sc, _, _) = self.delim_fields(d);
1303        let f = self.fam_fnt(sf, s);
1304        let b = if f == 0 || self.fonts.char_metrics(f, sc).is_none() {
1305            self.new_null_box_node(true)
1306        } else if !self.is_opentype_math_font(f) {
1307            self.char_box(f, sc)
1308        } else {
1309            let mut c = sc;
1310            let mut guard = 0;
1311            let mut assembly = None;
1312            loop {
1313                guard += 1;
1314                let m = self.fonts.char_metrics(f, c).copied().unwrap_or_default();
1315                if m.width >= w || guard > 256 {
1316                    break;
1317                }
1318                if let Some(parts) = self.math_char(f, c).and_then(|mc| mc.horiz.clone()) {
1319                    assembly = Some(parts);
1320                    break;
1321                }
1322                match self.math_char(f, c).and_then(|mc| mc.next) {
1323                    Some(n) if self.fonts.char_metrics(f, n).is_some() => c = n,
1324                    _ => break,
1325                }
1326            }
1327            match assembly {
1328                Some(parts) => {
1329                    let overlap = self.math_par(mp::CONNECTOR_OVERLAP_MIN);
1330                    self.ot_h_extensible_box(f, &parts, w, overlap)
1331                }
1332                None => self.char_box(f, c),
1333            }
1334        };
1335        self.attr_restore(saved);
1336        b
1337    }
1338
1339    /// The horizontal assembly of `parts` (left first) in font `f`, `w`
1340    /// wide (see [`Engine::ot_extensible_box`]).
1341    fn ot_h_extensible_box(
1342        &mut self,
1343        f: u32,
1344        parts: &[crate::fonts::ExtPart],
1345        w: i32,
1346        overlap: i32,
1347    ) -> NodeRef {
1348        let expand = |n: usize| -> Vec<crate::fonts::ExtPart> {
1349            let mut out = Vec::new();
1350            for p in parts.iter() {
1351                let k = if p.extender { n } else { 1 };
1352                for _ in 0..k {
1353                    out.push(*p);
1354                }
1355            }
1356            out
1357        };
1358        let joint = |a: &crate::fonts::ExtPart, b: &crate::fonts::ExtPart| {
1359            let max = a.end.min(b.start).max(0);
1360            (max, overlap.min(max))
1361        };
1362        let largest = |seq: &[crate::fonts::ExtPart]| -> i64 {
1363            let mut t: i64 = seq.iter().map(|p| i64::from(p.advance)).sum();
1364            for win in seq.windows(2) {
1365                t -= i64::from(joint(&win[0], &win[1]).1);
1366            }
1367            t
1368        };
1369        let has_extender = parts.iter().any(|p| p.extender);
1370        let mut n = 0;
1371        let mut seq = expand(n);
1372        while has_extender && largest(&seq) < i64::from(w) && n < 1000 {
1373            n += 1;
1374            seq = expand(n);
1375        }
1376        let mut head = NodeRef::NULL;
1377        let mut tail = NodeRef::NULL;
1378        for (i, p) in seq.iter().enumerate() {
1379            if i > 0 {
1380                let (max, min) = joint(&seq[i - 1], p);
1381                let g = crate::glue::Glue {
1382                    width: -max,
1383                    stretch: max - min,
1384                    ..crate::glue::Glue::ZERO
1385                };
1386                let gn = self.new_glue_node(g, 0);
1387                self.nodes.link(tail, gn);
1388                tail = gn;
1389            }
1390            let b = self.char_box(f, p.code);
1391            if head.is_null() {
1392                head = b;
1393            } else {
1394                self.nodes.link(tail, b);
1395            }
1396            tail = b;
1397        }
1398        let natural = largest(&seq) as i32;
1399        let target = w.max(natural.min(w));
1400        let b = self.hpack(head, PackSpec::Exactly(target));
1401        self.nodes.set_subtype(b, list_sub::H_EXTENSIBLE);
1402        b
1403    }
1404
1405    /// Places a math accent (`make_math_accent`).
1406    fn make_math_accent(&mut self, q: NodeRef) {
1407        let acc = self.nodes.noad(q).map_or(NodeRef::NULL, |n| n.accent);
1408        if acc.is_null() {
1409            // A bottom accent only: place it below the nucleus.
1410            self.make_bottom_accent(q);
1411            return;
1412        }
1413        let Some((f, mut c, mut i)) = self.fetch(acc) else {
1414            return;
1415        };
1416        // Compute the amount of skew.
1417        let mut s = 0;
1418        let nuc = self.noad_field(q, Field::Nucleus);
1419        let mut nuc_char = None;
1420        if !nuc.is_null()
1421            && self.nodes.id(nuc) == id::MATH_CHAR
1422            && let Some((nf, nc, _)) = self.fetch(nuc)
1423        {
1424            nuc_char = Some((nf, nc));
1425            let sk = self.font(nf).skew_char;
1426            if sk >= 0
1427                && let Some(LigOp::Kern(k)) = self.lig_kern_step(nf, nc, sk as u32)
1428            {
1429                s = k;
1430            }
1431        }
1432        let st = cramped_style(self.ts.math.style);
1433        let mut char_nucleus = !nuc.is_null() && self.nodes.id(nuc) == id::MATH_CHAR;
1434        if nuc_char.is_none()
1435            && self.is_opentype_math_font(f)
1436            && let Some(fc) = self.nested_accent_char(nuc)
1437        {
1438            // An accent on an accented character attaches like one on the
1439            // character (observed with \hat{\hat{x}}).
1440            nuc_char = Some(fc);
1441            char_nucleus = true;
1442        }
1443        let mut x = self.clean_box(nuc, st, list_sub::NUCLEUS);
1444        let (w, mut h) = self
1445            .nodes
1446            .box_data(x)
1447            .map_or((0, 0), |b| (b.width, b.height));
1448        // Switch to a larger accent if available and appropriate.
1449        let fixed = matches!(
1450            self.nodes.subtype(q),
1451            crate::nodes::accent_sub::FIXED_TOP | crate::nodes::accent_sub::FIXED_BOTH
1452        );
1453        if !fixed {
1454            loop {
1455                let (tag, rem) = self.char_tag(f, c);
1456                if tag != 2 {
1457                    break;
1458                }
1459                let Some(m) = self.fonts.char_metrics(f, rem).copied() else {
1460                    break;
1461                };
1462                if m.width > w {
1463                    break;
1464                }
1465                c = rem;
1466                i = m;
1467            }
1468        }
1469        let _ = i;
1470        let opentype = self.is_opentype_math_font(f);
1471        // OpenType fonts lower the accent by at most AccentBaseHeight.
1472        let xh = if opentype {
1473            self.font(f)
1474                .math_constants
1475                .as_ref()
1476                .and_then(|m| m.get("AccentBaseHeight").copied())
1477                .unwrap_or_else(|| self.x_height(f))
1478        } else {
1479            self.x_height(f)
1480        };
1481        let mut delta = if h < xh { h } else { xh };
1482        let sup = self.noad_field(q, Field::Sup);
1483        let sub = self.noad_field(q, Field::Sub);
1484        if (!sup.is_null() || !sub.is_null())
1485            && !nuc.is_null()
1486            && self.nodes.id(nuc) == id::MATH_CHAR
1487        {
1488            // Swap the subscript and superscript into box x.
1489            self.nodes.flush_list(x);
1490            let n = self.new_noad(noad_sub::ORD);
1491            let nk = self.take_field(q, Field::Nucleus);
1492            let a = self.take_field(q, Field::Sup);
1493            let b = self.take_field(q, Field::Sub);
1494            self.set_noad_field(n, Field::Nucleus, nk);
1495            self.set_noad_field(n, Field::Sup, a);
1496            self.set_noad_field(n, Field::Sub, b);
1497            let k = self.new_sub_list(true, n);
1498            self.set_noad_field(q, Field::Nucleus, k);
1499            x = self.clean_box(k, self.ts.math.style, list_sub::NUCLEUS);
1500            let nh = self.nodes.box_data(x).map_or(0, |b| b.height);
1501            delta = delta + nh - h;
1502            h = nh;
1503        }
1504        // The accent's box takes the accent's attributes (observed).
1505        let saved = self.attr_from(acc);
1506        let y = self.char_box(f, c);
1507        self.attr_restore(saved);
1508        let yw = self.nodes.box_data(y).map_or(0, |b| b.width);
1509        let shift = if opentype {
1510            // Top accent attachments: that of a character nucleus (else
1511            // half its width) minus that of the accent (else half its
1512            // width).
1513            let a = if char_nucleus {
1514                nuc_char
1515                    .and_then(|(nf, nc)| self.math_char(nf, nc).and_then(|m| m.top_accent))
1516                    .unwrap_or(half(w))
1517            } else {
1518                half(w)
1519            };
1520            let b = self
1521                .math_char(f, c)
1522                .and_then(|m| m.top_accent)
1523                .unwrap_or(half(yw));
1524            a - b
1525        } else if char_nucleus {
1526            // For a character nucleus LuaTeX follows TeX82; otherwise it
1527            // centers one unit to the right (observed: half(w - wa) + 1).
1528            s + half(w - yw)
1529        } else {
1530            s + half(w - yw) + 1
1531        };
1532        if let Some(b) = self.nodes.box_data_mut(y) {
1533            b.shift = shift;
1534            b.width = 0;
1535        }
1536        let p = self.new_kern_node(-delta, kern_sub::FONT);
1537        self.nodes.link(p, x);
1538        self.nodes.link(y, p);
1539        let y = self.vpack(y, PackSpec::NATURAL);
1540        self.nodes.set_subtype(y, list_sub::ACCENT);
1541        let mut xw = self.nodes.box_data(x).map_or(0, |b| b.width);
1542        // Observed: an OpenType accent on a character includes the
1543        // character's italic correction unless it ends the list.
1544        if opentype
1545            && !self.nodes.next(q).is_null()
1546            && let Some((nf, nc)) = nuc_char
1547            && self.nodes.id(nuc) == id::MATH_CHAR
1548        {
1549            xw += self.fonts.char_metrics(nf, nc).map_or(0, |m| m.italic);
1550        }
1551        let yh = self.nodes.box_data(y).map_or(0, |b| b.height);
1552        if let Some(b) = self.nodes.box_data_mut(y) {
1553            b.width = xw;
1554        }
1555        if yh < h {
1556            let p = self.new_kern_node(h - yh, kern_sub::FONT);
1557            let l = self.nodes.list_ptr(y);
1558            self.nodes.link(p, l);
1559            self.nodes.set_list_ptr(y, p);
1560            if let Some(b) = self.nodes.box_data_mut(y) {
1561                b.height = h;
1562            }
1563        }
1564        self.set_nucleus_box(q, y);
1565        let bot = self.nodes.noad(q).map_or(NodeRef::NULL, |n| n.bot_accent);
1566        if !bot.is_null() {
1567            self.make_bottom_accent(q);
1568        }
1569    }
1570
1571    /// A bottom accent (`\Umathaccent bottom`): centered below the
1572    /// nucleus.
1573    fn make_bottom_accent(&mut self, q: NodeRef) {
1574        let bot = match self.nodes.noad_mut(q) {
1575            Some(n) => std::mem::replace(&mut n.bot_accent, NodeRef::NULL),
1576            None => return,
1577        };
1578        let Some((f, mut c, _)) = self.fetch(bot) else {
1579            self.nodes.flush_list(bot);
1580            return;
1581        };
1582        let bot_attr = self.attr_of(bot);
1583        self.nodes.flush_list(bot);
1584        let fixed = matches!(
1585            self.nodes.subtype(q),
1586            crate::nodes::accent_sub::FIXED_BOTTOM | crate::nodes::accent_sub::FIXED_BOTH
1587        );
1588        let nuc = self.noad_field(q, Field::Nucleus);
1589        let char_nucleus = !nuc.is_null() && self.nodes.id(nuc) == id::MATH_CHAR;
1590        let nuc_char = if char_nucleus {
1591            self.fetch(nuc).map(|(nf, nc, _)| (nf, nc))
1592        } else {
1593            None
1594        };
1595        // As for top accents, the scripts of a character go inside.
1596        let sup = self.noad_field(q, Field::Sup);
1597        let sub = self.noad_field(q, Field::Sub);
1598        let x = if char_nucleus && (!sup.is_null() || !sub.is_null()) {
1599            let n = self.new_noad(noad_sub::ORD);
1600            let nk = self.take_field(q, Field::Nucleus);
1601            let a = self.take_field(q, Field::Sup);
1602            let b = self.take_field(q, Field::Sub);
1603            self.set_noad_field(n, Field::Nucleus, nk);
1604            self.set_noad_field(n, Field::Sup, a);
1605            self.set_noad_field(n, Field::Sub, b);
1606            let k = self.new_sub_list(true, n);
1607            self.set_noad_field(q, Field::Nucleus, k);
1608            self.clean_box(k, self.ts.math.style, list_sub::NUCLEUS)
1609        } else {
1610            self.clean_box(nuc, self.ts.math.style, list_sub::NUCLEUS)
1611        };
1612        let (bw, xh) = self
1613            .nodes
1614            .box_data(x)
1615            .map_or((0, 0), |b| (b.width, b.height));
1616        // The accent is centred on the character itself, not on the box
1617        // with its scripts (observed).
1618        let w = match nuc_char {
1619            Some((nf, nc)) => self.fonts.char_metrics(nf, nc).map_or(bw, |m| {
1620                if self.is_opentype_math_font(nf) {
1621                    m.width
1622                } else {
1623                    m.width + m.italic
1624                }
1625            }),
1626            None => bw,
1627        };
1628        // Switch to a wider accent if available, as for top accents.
1629        if !fixed {
1630            loop {
1631                let (tag, rem) = self.char_tag(f, c);
1632                if tag != 2 {
1633                    break;
1634                }
1635                match self.fonts.char_metrics(f, rem) {
1636                    Some(m) if m.width <= w => c = rem,
1637                    _ => break,
1638                }
1639            }
1640        }
1641        let saved = self.attr_set(bot_attr);
1642        let y = self.char_box(f, c);
1643        self.attr_restore(saved);
1644        let yw = self.nodes.box_data(y).map_or(0, |b| b.width);
1645        let shift = if self.is_opentype_math_font(f) {
1646            let a = nuc_char
1647                .filter(|_| self.noad_field(q, Field::Sup).is_null())
1648                .and_then(|(nf, nc)| self.math_char(nf, nc).and_then(|m| m.top_accent))
1649                .unwrap_or(half(w));
1650            let b = self
1651                .math_char(f, c)
1652                .and_then(|m| m.top_accent)
1653                .unwrap_or(half(yw));
1654            a - b
1655        } else if nuc_char.is_some() {
1656            half(w - yw)
1657        } else {
1658            w / 2 - yw / 2 + 1
1659        };
1660        if let Some(b) = self.nodes.box_data_mut(y) {
1661            b.shift = shift;
1662            b.width = 0;
1663        }
1664        self.nodes.link(x, y);
1665        // Observed: the nucleus and the accent are packed naturally and
1666        // the box is lowered so that the nucleus keeps its baseline.
1667        let v = self.vpack(x, PackSpec::NATURAL);
1668        self.nodes.set_subtype(v, list_sub::ACCENT);
1669        if let Some(b) = self.nodes.box_data_mut(v) {
1670            b.shift = b.height - xh;
1671            b.width = bw;
1672        }
1673        self.set_nucleus_box(q, v);
1674    }
1675
1676    /// Builds a generalized fraction (`make_fraction`).
1677    fn make_fraction(&mut self, q: NodeRef) {
1678        let (mut thickness, num, denom, left, right, options) = match self.nodes.data(q) {
1679            NodeData::Fraction(f) => (f.thickness, f.num, f.denom, f.left, f.right, f.options),
1680            _ => return,
1681        };
1682        if thickness == DEFAULT_CODE {
1683            thickness = self.math_par(mp::FRACTION_RULE);
1684        }
1685        let cs = self.ts.math.style;
1686        let mut x = self.clean_box(num, num_style(cs), list_sub::NUMERATOR);
1687        let mut z = self.clean_box(denom, denom_style(cs), list_sub::DENOMINATOR);
1688        let (xw, zw) = (
1689            self.nodes.box_data(x).map_or(0, |b| b.width),
1690            self.nodes.box_data(z).map_or(0, |b| b.width),
1691        );
1692        if xw < zw {
1693            x = self.rebox(x, zw);
1694        } else {
1695            z = self.rebox(z, xw);
1696        }
1697        let (mut shift_up, mut shift_down) = if thickness == 0 {
1698            (
1699                self.math_par(mp::STACK_NUM_UP),
1700                self.math_par(mp::STACK_DENOM_DOWN),
1701            )
1702        } else {
1703            (
1704                self.math_par(mp::FRACTION_NUM_UP),
1705                self.math_par(mp::FRACTION_DENOM_DOWN),
1706            )
1707        };
1708        let (xh, xd) = self
1709            .nodes
1710            .box_data(x)
1711            .map_or((0, 0), |b| (b.height, b.depth));
1712        let (zh, zd) = self
1713            .nodes
1714            .box_data(z)
1715            .map_or((0, 0), |b| (b.height, b.depth));
1716        let axis = self.math_par(mp::AXIS);
1717        let delta;
1718        if thickness == 0 {
1719            let clr = self.math_par(mp::STACK_VGAP);
1720            delta = half(clr - ((shift_up - xd) - (zh - shift_down)));
1721            if delta > 0 {
1722                shift_up += delta;
1723                shift_down += delta;
1724            }
1725        } else {
1726            let exact = options & noad_opt::EXACT == noad_opt::EXACT;
1727            let (clr_n, clr_d) = if exact {
1728                (0, 0)
1729            } else if thickness == self.math_par(mp::FRACTION_RULE) {
1730                (
1731                    self.math_par(mp::FRACTION_NUM_VGAP),
1732                    self.math_par(mp::FRACTION_DENOM_VGAP),
1733                )
1734            } else if cs < 2 {
1735                (3 * thickness, 3 * thickness)
1736            } else {
1737                (thickness, thickness)
1738            };
1739            delta = half(thickness);
1740            let delta1 = clr_n - ((shift_up - xd) - (axis + delta));
1741            let delta2 = clr_d - ((axis - delta) - (zh - shift_down));
1742            if delta1 > 0 {
1743                shift_up += delta1;
1744            }
1745            if delta2 > 0 {
1746                shift_down += delta2;
1747            }
1748        }
1749        // Construct a vlist box for the fraction.
1750        let v = self.new_null_box_node(false);
1751        self.nodes.set_subtype(v, list_sub::FRACTION);
1752        let width = self.nodes.box_data(x).map_or(0, |b| b.width);
1753        if let Some(b) = self.nodes.box_data_mut(v) {
1754            b.height = shift_up + xh;
1755            b.depth = zd + shift_down;
1756            b.width = width;
1757        }
1758        let p;
1759        if thickness == 0 {
1760            p = self.new_kern_node((shift_up - xd) - (zh - shift_down), kern_sub::FONT);
1761            self.nodes.link(p, z);
1762        } else {
1763            let y = self.fraction_rule(thickness);
1764            let p2 = self.new_kern_node((axis - delta) - (zh - shift_down), kern_sub::FONT);
1765            self.nodes.link(y, p2);
1766            self.nodes.link(p2, z);
1767            p = self.new_kern_node((shift_up - xd) - (axis + delta), kern_sub::FONT);
1768            self.nodes.link(p, y);
1769        }
1770        self.nodes.link(x, p);
1771        self.nodes.set_list_ptr(v, x);
1772        // Put the fraction into a box with its delimiters.
1773        let dsize = self.math_par(mp::FRACTION_DEL_SIZE);
1774        let size = self.ts.math.size;
1775        // A null delimiter given explicitly (`\atopwithdelims..`) stays
1776        // on the baseline; a missing one (`\atop`) is centered on the axis
1777        // (observed).
1778        let lx = self.var_delimiter(left, size, dsize, left.is_null());
1779        self.nodes.link(lx, v);
1780        let rz = self.var_delimiter(right, size, dsize, right.is_null());
1781        self.nodes.link(v, rz);
1782        let h = self.hpack_quiet(lx);
1783        self.nodes.set_subtype(h, list_sub::FRACTION);
1784        self.set_new_hlist(q, h);
1785    }
1786
1787    /// Builds an operator (`make_op`); returns the italic correction.
1788    fn make_op(&mut self, q: NodeRef) -> i32 {
1789        let cs = self.ts.math.style;
1790        if self.nodes.subtype(q) == noad_sub::OP_DISPLAY_LIMITS && cs < 2 {
1791            self.nodes.set_subtype(q, noad_sub::OP_LIMITS);
1792        }
1793        let limits = self.nodes.subtype(q) == noad_sub::OP_LIMITS;
1794        let nuc = self.noad_field(q, Field::Nucleus);
1795        let mut delta = 0;
1796        let mut opentype = false;
1797        if !nuc.is_null() && self.nodes.id(nuc) == id::MATH_CHAR {
1798            if let Some((f, c, m)) = self.fetch(nuc) {
1799                let mut m = m;
1800                if cs < 2 && self.is_opentype_math_font(f) {
1801                    // Observed (TeX Gyre Termes Math): the first variant
1802                    // at least DisplayOperatorMinHeight high (height plus
1803                    // depth), else the largest.
1804                    let target = self.math_par(mp::OPERATOR_SIZE);
1805                    let mut c = c;
1806                    let mut guard = 0;
1807                    while m.height + m.depth < target && guard < 64 {
1808                        guard += 1;
1809                        let (tag, rem) = self.char_tag(f, c);
1810                        let Some(m2) = self
1811                            .fonts
1812                            .char_metrics(f, rem)
1813                            .copied()
1814                            .filter(|_| tag == 2)
1815                        else {
1816                            break;
1817                        };
1818                        m = m2;
1819                        c = rem;
1820                    }
1821                    if let NodeData::MathChar { chr, .. } = self.nodes.data_mut(nuc) {
1822                        *chr = c;
1823                    }
1824                } else if cs < 2 {
1825                    let (tag, rem) = self.char_tag(f, c);
1826                    if tag == 2
1827                        && let Some(m2) = self.fonts.char_metrics(f, rem).copied()
1828                    {
1829                        m = m2;
1830                        if let NodeData::MathChar { chr, .. } = self.nodes.data_mut(nuc) {
1831                            *chr = rem;
1832                        }
1833                    }
1834                }
1835                delta = m.italic;
1836                opentype = self.is_opentype_math_font(f);
1837            }
1838            let x = self.clean_box(nuc, cs, list_sub::NUCLEUS);
1839            let has_sub = !self.noad_field(q, Field::Sub).is_null();
1840            let axis = self.math_par(mp::AXIS);
1841            if let Some(b) = self.nodes.box_data_mut(x) {
1842                if has_sub && !limits && !opentype {
1843                    b.width -= delta;
1844                }
1845                b.shift = half(b.height - b.depth) - axis;
1846            }
1847            self.set_nucleus_box(q, x);
1848            // An OpenType operator's box has no italic correction; with
1849            // \nolimits the subscript moves left by it (a font kern) and
1850            // a lone superscript is not moved.
1851            if opentype && !limits {
1852                if has_sub {
1853                    self.ts.math.op_kern = -delta;
1854                } else {
1855                    delta = 0;
1856                }
1857            }
1858        }
1859        if limits {
1860            // Construct a box with limits above and below it.
1861            let supk = self.noad_field(q, Field::Sup);
1862            let subk = self.noad_field(q, Field::Sub);
1863            let x = self.clean_box(supk, sup_style(cs), list_sub::SUP);
1864            let nk = self.noad_field(q, Field::Nucleus);
1865            let y = self.clean_box(nk, cs, list_sub::NUCLEUS);
1866            let z = self.clean_box(subk, sub_style(cs), list_sub::SUB);
1867            let v = self.new_null_box_node(false);
1868            self.nodes.set_subtype(v, list_sub::LIMITS);
1869            let wy = self.nodes.box_data(y).map_or(0, |b| b.width);
1870            let wx = self.nodes.box_data(x).map_or(0, |b| b.width);
1871            let wz = self.nodes.box_data(z).map_or(0, |b| b.width);
1872            let wv = wy.max(wx).max(wz);
1873            let x = self.rebox(x, wv);
1874            let y = self.rebox(y, wv);
1875            let z = self.rebox(z, wv);
1876            if let Some(b) = self.nodes.box_data_mut(x) {
1877                b.shift = half(delta);
1878            }
1879            if let Some(b) = self.nodes.box_data_mut(z) {
1880                b.shift = -half(delta);
1881            }
1882            let (yh, yd) = self
1883                .nodes
1884                .box_data(y)
1885                .map_or((0, 0), |b| (b.height, b.depth));
1886            let (mut vh, mut vd) = (yh, yd);
1887            let kern = self.math_par(mp::LIMIT_ABOVE_KERN);
1888            let kern_below = self.math_par(mp::LIMIT_BELOW_KERN);
1889            let head;
1890            if supk.is_null() {
1891                self.nodes.free(x);
1892                head = y;
1893            } else {
1894                let (xh, xd) = self
1895                    .nodes
1896                    .box_data(x)
1897                    .map_or((0, 0), |b| (b.height, b.depth));
1898                let mut shift_up = self.math_par(mp::LIMIT_ABOVE_BGAP) - xd;
1899                let g1 = self.math_par(mp::LIMIT_ABOVE_VGAP);
1900                if shift_up < g1 {
1901                    shift_up = g1;
1902                }
1903                let p = self.new_kern_node(shift_up, kern_sub::FONT);
1904                self.nodes.link(p, y);
1905                self.nodes.link(x, p);
1906                let p = self.new_kern_node(kern, kern_sub::FONT);
1907                self.nodes.link(p, x);
1908                head = p;
1909                vh += kern + xh + xd + shift_up;
1910            }
1911            if subk.is_null() {
1912                self.nodes.free(z);
1913            } else {
1914                let (zh, zd) = self
1915                    .nodes
1916                    .box_data(z)
1917                    .map_or((0, 0), |b| (b.height, b.depth));
1918                let mut shift_down = self.math_par(mp::LIMIT_BELOW_BGAP) - zh;
1919                let g2 = self.math_par(mp::LIMIT_BELOW_VGAP);
1920                if shift_down < g2 {
1921                    shift_down = g2;
1922                }
1923                let p = self.new_kern_node(shift_down, kern_sub::FONT);
1924                self.nodes.link(y, p);
1925                self.nodes.link(p, z);
1926                let p = self.new_kern_node(kern_below, kern_sub::FONT);
1927                self.nodes.link(z, p);
1928                vd += kern_below + zh + zd + shift_down;
1929            }
1930            if let Some(b) = self.nodes.box_data_mut(v) {
1931                b.width = wv;
1932                b.height = vh;
1933                b.depth = vd;
1934                b.head = head;
1935            }
1936            // The scripts are now part of the box.
1937            let a = self.take_field(q, Field::Sup);
1938            let b = self.take_field(q, Field::Sub);
1939            self.nodes.flush_list(a);
1940            self.nodes.flush_list(b);
1941            self.set_new_hlist(q, v);
1942        }
1943        delta
1944    }
1945
1946    /// Ligatures and kerns between ordinary characters (`make_ord`).
1947    fn make_ord(&mut self, q: NodeRef) {
1948        'restart: loop {
1949            if !self.noad_field(q, Field::Sub).is_null()
1950                || !self.noad_field(q, Field::Sup).is_null()
1951            {
1952                return;
1953            }
1954            let nuc = self.noad_field(q, Field::Nucleus);
1955            if nuc.is_null() || self.nodes.id(nuc) != id::MATH_CHAR {
1956                return;
1957            }
1958            let p = self.nodes.next(q);
1959            if p.is_null()
1960                || self.nodes.id(p) != id::NOAD
1961                || !matches!(
1962                    self.nodes.subtype(p),
1963                    noad_sub::ORD
1964                        | noad_sub::OP_DISPLAY_LIMITS
1965                        | noad_sub::OP_LIMITS
1966                        | noad_sub::OP_NO_LIMITS
1967                        | noad_sub::BIN
1968                        | noad_sub::REL
1969                        | noad_sub::OPEN
1970                        | noad_sub::CLOSE
1971                        | noad_sub::PUNCT
1972                )
1973            {
1974                return;
1975            }
1976            let pn = self.noad_field(p, Field::Nucleus);
1977            if pn.is_null() || self.nodes.id(pn) != id::MATH_CHAR {
1978                return;
1979            }
1980            let (qfam, pfam, pchr) = match (self.nodes.data(nuc), self.nodes.data(pn)) {
1981                (NodeData::MathChar { fam: a, .. }, NodeData::MathChar { fam: b, chr }) => {
1982                    (*a, *b, *chr)
1983                }
1984                _ => return,
1985            };
1986            if qfam != pfam {
1987                return;
1988            }
1989            self.nodes.get_mut(nuc).id = id::MATH_TEXT_CHAR;
1990            let Some((f, c, _)) = self.fetch(nuc) else {
1991                return;
1992            };
1993            let Some(op) = self
1994                .lig_kern_step(f, c, pchr)
1995                .or_else(|| self.base_mode_kern(f, c, pchr))
1996            else {
1997                return;
1998            };
1999            match op {
2000                LigOp::Kern(k) => {
2001                    let kn = self.new_kern_node(k, kern_sub::FONT);
2002                    let n = self.nodes.next(q);
2003                    self.nodes.link(kn, n);
2004                    self.nodes.link(q, kn);
2005                    return;
2006                }
2007                LigOp::Lig { op, chr } => {
2008                    match op {
2009                        1 | 5 => self.set_char(nuc, chr),
2010                        2 | 6 => self.set_char(pn, chr),
2011                        3 | 7 | 11 => {
2012                            let r = self.new_noad(noad_sub::ORD);
2013                            let k = self.new_math_char(qfam, chr);
2014                            if op >= 11 {
2015                                self.nodes.get_mut(k).id = id::MATH_TEXT_CHAR;
2016                            }
2017                            self.set_noad_field(r, Field::Nucleus, k);
2018                            self.nodes.link(q, r);
2019                            self.nodes.link(r, p);
2020                        }
2021                        _ => {
2022                            let n = self.nodes.next(p);
2023                            self.nodes.link(q, n);
2024                            if n.is_null() {
2025                                self.nodes.set_next(q, NodeRef::NULL);
2026                            }
2027                            self.set_char(nuc, chr);
2028                            let a = self.take_field(p, Field::Sub);
2029                            let b = self.take_field(p, Field::Sup);
2030                            self.set_noad_field(q, Field::Sub, a);
2031                            self.set_noad_field(q, Field::Sup, b);
2032                            self.nodes.set_next(p, NodeRef::NULL);
2033                            self.nodes.flush_list(p);
2034                        }
2035                    }
2036                    if op > 3 {
2037                        return;
2038                    }
2039                    self.nodes.get_mut(nuc).id = id::MATH_CHAR;
2040                    continue 'restart;
2041                }
2042            }
2043        }
2044    }
2045
2046    fn set_char(&mut self, k: NodeRef, c: u32) {
2047        if let NodeData::MathChar { chr, .. } = self.nodes.data_mut(k) {
2048            *chr = c;
2049        }
2050    }
2051
2052    /// Converts the nucleus of `q` to an hlist and attaches the scripts
2053    /// (§754–§755).
2054    fn convert_nucleus(&mut self, q: NodeRef, delta: i32) {
2055        let mut delta = delta;
2056        let nuc = self.noad_field(q, Field::Nucleus);
2057        let kid = if nuc.is_null() { 0 } else { self.nodes.id(nuc) };
2058        let has_sub = !self.noad_field(q, Field::Sub).is_null();
2059        let p = match kid {
2060            id::MATH_CHAR | id::MATH_TEXT_CHAR => match self.fetch(nuc) {
2061                Some((f, c, m)) => {
2062                    delta = m.italic;
2063                    // The glyph and its italic correction take the
2064                    // character's attributes (observed).
2065                    let saved = self.attr_from(nuc);
2066                    let p = self.new_math_glyph(f, c);
2067                    let has_sup = !self.noad_field(q, Field::Sup).is_null();
2068                    if self.is_opentype_math_font(f) {
2069                        // Observed: an OpenType math character gets its
2070                        // italic correction before a character of the same
2071                        // family (a "text char"), before a superscript and
2072                        // in its scripts, but not otherwise.
2073                        if kid == id::MATH_CHAR && !has_sup && !has_sub {
2074                            delta = 0;
2075                        }
2076                    } else if kid == id::MATH_TEXT_CHAR
2077                        && self.font(f).params.get(2).copied().unwrap_or(0) != 0
2078                    {
2079                        delta = 0;
2080                    }
2081                    // Observed: an OpenType character's italic correction
2082                    // before another character takes the noad's attributes.
2083                    if kid == id::MATH_TEXT_CHAR && self.is_opentype_math_font(f) {
2084                        self.attr_restore(saved.clone());
2085                    }
2086                    if !has_sub && delta != 0 {
2087                        let k = self.new_kern_node(delta, kern_sub::ITALIC);
2088                        self.nodes.link(p, k);
2089                        delta = 0;
2090                    }
2091                    self.attr_restore(saved);
2092                    p
2093                }
2094                None => NodeRef::NULL,
2095            },
2096            id::SUB_BOX => {
2097                let b = self.nodes.list_ptr_kernel(nuc);
2098                self.nodes.set_kernel_list(nuc, NodeRef::NULL);
2099                let k = std::mem::take(&mut self.ts.math.op_kern);
2100                if k != 0 && !b.is_null() {
2101                    let kn = self.new_kern_node(k, kern_sub::FONT);
2102                    self.nodes.link(b, kn);
2103                }
2104                b
2105            }
2106            id::SUB_MLIST => {
2107                let l = self.nodes.list_ptr_kernel(nuc);
2108                self.nodes.set_kernel_list(nuc, NodeRef::NULL);
2109                let hl = self.clean_box_mlist(l, self.ts.math.style);
2110                // The box takes the sub-list's attributes (observed).
2111                let saved = self.attr_from(nuc);
2112                let b = self.hpack_quiet(hl);
2113                self.attr_restore(saved);
2114                b
2115            }
2116            _ => {
2117                // LuaTeX gives an empty nucleus with scripts an empty box.
2118                if has_sub || !self.noad_field(q, Field::Sup).is_null() {
2119                    self.new_null_box_node(true)
2120                } else {
2121                    NodeRef::NULL
2122                }
2123            }
2124        };
2125        self.set_new_hlist(q, p);
2126        let has_sup = !self.noad_field(q, Field::Sup).is_null();
2127        if !has_sub && !has_sup {
2128            return;
2129        }
2130        self.make_scripts(q, delta);
2131    }
2132
2133    /// Attaches subscript and superscript (`make_scripts`).
2134    fn make_scripts(&mut self, q: NodeRef, delta: i32) {
2135        let p = self.get_new_hlist(q);
2136        let cs = self.ts.math.style;
2137        let (mut shift_up, mut shift_down);
2138        if !p.is_null() && self.nodes.is_glyph(p) {
2139            shift_up = 0;
2140            shift_down = 0;
2141        } else {
2142            let (h, d) = self.list_height_depth(p);
2143            shift_up = h - self.math_par(mp::SUP_SHIFT_DROP);
2144            shift_down = d + self.math_par(mp::SUB_SHIFT_DROP);
2145        }
2146        let opts = self.nodes.noad(q).map_or(0, |n| n.options);
2147        let supk = self.take_field(q, Field::Sup);
2148        let subk = self.take_field(q, Field::Sub);
2149        if !p.is_null() && self.nodes.is_glyph(p) {
2150            // LuaTeX looks at character scripts in the current size (for
2151            // OpenType math kerns), which reports undefined families.
2152            for k in [supk, subk] {
2153                if !k.is_null() && self.nodes.id(k) == id::MATH_CHAR {
2154                    let _ = self.fetch(k);
2155                }
2156            }
2157        }
2158        let script_space = self.math_par(mp::SPACE_AFTER_SCRIPT);
2159        let x;
2160        if supk.is_null() {
2161            // A subscript without superscript.
2162            let st = if opts & noad_opt::NO_SUB_SCRIPT == noad_opt::NO_SUB_SCRIPT {
2163                cs
2164            } else {
2165                sub_style(cs)
2166            };
2167            x = self.clean_box(subk, st, list_sub::SUB);
2168            self.nodes.flush_list(subk);
2169            let xh = self.nodes.box_data(x).map_or(0, |b| b.height);
2170            if let Some(b) = self.nodes.box_data_mut(x) {
2171                b.width += script_space;
2172            }
2173            let s1 = self.math_par(mp::SUB_SHIFT_DOWN);
2174            if shift_down < s1 {
2175                shift_down = s1;
2176            }
2177            let clr = xh - self.math_par(mp::SUB_TOP_MAX);
2178            if shift_down < clr {
2179                shift_down = clr;
2180            }
2181            if let Some(b) = self.nodes.box_data_mut(x) {
2182                b.shift = shift_down;
2183            }
2184        } else {
2185            let st = if opts & noad_opt::NO_SUPER_SCRIPT == noad_opt::NO_SUPER_SCRIPT {
2186                cs
2187            } else {
2188                sup_style(cs)
2189            };
2190            let sx = self.clean_box(supk, st, list_sub::SUP);
2191            self.nodes.flush_list(supk);
2192            let xd = self.nodes.box_data(sx).map_or(0, |b| b.depth);
2193            if let Some(b) = self.nodes.box_data_mut(sx) {
2194                b.width += script_space;
2195            }
2196            let clr = self.math_par(mp::SUP_SHIFT_UP);
2197            if shift_up < clr {
2198                shift_up = clr;
2199            }
2200            let clr = xd + self.math_par(mp::SUP_BOTTOM_MIN);
2201            if shift_up < clr {
2202                shift_up = clr;
2203            }
2204            if subk.is_null() {
2205                if let Some(b) = self.nodes.box_data_mut(sx) {
2206                    b.shift = -shift_up;
2207                }
2208                x = sx;
2209            } else {
2210                let st = if opts & noad_opt::NO_SUB_SCRIPT == noad_opt::NO_SUB_SCRIPT {
2211                    cs
2212                } else {
2213                    sub_style(cs)
2214                };
2215                let y = self.clean_box(subk, st, list_sub::SUB);
2216                self.nodes.flush_list(subk);
2217                let yh = self.nodes.box_data(y).map_or(0, |b| b.height);
2218                if let Some(b) = self.nodes.box_data_mut(y) {
2219                    b.width += script_space;
2220                }
2221                let s2 = self.math_par(mp::SUB_SUP_SHIFT_DOWN);
2222                if shift_down < s2 {
2223                    shift_down = s2;
2224                }
2225                let mut clr =
2226                    self.math_par(mp::SUB_SUP_VGAP) - ((shift_up - xd) - (yh - shift_down));
2227                if clr > 0 {
2228                    shift_down += clr;
2229                    clr = self.math_par(mp::SUP_SUB_BOTTOM_MAX) - (shift_up - xd);
2230                    if clr > 0 {
2231                        shift_up += clr;
2232                        shift_down -= clr;
2233                    }
2234                }
2235                if let Some(b) = self.nodes.box_data_mut(sx) {
2236                    b.shift = delta;
2237                }
2238                let p = self.new_kern_node((shift_up - xd) - (yh - shift_down), kern_sub::FONT);
2239                self.nodes.link(sx, p);
2240                self.nodes.link(p, y);
2241                x = self.vpack(sx, PackSpec::NATURAL);
2242                self.nodes.set_subtype(x, list_sub::SCRIPTS);
2243                if let Some(b) = self.nodes.box_data_mut(x) {
2244                    b.shift = shift_down;
2245                }
2246            }
2247        }
2248        let nh = self.get_new_hlist(q);
2249        if nh.is_null() {
2250            self.set_new_hlist(q, x);
2251        } else {
2252            let t = self.nodes.tail_of(nh);
2253            self.nodes.link(t, x);
2254        }
2255    }
2256
2257    /// The box of `\Uvextensible`, as observed in LuaTeX: without a
2258    /// height and depth the delimiter's smallest variant, unshifted;
2259    /// otherwise the variant for their sum, centered on the baseline (on
2260    /// the axis with `axis`), in a box as high and deep as asked (raised
2261    /// by the axis with `axis`), or as the delimiter with `exact`.
2262    fn extensible_fence(
2263        &mut self,
2264        delim: NodeRef,
2265        height: i32,
2266        depth: i32,
2267        options: u32,
2268        axis: i32,
2269    ) -> NodeRef {
2270        let size = self.ts.math.size;
2271        if height == 0 && depth == 0 {
2272            let b = self.var_delimiter(delim, size, 0, false);
2273            if let Some(bd) = self.nodes.box_data_mut(b) {
2274                bd.shift = 0;
2275            }
2276            return b;
2277        }
2278        let up = if options & noad_opt::AXIS == noad_opt::AXIS {
2279            axis
2280        } else {
2281            0
2282        };
2283        let b = self.var_delimiter(delim, size, height + depth, false);
2284        if let Some(bd) = self.nodes.box_data_mut(b) {
2285            bd.shift = half(bd.height - bd.depth) - up;
2286        }
2287        let w = self.hpack_quiet(b);
2288        if options & noad_opt::EXACT != noad_opt::EXACT
2289            && let Some(bd) = self.nodes.box_data_mut(w)
2290        {
2291            bd.height = height + up;
2292            bd.depth = depth - up;
2293        }
2294        w
2295    }
2296
2297    /// Builds the delimiter of a fence (`make_left_right`).
2298    fn make_left_right(&mut self, q: NodeRef, style: i32, max_d: i32, max_h: i32) -> NoadType {
2299        self.set_cur_style(style);
2300        let (delim, height, depth, options) = match self.nodes.data(q) {
2301            NodeData::Fence(f) => (f.delim, f.height, f.depth, f.options),
2302            _ => (NodeRef::NULL, 0, 0, 0),
2303        };
2304        let axis = self.math_par(mp::AXIS);
2305        if self.nodes.subtype(q) == fence_sub::NO {
2306            let b = self.extensible_fence(delim, height, depth, options, axis);
2307            self.set_new_hlist(q, b);
2308            return NoadType::Close;
2309        }
2310        let b = if height != 0 || depth != 0 {
2311            let total = height + depth;
2312            let b = self.var_delimiter(delim, self.ts.math.size, total, true);
2313            if options & noad_opt::NO_AXIS == noad_opt::NO_AXIS
2314                && let Some(bd) = self.nodes.box_data_mut(b)
2315            {
2316                bd.shift = 0;
2317            }
2318            b
2319        } else {
2320            let mut delta2 = max_d + axis;
2321            let mut delta1 = max_h + max_d - delta2;
2322            if delta2 > delta1 {
2323                delta1 = delta2;
2324            }
2325            let mut delta = (delta1 / 500) * self.eqtb.int(ip::DELIMITERFACTOR);
2326            delta2 = delta1 + delta1 - self.eqtb.dimen(dp::DELIMITERSHORTFALL);
2327            if delta < delta2 {
2328                delta = delta2;
2329            }
2330            self.var_delimiter(delim, self.ts.math.size, delta, false)
2331        };
2332        self.set_new_hlist(q, b);
2333        if self.nodes.subtype(q) == fence_sub::LEFT {
2334            NoadType::Open
2335        } else {
2336            NoadType::Close
2337        }
2338    }
2339
2340    // ----- the second pass ---------------------------------------------
2341
2342    fn second_pass(
2343        &mut self,
2344        mlist: NodeRef,
2345        style: i32,
2346        penalties: bool,
2347        max_h: i32,
2348        max_d: i32,
2349    ) -> NodeRef {
2350        let head = self.nodes.new_temp();
2351        let mut p = head;
2352        let mut q = mlist;
2353        let mut r_type: Option<NoadType> = None;
2354        self.set_cur_style(style);
2355        let binpen = self.eqtb.int(ip::BINOPPENALTY);
2356        let relpen = self.eqtb.int(ip::RELPENALTY);
2357        while !q.is_null() {
2358            let next = self.nodes.next(q);
2359            let Some(nt) = self.noad_type(q) else {
2360                if let NodeData::Style { style } = self.nodes.data(q) {
2361                    let s = i32::from(*style);
2362                    self.set_cur_style(s);
2363                    self.nodes.free(q);
2364                } else {
2365                    self.nodes.link(p, q);
2366                    self.nodes.set_next(q, NodeRef::NULL);
2367                    p = q;
2368                }
2369                q = next;
2370                continue;
2371            };
2372            let mut t = NoadType::Ord;
2373            let mut pen = 10000;
2374            match nt {
2375                NoadType::Op
2376                | NoadType::Open
2377                | NoadType::Close
2378                | NoadType::Punct
2379                | NoadType::Inner => t = nt,
2380                NoadType::Bin => {
2381                    t = NoadType::Bin;
2382                    pen = binpen;
2383                }
2384                NoadType::Rel => {
2385                    t = NoadType::Rel;
2386                    pen = relpen;
2387                }
2388                NoadType::Fraction => t = NoadType::Inner,
2389                NoadType::Left | NoadType::Right => {
2390                    let saved = self.attr_from(q);
2391                    t = self.make_left_right(q, style, max_d, max_h);
2392                    self.attr_restore(saved);
2393                }
2394                _ => {}
2395            }
2396            // Inter-element spacing.
2397            if let Some(rt) = r_type {
2398                let style = self.ts.math.style as u32;
2399                if let Some((g, sub)) = self.math_spacing(rt as u32, t as u32, style) {
2400                    let y = self.math_glue(g, self.ts.math.mu);
2401                    // Observed: the glue takes the attributes of the node
2402                    // before it.
2403                    let saved = self.attr_from(p);
2404                    let z = self.new_glue_node(y, sub);
2405                    self.attr_restore(saved);
2406                    self.nodes.link(p, z);
2407                    p = z;
2408                }
2409            }
2410            // Append the new_hlist entries and penalties.
2411            let nh = self.get_new_hlist(q);
2412            self.set_new_hlist(q, NodeRef::NULL);
2413            if !nh.is_null() {
2414                self.nodes.link(p, nh);
2415                p = self.nodes.tail_of(nh);
2416            }
2417            if penalties && !next.is_null() && pen < 10000 {
2418                let nid = self.nodes.id(next);
2419                let next_is_rel = self.noad_type(next) == Some(NoadType::Rel);
2420                if nid != id::PENALTY && !next_is_rel {
2421                    // Observed: the penalty takes the noad's attributes.
2422                    let saved = self.attr_from(q);
2423                    let z = self.new_penalty_node(pen);
2424                    self.attr_restore(saved);
2425                    self.nodes.set_subtype(z, 5);
2426                    self.nodes.link(p, z);
2427                    p = z;
2428                }
2429            }
2430            if nt == NoadType::Right {
2431                t = NoadType::Open;
2432            }
2433            r_type = Some(t);
2434            // delete_q:
2435            self.nodes.set_next(q, NodeRef::NULL);
2436            self.nodes.flush_list(q);
2437            q = next;
2438        }
2439        let first = self.nodes.next(head);
2440        self.nodes.free(head);
2441        if !first.is_null() {
2442            self.nodes.set_prev(first, NodeRef::NULL);
2443        }
2444        first
2445    }
2446}
2447
2448#[derive(Clone, Copy, PartialEq, Eq)]
2449enum Action {
2450    Convert,
2451    CheckDimensions,
2452    DoneWithNoad,
2453}