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. Observed: without
524    /// language data (language 0, hyphen minima 0, no `\uchyph`), so that
525    /// language-specific processing of the list (babel-french's spacing
526    /// before `;` and `:`) leaves it alone.
527    fn new_math_glyph(&mut self, f: u32, c: u32) -> NodeRef {
528        let g = self.new_char_node(f, c);
529        self.nodes.set_subtype(g, PROTECTED_GLYPH);
530        if let NodeData::Glyph(d) = &mut self.nodes.get_mut(g).data {
531            d.lang = 0;
532            d.left = 0;
533            d.right = 0;
534            d.uchyph = false;
535        }
536        g
537    }
538
539    /// A box holding one character, whose width includes the italic
540    /// correction (`char_box`).
541    fn char_box(&mut self, f: u32, c: u32) -> NodeRef {
542        let m = self.fonts.char_metrics(f, c).copied().unwrap_or_default();
543        // OpenType math boxes leave out the italic correction (observed;
544        // LuaTeX 1.24 includes it).
545        let italic = if self.is_opentype_math_font(f) && !self.math_124 {
546            0
547        } else {
548            m.italic
549        };
550        let b = self.new_null_box_node(true);
551        let g = self.new_math_glyph(f, c);
552        if let Some(bd) = self.nodes.box_data_mut(b) {
553            bd.width = m.width + italic;
554            bd.height = m.height;
555            bd.depth = m.depth;
556            bd.head = g;
557        }
558        self.nodes.set_subtype(b, list_sub::MATH_CHAR);
559        b
560    }
561
562    /// The TFM tag and remainder of character `c` in font `f`
563    /// (0 none, 1 lig/kern, 2 list, 3 extensible).
564    fn char_tag(&self, f: u32, c: u32) -> (u8, u32) {
565        if let Some(t) = self.tfm_of(f)
566            && let Some(ci) = t.char_info(c)
567        {
568            return (ci.tag, u32::from(ci.remainder));
569        }
570        if let Some(m) = self.math_char(f, c) {
571            if let Some(n) = m.next {
572                return (2, n);
573            }
574            if m.vert.is_some() {
575                return (3, c);
576            }
577        }
578        (0, 0)
579    }
580
581    // ----- boxes ----------------------------------------------------------
582
583    /// Puts a noad field into a box of subtype `sub` in style `s`
584    /// (`clean_box`).
585    fn clean_box(&mut self, k: NodeRef, s: i32, sub: u16) -> NodeRef {
586        let mut q;
587        let kid = if k.is_null() { 0 } else { self.nodes.id(k) };
588        match kid {
589            id::MATH_CHAR | id::MATH_TEXT_CHAR => {
590                let n = self.new_noad(noad_sub::ORD);
591                let kc = self.nodes.copy_node(k);
592                self.set_noad_field(n, Field::Nucleus, kc);
593                q = self.clean_box_mlist(n, s);
594            }
595            id::SUB_BOX => {
596                q = self.nodes.list_ptr_kernel(k);
597                self.nodes.set_kernel_list(k, NodeRef::NULL);
598            }
599            id::SUB_MLIST => {
600                let l = self.nodes.list_ptr_kernel(k);
601                self.nodes.set_kernel_list(k, NodeRef::NULL);
602                q = self.clean_box_mlist(l, s);
603            }
604            _ => {
605                q = self.new_null_box_node(true);
606            }
607        }
608        // found: a new box takes the attributes of its first node
609        // (observed).
610        let x = if q.is_null() {
611            self.hpack_quiet(q)
612        } else if !self.nodes.is_glyph(q)
613            && self.nodes.next(q).is_null()
614            && matches!(self.nodes.id(q), id::HLIST | id::VLIST)
615            && self.nodes.box_data(q).is_some_and(|b| b.shift == 0)
616        {
617            q
618        } else {
619            let saved = self.attr_from(q);
620            let x = self.hpack_quiet(q);
621            self.attr_restore(saved);
622            x
623        };
624        self.nodes.set_subtype(x, sub);
625        // Simplify a trivial box.
626        q = self.nodes.list_ptr(x);
627        if !q.is_null() && self.nodes.is_glyph(q) {
628            let r = self.nodes.next(q);
629            if !r.is_null() && self.nodes.next(r).is_null() && self.nodes.id(r) == id::KERN {
630                self.nodes.free(r);
631                self.nodes.set_next(q, NodeRef::NULL);
632            }
633        }
634        x
635    }
636
637    fn clean_box_mlist(&mut self, l: NodeRef, s: i32) -> NodeRef {
638        let saved = self.ts.math;
639        let r = self.mlist_to_hlist_rec(l, s, false);
640        self.ts.math = saved;
641        r
642    }
643
644    /// Centers box `b` in a box of width `w` (`rebox`).
645    fn rebox(&mut self, b: NodeRef, w: i32) -> NodeRef {
646        let bw = self.nodes.box_data(b).map_or(0, |x| x.width);
647        let lp = self.nodes.list_ptr(b);
648        if bw != w && !lp.is_null() {
649            // The new nodes take the attributes of the box (observed).
650            let saved = self.attr_from(b);
651            let mut b = b;
652            if self.nodes.id(b) == id::VLIST {
653                b = self.hpack_quiet(b);
654            }
655            let mut p = self.nodes.list_ptr(b);
656            let bw = self.nodes.box_data(b).map_or(0, |x| x.width);
657            if self.nodes.is_glyph(p) && self.nodes.next(p).is_null() {
658                let (f, c) = {
659                    let g = self.nodes.glyph(p).expect("glyph");
660                    (g.font, g.chr)
661                };
662                let v = self.fonts.char_metrics(f, c).map_or(0, |m| m.width);
663                if v != bw {
664                    let k = self.new_kern_node(bw - v, kern_sub::FONT);
665                    self.nodes.link(p, k);
666                }
667            }
668            self.nodes.set_list_ptr(b, NodeRef::NULL);
669            self.nodes.free(b);
670            let ss = crate::glue::Glue {
671                stretch: retex_arith::UNITY,
672                stretch_order: crate::glue::FIL,
673                shrink: retex_arith::UNITY,
674                shrink_order: crate::glue::FIL,
675                ..crate::glue::Glue::ZERO
676            };
677            let g1 = self.new_glue_node(ss, glue_sub::USER);
678            self.nodes.link(g1, p);
679            while !self.nodes.next(p).is_null() {
680                p = self.nodes.next(p);
681            }
682            let g2 = self.new_glue_node(ss, glue_sub::USER);
683            self.nodes.link(p, g2);
684            let r = self.hpack(g1, PackSpec::Exactly(w));
685            self.attr_restore(saved);
686            r
687        } else {
688            if let Some(x) = self.nodes.box_data_mut(b) {
689                x.width = w;
690            }
691            b
692        }
693    }
694
695    /// A rule of thickness `t` (`fraction_rule`).
696    fn fraction_rule(&mut self, t: i32) -> NodeRef {
697        self.new_node(
698            id::RULE,
699            0,
700            NodeData::Rule {
701                width: crate::typeset::NULL_FLAG,
702                depth: 0,
703                height: t,
704                dir: 0,
705                index: 0,
706                left: 0,
707                right: 0,
708            },
709        )
710    }
711
712    /// Box `b` under a kern `k`, a rule `t` and a kern `top` (`overbar`).
713    fn overbar(&mut self, b: NodeRef, k: i32, t: i32, top: i32) -> NodeRef {
714        let p = self.new_kern_node(k, kern_sub::FONT);
715        self.nodes.link(p, b);
716        let q = self.fraction_rule(t);
717        self.nodes.link(q, p);
718        let p = self.new_kern_node(top, kern_sub::FONT);
719        self.nodes.link(p, q);
720        self.vpack(p, PackSpec::NATURAL)
721    }
722
723    // ----- delimiters -----------------------------------------------------
724
725    /// A box with the smallest variant of delimiter `d` whose height plus
726    /// depth is at least `v`, built from extensible pieces if needed,
727    /// centered on the axis of size `s` (`var_delimiter`).
728    /// Its nodes take the attributes of the delimiter (none for a null
729    /// one; observed).
730    pub fn var_delimiter(&mut self, d: NodeRef, s: u32, v: i32, shift: bool) -> NodeRef {
731        let saved = self.attr_from(d);
732        let b = self.var_delimiter_box(d, s, v, shift);
733        self.attr_restore(saved);
734        b
735    }
736
737    fn var_delimiter_box(&mut self, d: NodeRef, s: u32, v: i32, shift: bool) -> NodeRef {
738        let (sf, sc, lf, lc) = self.delim_fields(d);
739        let mut best: Option<(u32, u32, bool)> = None;
740        let mut w = 0i32;
741        'search: for (z0, x0) in [(sf, sc), (lf, lc)] {
742            if z0 == 0 && x0 == 0 {
743                continue;
744            }
745            // Observed: only the fonts of the current size are searched
746            // (TeX also tries the smaller sizes' fonts; LuaTeX takes the
747            // large variant of the script font, cmex8, before the small
748            // variant of the text font).
749            let mut z = s as i32;
750            while z == s as i32 {
751                let g = self.fam_fnt(z0, z as u32);
752                if g != 0 {
753                    let mut y = x0;
754                    let mut guard = 0;
755                    loop {
756                        guard += 1;
757                        if guard > 256 {
758                            break;
759                        }
760                        let Some(m) = self.fonts.char_metrics(g, y).copied() else {
761                            break;
762                        };
763                        let (tag, rem) = self.char_tag(g, y);
764                        // An OpenType variant is used when it is large
765                        // enough; the assembly only after the last one
766                        // (a variant with a larger one keeps the search
767                        // going even if it has an assembly of its own).
768                        let opentype = self.is_opentype_math_font(g);
769                        if tag == 3 && !opentype {
770                            best = Some((g, y, true));
771                            break 'search;
772                        }
773                        let u = m.height + m.depth;
774                        if u > w {
775                            best = Some((g, y, false));
776                            w = u;
777                            if u >= v {
778                                break 'search;
779                            }
780                        }
781                        if opentype && tag != 2 && self.vert_parts(g, y).is_some() {
782                            best = Some((g, y, true));
783                            break 'search;
784                        }
785                        if tag == 2 {
786                            y = rem;
787                        } else {
788                            break;
789                        }
790                    }
791                }
792                z -= 1;
793            }
794        }
795        let b = match best {
796            Some((f, c, true)) if self.is_opentype_math_font(f) => {
797                let saved = self.ts.math;
798                self.set_cur_style(if s == 0 { 2 } else { 2 * s as i32 + 2 });
799                let overlap = self.math_par(mp::CONNECTOR_OVERLAP_MIN);
800                self.ts.math = saved;
801                self.ot_extensible_box(f, c, v, overlap)
802            }
803            Some((f, c, true)) => self.extensible_box(f, c, v),
804            Some((f, c, false)) => self.char_box(f, c),
805            None => {
806                let b = self.new_null_box_node(true);
807                let nds = self.eqtb.dimen(dp::NULLDELIMITERSPACE);
808                if let Some(bd) = self.nodes.box_data_mut(b) {
809                    bd.width = nds;
810                }
811                self.nodes.set_subtype(b, list_sub::V_DELIMITER);
812                if !shift {
813                    return b;
814                }
815                b
816            }
817        };
818        let saved = self.ts.math;
819        self.set_cur_style(if s == 0 { 2 } else { 2 * s as i32 + 2 });
820        let axis = self.math_par(mp::AXIS);
821        self.ts.math = saved;
822        if let Some(bd) = self.nodes.box_data_mut(b) {
823            bd.shift = half(bd.height - bd.depth) - axis;
824        }
825        b
826    }
827
828    /// For a nucleus that is a list of one accent noad on a character
829    /// (possibly accented itself), that character's font and code.
830    fn nested_accent_char(&mut self, k: NodeRef) -> Option<(u32, u32)> {
831        if k.is_null() || self.nodes.id(k) != id::SUB_MLIST {
832            return None;
833        }
834        let l = self.nodes.list_ptr_kernel(k);
835        if l.is_null()
836            || !self.nodes.next(l).is_null()
837            || self.noad_type(l) != Some(NoadType::Accent)
838        {
839            return None;
840        }
841        let n = self.noad_field(l, Field::Nucleus);
842        if n.is_null() {
843            return None;
844        }
845        if self.nodes.id(n) == id::MATH_CHAR {
846            return self.fetch(n).map(|(f, c, _)| (f, c));
847        }
848        self.nested_accent_char(n)
849    }
850
851    /// The vertical extensible parts of OpenType math character `c`.
852    fn vert_parts(&self, f: u32, c: u32) -> Option<std::rc::Rc<[crate::fonts::ExtPart]>> {
853        self.math_char(f, c)?.vert.clone()
854    }
855
856    /// Builds the vertical assembly of OpenType math character `c` with
857    /// height plus depth at least `v`, as LuaTeX does (observed with Latin
858    /// Modern Math): each extender is repeated the smallest number of
859    /// times that lets the parts, overlapping by at least `overlap`, reach
860    /// `v`; the parts are boxes as high as their glyphs, separated by
861    /// glue that removes the largest overlap the connectors allow and
862    /// stretches back to the smallest; the vbox is set to `v`.
863    fn ot_extensible_box(&mut self, f: u32, c: u32, v: i32, overlap: i32) -> NodeRef {
864        let parts = self
865            .vert_parts(f, c)
866            .unwrap_or_else(|| std::rc::Rc::from(Vec::new()));
867        let expand = |n: usize| -> Vec<crate::fonts::ExtPart> {
868            let mut out = Vec::new();
869            for p in parts.iter() {
870                let k = if p.extender { n } else { 1 };
871                for _ in 0..k {
872                    out.push(*p);
873                }
874            }
875            out
876        };
877        let joint = |a: &crate::fonts::ExtPart, b: &crate::fonts::ExtPart| {
878            let max = a.end.min(b.start).max(0);
879            (max, overlap.min(max))
880        };
881        let largest = |seq: &[crate::fonts::ExtPart]| -> i64 {
882            let mut t: i64 = seq.iter().map(|p| i64::from(p.advance)).sum();
883            for w in seq.windows(2) {
884                t -= i64::from(joint(&w[0], &w[1]).1);
885            }
886            t
887        };
888        let has_extender = parts.iter().any(|p| p.extender);
889        let mut n = 0;
890        let mut seq = expand(n);
891        while has_extender && largest(&seq) < i64::from(v) && n < 1000 {
892            n += 1;
893            seq = expand(n);
894        }
895        // Build the vbox from the top down.
896        let mut head = NodeRef::NULL;
897        let mut tail = NodeRef::NULL;
898        let mut width = 0;
899        for (i, p) in seq.iter().enumerate().rev() {
900            if i + 1 < seq.len() {
901                let (max, min) = joint(p, &seq[i + 1]);
902                let g = crate::glue::Glue {
903                    width: -max,
904                    stretch: max - min,
905                    ..crate::glue::Glue::ZERO
906                };
907                let gn = self.new_glue_node(g, 0);
908                self.nodes.link(tail, gn);
909                tail = gn;
910            }
911            let m = self
912                .fonts
913                .char_metrics(f, p.code)
914                .copied()
915                .unwrap_or_default();
916            let b = self.new_null_box_node(true);
917            self.nodes.set_subtype(b, list_sub::MATH_CHAR);
918            let gl = self.new_math_glyph(f, p.code);
919            // Observed: the box is as high as the glyph (height plus
920            // depth), not as its advance in the assembly, where they differ
921            // (Libertinus Math).
922            if let Some(bd) = self.nodes.box_data_mut(b) {
923                bd.width = m.width;
924                bd.height = m.height + m.depth;
925                bd.depth = 0;
926                bd.head = gl;
927            }
928            width = width.max(m.width);
929            if head.is_null() {
930                head = b;
931            } else {
932                self.nodes.link(tail, b);
933            }
934            tail = b;
935        }
936        let natural = largest(&seq) as i32;
937        let target = v.max(natural.min(v));
938        let b = self.vpack(head, crate::build::PackSpec::Exactly(target));
939        self.nodes.set_subtype(b, list_sub::V_EXTENSIBLE);
940        if let Some(bd) = self.nodes.box_data_mut(b) {
941            bd.width = width;
942        }
943        b
944    }
945
946    /// Builds an extensible character from the recipe of `c` in font `f`
947    /// (§713–§714).
948    fn extensible_box(&mut self, f: u32, c: u32, v: i32) -> NodeRef {
949        let recipe = self
950            .tfm_of(f)
951            .and_then(|t| {
952                let (_, rem) = self.char_tag(f, c);
953                t.exten.get(rem as usize).copied()
954            })
955            .unwrap_or([0, 0, 0, 0]);
956        let [top, mid, bot, rep] = recipe.map(u32::from);
957        let hd = |e: &Engine, ch: u32| {
958            e.fonts
959                .char_metrics(f, ch)
960                .map_or(0, |m| m.height + m.depth)
961        };
962        let b = self.new_null_box_node(false);
963        let u = hd(self, rep);
964        let mut w = 0;
965        let rm = self.fonts.char_metrics(f, rep).copied().unwrap_or_default();
966        let width = rm.width + rm.italic;
967        if bot != 0 {
968            w += hd(self, bot);
969        }
970        if mid != 0 {
971            w += hd(self, mid);
972        }
973        if top != 0 {
974            w += hd(self, top);
975        }
976        let mut n = 0;
977        if u > 0 {
978            while w < v {
979                w += u;
980                n += 1;
981                if mid != 0 {
982                    w += u;
983                }
984            }
985        }
986        if let Some(bd) = self.nodes.box_data_mut(b) {
987            bd.width = width;
988        }
989        if bot != 0 {
990            self.stack_into_box(b, f, bot);
991        }
992        for _ in 0..n {
993            self.stack_into_box(b, f, rep);
994        }
995        if mid != 0 {
996            self.stack_into_box(b, f, mid);
997            for _ in 0..n {
998                self.stack_into_box(b, f, rep);
999            }
1000        }
1001        if top != 0 {
1002            self.stack_into_box(b, f, top);
1003        }
1004        // LuaTeX gives the whole height to the box (TeX82 keeps the
1005        // height of the topmost piece and puts the rest in the depth).
1006        if let Some(bd) = self.nodes.box_data_mut(b) {
1007            bd.height = w;
1008            bd.depth = 0;
1009        }
1010        self.nodes.set_subtype(b, list_sub::V_EXTENSIBLE);
1011        b
1012    }
1013
1014    fn stack_into_box(&mut self, b: NodeRef, f: u32, c: u32) {
1015        let p = self.char_box(f, c);
1016        let head = self.nodes.list_ptr(b);
1017        self.nodes.link(p, head);
1018        self.nodes.set_list_ptr(b, p);
1019        let h = self.nodes.box_data(p).map_or(0, |x| x.height);
1020        if let Some(bd) = self.nodes.box_data_mut(b) {
1021            bd.height = h;
1022        }
1023    }
1024
1025    // ----- the make_… procedures -----------------------------------------
1026
1027    fn take_field(&mut self, q: NodeRef, f: Field) -> NodeRef {
1028        let k = self.noad_field(q, f);
1029        self.set_noad_field(q, f, NodeRef::NULL);
1030        k
1031    }
1032
1033    /// Replaces the nucleus of `q` by a `sub_box` holding `b`.
1034    fn set_nucleus_box(&mut self, q: NodeRef, b: NodeRef) {
1035        let old = self.take_field(q, Field::Nucleus);
1036        self.nodes.flush_list(old);
1037        let k = self.new_sub_list(false, b);
1038        self.set_noad_field(q, Field::Nucleus, k);
1039    }
1040
1041    fn make_over(&mut self, q: NodeRef) {
1042        let k = self.noad_field(q, Field::Nucleus);
1043        let st = cramped_style(self.ts.math.style);
1044        let x = self.clean_box(k, st, list_sub::NUCLEUS);
1045        // The bar takes the attributes of the nucleus (observed).
1046        let saved = self.attr_from(k);
1047        let b = self.overbar(
1048            x,
1049            self.math_par(mp::OVERBAR_VGAP),
1050            self.math_par(mp::OVERBAR_RULE),
1051            self.math_par(mp::OVERBAR_KERN),
1052        );
1053        self.attr_restore(saved);
1054        self.nodes.set_subtype(b, list_sub::OVER);
1055        self.set_nucleus_box(q, b);
1056    }
1057
1058    fn make_under(&mut self, q: NodeRef) {
1059        let k = self.noad_field(q, Field::Nucleus);
1060        let x = self.clean_box(k, self.ts.math.style, list_sub::NUCLEUS);
1061        let p = self.new_kern_node(self.math_par(mp::UNDERBAR_VGAP), kern_sub::FONT);
1062        self.nodes.link(x, p);
1063        let r = self.fraction_rule(self.math_par(mp::UNDERBAR_RULE));
1064        self.nodes.link(p, r);
1065        let y = self.vpack(x, PackSpec::NATURAL);
1066        self.nodes.set_subtype(y, list_sub::UNDER);
1067        let (yh, yd) = self
1068            .nodes
1069            .box_data(y)
1070            .map_or((0, 0), |b| (b.height, b.depth));
1071        let xh = self.nodes.box_data(x).map_or(0, |b| b.height);
1072        let delta = yh + yd + self.math_par(mp::UNDERBAR_KERN);
1073        if let Some(b) = self.nodes.box_data_mut(y) {
1074            b.height = xh;
1075            b.depth = delta - xh;
1076        }
1077        self.set_nucleus_box(q, y);
1078    }
1079
1080    fn make_vcenter(&mut self, q: NodeRef) {
1081        let k = self.noad_field(q, Field::Nucleus);
1082        let v = self.nodes.list_ptr_kernel(k);
1083        if v.is_null() || self.nodes.id(v) != id::VLIST {
1084            self.confusion("vcenter");
1085        }
1086        let axis = self.math_par(mp::AXIS);
1087        if let Some(b) = self.nodes.box_data_mut(v) {
1088            let delta = b.height + b.depth;
1089            b.height = axis + half(delta);
1090            b.depth = delta - b.height;
1091        }
1092    }
1093
1094    fn make_radical(&mut self, q: NodeRef) {
1095        let sub = self.nodes.subtype(q);
1096        match sub {
1097            radical_sub::UUNDERDELIMITER
1098            | radical_sub::UOVERDELIMITER
1099            | radical_sub::UDELIMITERUNDER
1100            | radical_sub::UDELIMITEROVER
1101            | radical_sub::UHEXTENSIBLE => {
1102                self.make_delimiter_radical(q, sub);
1103                return;
1104            }
1105            _ => {}
1106        }
1107        let k = self.noad_field(q, Field::Nucleus);
1108        let x = self.clean_box(k, cramped_style(self.ts.math.style), list_sub::NUCLEUS);
1109        let (xh, xd) = self
1110            .nodes
1111            .box_data(x)
1112            .map_or((0, 0), |b| (b.height, b.depth));
1113        let mut clr = self.math_par(mp::RADICAL_VGAP);
1114        let theta = self.math_par_opt(mp::RADICAL_RULE);
1115        let kern = self.math_par(mp::RADICAL_KERN);
1116        let t = theta.unwrap_or(self.math_par(mp::FRACTION_RULE));
1117        let left = self.nodes.noad(q).map_or(NodeRef::NULL, |n| n.left);
1118        let size = self.ts.math.size;
1119        let y = self.var_delimiter(left, size, xh + xd + clr + t, true);
1120        let (yh, yd) = self
1121            .nodes
1122            .box_data(y)
1123            .map_or((0, 0), |b| (b.height, b.depth));
1124        let theta = if let Some(th) = theta {
1125            // With \Umathradicalrule set (OpenType fonts): the sign's whole
1126            // size counts, and its top is put at the top of the rule.
1127            let delta = yh + yd - th - (xh + xd + clr);
1128            if delta > 0 {
1129                clr += half(delta);
1130            }
1131            if let Some(b) = self.nodes.box_data_mut(y) {
1132                b.shift = yh - (xh + clr + th);
1133            }
1134            th
1135        } else {
1136            // TeX82 assumes the sign's height is the rule thickness. LuaTeX
1137            // gives an extensible sign its whole size as height, so measure
1138            // it from its topmost piece and convert.
1139            let top_h = if self.nodes.id(y) == id::VLIST {
1140                let h = self.nodes.list_ptr(y);
1141                self.nodes.box_data(h).map_or(yh, |b| b.height)
1142            } else {
1143                yh
1144            };
1145            let eq_depth = yh + yd - top_h;
1146            let delta = eq_depth - (xh + xd + clr);
1147            if delta > 0 {
1148                clr += half(delta);
1149            }
1150            if let Some(b) = self.nodes.box_data_mut(y) {
1151                b.shift = -(xh + clr) + (yh - top_h);
1152            }
1153            top_h
1154        };
1155        // The kern above the rule is \Umathradicalkern, the default rule
1156        // thickness for traditional fonts, even where the sign's height
1157        // (the rule) differs from it (observed with newtxmath).
1158        // The bar takes the attributes of the delimiter (observed).
1159        let saved = self.attr_from(left);
1160        let ob = self.overbar(x, clr, theta, kern);
1161        self.attr_restore(saved);
1162        self.nodes.set_subtype(ob, list_sub::RADICAL);
1163        self.nodes.link(y, ob);
1164        let deg = self.noad_field(q, Field::Degree);
1165        let list = if deg.is_null() {
1166            y
1167        } else {
1168            // \Uroot: the degree in scriptscript style.
1169            let r = self.clean_box(deg, 6, list_sub::DEGREE);
1170            let (rw, rh, rd) = self
1171                .nodes
1172                .box_data(r)
1173                .map_or((0, 0, 0), |b| (b.width, b.height, b.depth));
1174            let _ = rd;
1175            let before = self.math_par(mp::RADICAL_DEGREE_BEFORE);
1176            let after = self.math_par(mp::RADICAL_DEGREE_AFTER);
1177            let raise = self.math_par(mp::RADICAL_DEGREE_RAISE);
1178            let (yh2, yd2, ys) = self
1179                .nodes
1180                .box_data(y)
1181                .map_or((0, 0, 0), |b| (b.height, b.depth, b.shift));
1182            let ytot = yh2 + yd2;
1183            let shift = retex_arith::xn_over_d(ytot, raise, 100).map_or(0, |r| r.quotient);
1184            // The degree's baseline is `raise` percent of the sign's size
1185            // above the sign's bottom.
1186            if self.math_par_opt(mp::RADICAL_RULE).is_some() {
1187                if let Some(b) = self.nodes.box_data_mut(r) {
1188                    b.shift = -(shift - (yd2 + ys));
1189                }
1190            } else if let Some(b) = self.nodes.box_data_mut(r) {
1191                b.shift = -(shift - yd2 + b.shift.min(0));
1192            }
1193            let _ = rh;
1194            // The kerns take the attributes of the degree (observed).
1195            let saved = self.attr_from(r);
1196            let k1 = self.new_kern_node(before, kern_sub::FONT);
1197            let k2 = self.new_kern_node(after, kern_sub::FONT);
1198            self.attr_restore(saved);
1199            let _ = rw;
1200            self.nodes.link(k1, r);
1201            self.nodes.link(r, k2);
1202            self.nodes.link(k2, y);
1203            k1
1204        };
1205        let b = self.hpack_quiet(list);
1206        self.set_nucleus_box(q, b);
1207    }
1208
1209    /// `\Uoverdelimiter` and friends: a delimiter above or below the
1210    /// nucleus, as observed in LuaTeX. `\Uoverdelimiter` puts the nucleus,
1211    /// in superscript style, over the delimiter and `\Uunderdelimiter`
1212    /// puts it, in subscript style, under it; `\Udelimiterover` and
1213    /// `\Udelimiterunder` put the delimiter over or under a nucleus in the
1214    /// current style, measuring the gap from the delimiter's baseline.
1215    /// Both boxes take the larger width; the baseline is the delimiter's
1216    /// for the first two, the nucleus's for the others.
1217    fn make_delimiter_radical(&mut self, q: NodeRef, sub: u16) {
1218        let k = self.noad_field(q, Field::Nucleus);
1219        let cs = self.ts.math.style;
1220        let left = self.nodes.noad(q).map_or(NodeRef::NULL, |n| n.left);
1221        let size = self.ts.math.size;
1222        if sub == radical_sub::UHEXTENSIBLE {
1223            let width = self.nodes.noad(q).map_or(0, |n| n.width);
1224            let y = self.h_delimiter(left, size, width);
1225            let b = self.hpack_quiet(y);
1226            self.set_nucleus_box(q, b);
1227            return;
1228        }
1229        let st = match sub {
1230            radical_sub::UOVERDELIMITER => sup_style(cs),
1231            radical_sub::UUNDERDELIMITER => sub_style(cs),
1232            _ => cs,
1233        };
1234        let x = if k.is_null() {
1235            self.new_null_box_node(true)
1236        } else {
1237            self.clean_box(k, st, list_sub::NUCLEUS)
1238        };
1239        // Observed: \Udelimiterover and \Udelimiterunder take the
1240        // delimiter in the script size (like limits), the others in the
1241        // current size; it is made as wide as the nucleus.
1242        let dsize = match sub {
1243            radical_sub::UDELIMITEROVER | radical_sub::UDELIMITERUNDER => (size + 1).min(2),
1244            _ => size,
1245        };
1246        let xw0 = self.nodes.box_data(x).map_or(0, |b| b.width);
1247        let y = self.h_delimiter(left, dsize, xw0);
1248        let dim = |e: &Self, b: NodeRef| {
1249            e.nodes
1250                .box_data(b)
1251                .map_or((0, 0, 0), |b| (b.width, b.height, b.depth))
1252        };
1253        let (xw, xh, xd) = dim(self, x);
1254        let (yw, yh, yd) = dim(self, y);
1255        let w = xw.max(yw);
1256        for b in [x, y] {
1257            if let Some(bd) = self.nodes.box_data_mut(b) {
1258                bd.width = w;
1259            }
1260        }
1261        let over = matches!(
1262            sub,
1263            radical_sub::UOVERDELIMITER | radical_sub::UDELIMITEROVER
1264        );
1265        let (vgap, bgap) = if over {
1266            (
1267                self.math_par(mp::OVER_DELIMITER_VGAP),
1268                self.math_par(mp::OVER_DELIMITER_BGAP),
1269            )
1270        } else {
1271            (
1272                self.math_par(mp::UNDER_DELIMITER_VGAP),
1273                self.math_par(mp::UNDER_DELIMITER_BGAP),
1274            )
1275        };
1276        let (top, bottom, kern) = match sub {
1277            radical_sub::UOVERDELIMITER => (x, y, vgap.max(bgap - yh - xd)),
1278            radical_sub::UUNDERDELIMITER => (y, x, vgap.max(bgap - xh - yd)),
1279            radical_sub::UDELIMITEROVER => (y, x, vgap.max(bgap - xh - (yh + yd)) - yd),
1280            _ => (x, y, vgap.max(bgap - xd - (yh + yd)) - yh),
1281        };
1282        let kk = self.new_kern_node(kern, kern_sub::FONT);
1283        self.nodes.link(top, kk);
1284        self.nodes.link(kk, bottom);
1285        let v = self.vpack(top, PackSpec::NATURAL);
1286        self.nodes.set_subtype(
1287            v,
1288            if over {
1289                list_sub::OVER_DELIMITER
1290            } else {
1291                list_sub::UNDER_DELIMITER
1292            },
1293        );
1294        // Under the nucleus or the delimiter on top, the baseline is that
1295        // of the top box.
1296        if matches!(
1297            sub,
1298            radical_sub::UUNDERDELIMITER | radical_sub::UDELIMITERUNDER
1299        ) {
1300            let th = self.nodes.box_data(top).map_or(0, |b| b.height);
1301            if let Some(b) = self.nodes.box_data_mut(v) {
1302                let tot = b.height + b.depth;
1303                b.height = th;
1304                b.depth = tot - th;
1305            }
1306        }
1307        self.set_nucleus_box(q, v);
1308    }
1309
1310    /// A horizontal delimiter of size `s` at least `w` wide: for an
1311    /// OpenType font the first variant that wide, else the assembly of
1312    /// the last one stretched to `w` (observed: as the vertical one, with
1313    /// the parts in boxes as wide as their glyphs and the connector
1314    /// overlap of the current style); for other fonts the character.
1315    /// Its nodes take the delimiter's attributes.
1316    fn h_delimiter(&mut self, d: NodeRef, s: u32, w: i32) -> NodeRef {
1317        let saved = self.attr_from(d);
1318        let (sf, sc, _, _) = self.delim_fields(d);
1319        let f = self.fam_fnt(sf, s);
1320        let b = if f == 0 || self.fonts.char_metrics(f, sc).is_none() {
1321            self.new_null_box_node(true)
1322        } else if !self.is_opentype_math_font(f) {
1323            self.char_box(f, sc)
1324        } else {
1325            let mut c = sc;
1326            let mut guard = 0;
1327            let mut assembly = None;
1328            loop {
1329                guard += 1;
1330                let m = self.fonts.char_metrics(f, c).copied().unwrap_or_default();
1331                if m.width >= w || guard > 256 {
1332                    break;
1333                }
1334                if let Some(parts) = self.math_char(f, c).and_then(|mc| mc.horiz.clone()) {
1335                    assembly = Some(parts);
1336                    break;
1337                }
1338                match self.math_char(f, c).and_then(|mc| mc.next) {
1339                    Some(n) if self.fonts.char_metrics(f, n).is_some() => c = n,
1340                    _ => break,
1341                }
1342            }
1343            match assembly {
1344                Some(parts) => {
1345                    let overlap = self.math_par(mp::CONNECTOR_OVERLAP_MIN);
1346                    self.ot_h_extensible_box(f, &parts, w, overlap)
1347                }
1348                None => self.char_box(f, c),
1349            }
1350        };
1351        self.attr_restore(saved);
1352        b
1353    }
1354
1355    /// The horizontal assembly of `parts` (left first) in font `f`, `w`
1356    /// wide (see [`Engine::ot_extensible_box`]).
1357    fn ot_h_extensible_box(
1358        &mut self,
1359        f: u32,
1360        parts: &[crate::fonts::ExtPart],
1361        w: i32,
1362        overlap: i32,
1363    ) -> NodeRef {
1364        let expand = |n: usize| -> Vec<crate::fonts::ExtPart> {
1365            let mut out = Vec::new();
1366            for p in parts.iter() {
1367                let k = if p.extender { n } else { 1 };
1368                for _ in 0..k {
1369                    out.push(*p);
1370                }
1371            }
1372            out
1373        };
1374        let joint = |a: &crate::fonts::ExtPart, b: &crate::fonts::ExtPart| {
1375            let max = a.end.min(b.start).max(0);
1376            (max, overlap.min(max))
1377        };
1378        let largest = |seq: &[crate::fonts::ExtPart]| -> i64 {
1379            let mut t: i64 = seq.iter().map(|p| i64::from(p.advance)).sum();
1380            for win in seq.windows(2) {
1381                t -= i64::from(joint(&win[0], &win[1]).1);
1382            }
1383            t
1384        };
1385        let has_extender = parts.iter().any(|p| p.extender);
1386        let mut n = 0;
1387        let mut seq = expand(n);
1388        while has_extender && largest(&seq) < i64::from(w) && n < 1000 {
1389            n += 1;
1390            seq = expand(n);
1391        }
1392        let mut head = NodeRef::NULL;
1393        let mut tail = NodeRef::NULL;
1394        for (i, p) in seq.iter().enumerate() {
1395            if i > 0 {
1396                let (max, min) = joint(&seq[i - 1], p);
1397                let g = crate::glue::Glue {
1398                    width: -max,
1399                    stretch: max - min,
1400                    ..crate::glue::Glue::ZERO
1401                };
1402                let gn = self.new_glue_node(g, 0);
1403                self.nodes.link(tail, gn);
1404                tail = gn;
1405            }
1406            let b = self.char_box(f, p.code);
1407            if head.is_null() {
1408                head = b;
1409            } else {
1410                self.nodes.link(tail, b);
1411            }
1412            tail = b;
1413        }
1414        let natural = largest(&seq) as i32;
1415        let target = w.max(natural.min(w));
1416        let b = self.hpack(head, PackSpec::Exactly(target));
1417        self.nodes.set_subtype(b, list_sub::H_EXTENSIBLE);
1418        b
1419    }
1420
1421    /// Places a math accent (`make_math_accent`).
1422    fn make_math_accent(&mut self, q: NodeRef) {
1423        let acc = self.nodes.noad(q).map_or(NodeRef::NULL, |n| n.accent);
1424        if acc.is_null() {
1425            // A bottom accent only: place it below the nucleus.
1426            self.make_bottom_accent(q);
1427            return;
1428        }
1429        let Some((f, mut c, mut i)) = self.fetch(acc) else {
1430            return;
1431        };
1432        // Compute the amount of skew.
1433        let mut s = 0;
1434        let nuc = self.noad_field(q, Field::Nucleus);
1435        let mut nuc_char = None;
1436        if !nuc.is_null()
1437            && self.nodes.id(nuc) == id::MATH_CHAR
1438            && let Some((nf, nc, _)) = self.fetch(nuc)
1439        {
1440            nuc_char = Some((nf, nc));
1441            let sk = self.font(nf).skew_char;
1442            if sk >= 0
1443                && let Some(LigOp::Kern(k)) = self.lig_kern_step(nf, nc, sk as u32)
1444            {
1445                s = k;
1446            }
1447        }
1448        let st = cramped_style(self.ts.math.style);
1449        let mut char_nucleus = !nuc.is_null() && self.nodes.id(nuc) == id::MATH_CHAR;
1450        if nuc_char.is_none()
1451            && self.is_opentype_math_font(f)
1452            && let Some(fc) = self.nested_accent_char(nuc)
1453        {
1454            // An accent on an accented character attaches like one on the
1455            // character (observed with \hat{\hat{x}}).
1456            nuc_char = Some(fc);
1457            char_nucleus = true;
1458        }
1459        let mut x = self.clean_box(nuc, st, list_sub::NUCLEUS);
1460        let (w, mut h) = self
1461            .nodes
1462            .box_data(x)
1463            .map_or((0, 0), |b| (b.width, b.height));
1464        // Switch to a larger accent if available and appropriate.
1465        let fixed = matches!(
1466            self.nodes.subtype(q),
1467            crate::nodes::accent_sub::FIXED_TOP | crate::nodes::accent_sub::FIXED_BOTH
1468        );
1469        if !fixed {
1470            loop {
1471                let (tag, rem) = self.char_tag(f, c);
1472                if tag != 2 {
1473                    break;
1474                }
1475                let Some(m) = self.fonts.char_metrics(f, rem).copied() else {
1476                    break;
1477                };
1478                if m.width > w {
1479                    break;
1480                }
1481                c = rem;
1482                i = m;
1483            }
1484        }
1485        let _ = i;
1486        let opentype = self.is_opentype_math_font(f);
1487        // OpenType fonts lower the accent by at most AccentBaseHeight.
1488        let xh = if opentype {
1489            self.font(f)
1490                .math_constants
1491                .as_ref()
1492                .and_then(|m| m.get("AccentBaseHeight").copied())
1493                .unwrap_or_else(|| self.x_height(f))
1494        } else {
1495            self.x_height(f)
1496        };
1497        let mut delta = if h < xh { h } else { xh };
1498        let sup = self.noad_field(q, Field::Sup);
1499        let sub = self.noad_field(q, Field::Sub);
1500        if (!sup.is_null() || !sub.is_null())
1501            && !nuc.is_null()
1502            && self.nodes.id(nuc) == id::MATH_CHAR
1503        {
1504            // Swap the subscript and superscript into box x.
1505            self.nodes.flush_list(x);
1506            let n = self.new_noad(noad_sub::ORD);
1507            let nk = self.take_field(q, Field::Nucleus);
1508            let a = self.take_field(q, Field::Sup);
1509            let b = self.take_field(q, Field::Sub);
1510            self.set_noad_field(n, Field::Nucleus, nk);
1511            self.set_noad_field(n, Field::Sup, a);
1512            self.set_noad_field(n, Field::Sub, b);
1513            let k = self.new_sub_list(true, n);
1514            self.set_noad_field(q, Field::Nucleus, k);
1515            x = self.clean_box(k, self.ts.math.style, list_sub::NUCLEUS);
1516            let nh = self.nodes.box_data(x).map_or(0, |b| b.height);
1517            delta = delta + nh - h;
1518            h = nh;
1519        }
1520        // The accent's box takes the accent's attributes (observed).
1521        let saved = self.attr_from(acc);
1522        let y = self.char_box(f, c);
1523        self.attr_restore(saved);
1524        let yw = self.nodes.box_data(y).map_or(0, |b| b.width);
1525        let shift = if opentype {
1526            // Top accent attachments: that of a character nucleus (else
1527            // half its width) minus that of the accent (else half its
1528            // width).
1529            let a = if char_nucleus {
1530                nuc_char
1531                    .and_then(|(nf, nc)| self.math_char(nf, nc).and_then(|m| m.top_accent))
1532                    .unwrap_or(half(w))
1533            } else {
1534                half(w)
1535            };
1536            let b = self
1537                .math_char(f, c)
1538                .and_then(|m| m.top_accent)
1539                .unwrap_or(half(yw));
1540            a - b
1541        } else if char_nucleus {
1542            // For a character nucleus LuaTeX follows TeX82; otherwise it
1543            // centers one unit to the right (observed: half(w - wa) + 1).
1544            s + half(w - yw)
1545        } else {
1546            s + half(w - yw) + 1
1547        };
1548        if let Some(b) = self.nodes.box_data_mut(y) {
1549            b.shift = shift;
1550            b.width = 0;
1551        }
1552        let p = self.new_kern_node(-delta, kern_sub::FONT);
1553        self.nodes.link(p, x);
1554        self.nodes.link(y, p);
1555        let y = self.vpack(y, PackSpec::NATURAL);
1556        self.nodes.set_subtype(y, list_sub::ACCENT);
1557        let mut xw = self.nodes.box_data(x).map_or(0, |b| b.width);
1558        // Observed: an OpenType accent on a character includes the
1559        // character's italic correction unless it ends the list (LuaTeX
1560        // 1.24: always, through the character's box).
1561        if opentype
1562            && !self.math_124
1563            && !self.nodes.next(q).is_null()
1564            && let Some((nf, nc)) = nuc_char
1565            && self.nodes.id(nuc) == id::MATH_CHAR
1566        {
1567            xw += self.fonts.char_metrics(nf, nc).map_or(0, |m| m.italic);
1568        }
1569        let yh = self.nodes.box_data(y).map_or(0, |b| b.height);
1570        if let Some(b) = self.nodes.box_data_mut(y) {
1571            b.width = xw;
1572        }
1573        if yh < h {
1574            let p = self.new_kern_node(h - yh, kern_sub::FONT);
1575            let l = self.nodes.list_ptr(y);
1576            self.nodes.link(p, l);
1577            self.nodes.set_list_ptr(y, p);
1578            if let Some(b) = self.nodes.box_data_mut(y) {
1579                b.height = h;
1580            }
1581        }
1582        self.set_nucleus_box(q, y);
1583        let bot = self.nodes.noad(q).map_or(NodeRef::NULL, |n| n.bot_accent);
1584        if !bot.is_null() {
1585            self.make_bottom_accent(q);
1586        }
1587    }
1588
1589    /// A bottom accent (`\Umathaccent bottom`): centered below the
1590    /// nucleus.
1591    fn make_bottom_accent(&mut self, q: NodeRef) {
1592        let bot = match self.nodes.noad_mut(q) {
1593            Some(n) => std::mem::replace(&mut n.bot_accent, NodeRef::NULL),
1594            None => return,
1595        };
1596        let Some((f, mut c, _)) = self.fetch(bot) else {
1597            self.nodes.flush_list(bot);
1598            return;
1599        };
1600        let bot_attr = self.attr_of(bot);
1601        self.nodes.flush_list(bot);
1602        let fixed = matches!(
1603            self.nodes.subtype(q),
1604            crate::nodes::accent_sub::FIXED_BOTTOM | crate::nodes::accent_sub::FIXED_BOTH
1605        );
1606        let nuc = self.noad_field(q, Field::Nucleus);
1607        let char_nucleus = !nuc.is_null() && self.nodes.id(nuc) == id::MATH_CHAR;
1608        let nuc_char = if char_nucleus {
1609            self.fetch(nuc).map(|(nf, nc, _)| (nf, nc))
1610        } else {
1611            None
1612        };
1613        // As for top accents, the scripts of a character go inside.
1614        let sup = self.noad_field(q, Field::Sup);
1615        let sub = self.noad_field(q, Field::Sub);
1616        let x = if char_nucleus && (!sup.is_null() || !sub.is_null()) {
1617            let n = self.new_noad(noad_sub::ORD);
1618            let nk = self.take_field(q, Field::Nucleus);
1619            let a = self.take_field(q, Field::Sup);
1620            let b = self.take_field(q, Field::Sub);
1621            self.set_noad_field(n, Field::Nucleus, nk);
1622            self.set_noad_field(n, Field::Sup, a);
1623            self.set_noad_field(n, Field::Sub, b);
1624            let k = self.new_sub_list(true, n);
1625            self.set_noad_field(q, Field::Nucleus, k);
1626            self.clean_box(k, self.ts.math.style, list_sub::NUCLEUS)
1627        } else {
1628            self.clean_box(nuc, self.ts.math.style, list_sub::NUCLEUS)
1629        };
1630        let (bw, xh) = self
1631            .nodes
1632            .box_data(x)
1633            .map_or((0, 0), |b| (b.width, b.height));
1634        // The accent is centred on the character itself, not on the box
1635        // with its scripts (observed).
1636        let w = match nuc_char {
1637            Some((nf, nc)) => self.fonts.char_metrics(nf, nc).map_or(bw, |m| {
1638                if self.is_opentype_math_font(nf) {
1639                    m.width
1640                } else {
1641                    m.width + m.italic
1642                }
1643            }),
1644            None => bw,
1645        };
1646        // Switch to a wider accent if available, as for top accents.
1647        if !fixed {
1648            loop {
1649                let (tag, rem) = self.char_tag(f, c);
1650                if tag != 2 {
1651                    break;
1652                }
1653                match self.fonts.char_metrics(f, rem) {
1654                    Some(m) if m.width <= w => c = rem,
1655                    _ => break,
1656                }
1657            }
1658        }
1659        let saved = self.attr_set(bot_attr);
1660        let y = self.char_box(f, c);
1661        self.attr_restore(saved);
1662        let yw = self.nodes.box_data(y).map_or(0, |b| b.width);
1663        let shift = if self.is_opentype_math_font(f) {
1664            let a = nuc_char
1665                .filter(|_| self.noad_field(q, Field::Sup).is_null())
1666                .and_then(|(nf, nc)| self.math_char(nf, nc).and_then(|m| m.top_accent))
1667                .unwrap_or(half(w));
1668            let b = self
1669                .math_char(f, c)
1670                .and_then(|m| m.top_accent)
1671                .unwrap_or(half(yw));
1672            a - b
1673        } else if nuc_char.is_some() {
1674            half(w - yw)
1675        } else {
1676            w / 2 - yw / 2 + 1
1677        };
1678        if let Some(b) = self.nodes.box_data_mut(y) {
1679            b.shift = shift;
1680            b.width = 0;
1681        }
1682        self.nodes.link(x, y);
1683        // Observed: the nucleus and the accent are packed naturally and
1684        // the box is lowered so that the nucleus keeps its baseline.
1685        let v = self.vpack(x, PackSpec::NATURAL);
1686        self.nodes.set_subtype(v, list_sub::ACCENT);
1687        if let Some(b) = self.nodes.box_data_mut(v) {
1688            b.shift = b.height - xh;
1689            b.width = bw;
1690        }
1691        self.set_nucleus_box(q, v);
1692    }
1693
1694    /// Builds a generalized fraction (`make_fraction`).
1695    fn make_fraction(&mut self, q: NodeRef) {
1696        let (mut thickness, num, denom, left, right, options) = match self.nodes.data(q) {
1697            NodeData::Fraction(f) => (f.thickness, f.num, f.denom, f.left, f.right, f.options),
1698            _ => return,
1699        };
1700        if thickness == DEFAULT_CODE {
1701            thickness = self.math_par(mp::FRACTION_RULE);
1702        }
1703        let cs = self.ts.math.style;
1704        let mut x = self.clean_box(num, num_style(cs), list_sub::NUMERATOR);
1705        let mut z = self.clean_box(denom, denom_style(cs), list_sub::DENOMINATOR);
1706        let (xw, zw) = (
1707            self.nodes.box_data(x).map_or(0, |b| b.width),
1708            self.nodes.box_data(z).map_or(0, |b| b.width),
1709        );
1710        if xw < zw {
1711            x = self.rebox(x, zw);
1712        } else {
1713            z = self.rebox(z, xw);
1714        }
1715        let (mut shift_up, mut shift_down) = if thickness == 0 {
1716            (
1717                self.math_par(mp::STACK_NUM_UP),
1718                self.math_par(mp::STACK_DENOM_DOWN),
1719            )
1720        } else {
1721            (
1722                self.math_par(mp::FRACTION_NUM_UP),
1723                self.math_par(mp::FRACTION_DENOM_DOWN),
1724            )
1725        };
1726        let (xh, xd) = self
1727            .nodes
1728            .box_data(x)
1729            .map_or((0, 0), |b| (b.height, b.depth));
1730        let (zh, zd) = self
1731            .nodes
1732            .box_data(z)
1733            .map_or((0, 0), |b| (b.height, b.depth));
1734        let axis = self.math_par(mp::AXIS);
1735        let delta;
1736        if thickness == 0 {
1737            let clr = self.math_par(mp::STACK_VGAP);
1738            delta = half(clr - ((shift_up - xd) - (zh - shift_down)));
1739            if delta > 0 {
1740                shift_up += delta;
1741                shift_down += delta;
1742            }
1743        } else {
1744            let exact = options & noad_opt::EXACT == noad_opt::EXACT;
1745            let (clr_n, clr_d) = if exact {
1746                (0, 0)
1747            } else if thickness == self.math_par(mp::FRACTION_RULE) {
1748                (
1749                    self.math_par(mp::FRACTION_NUM_VGAP),
1750                    self.math_par(mp::FRACTION_DENOM_VGAP),
1751                )
1752            } else if cs < 2 {
1753                (3 * thickness, 3 * thickness)
1754            } else {
1755                (thickness, thickness)
1756            };
1757            delta = half(thickness);
1758            let delta1 = clr_n - ((shift_up - xd) - (axis + delta));
1759            let delta2 = clr_d - ((axis - delta) - (zh - shift_down));
1760            if delta1 > 0 {
1761                shift_up += delta1;
1762            }
1763            if delta2 > 0 {
1764                shift_down += delta2;
1765            }
1766        }
1767        // Construct a vlist box for the fraction.
1768        let v = self.new_null_box_node(false);
1769        self.nodes.set_subtype(v, list_sub::FRACTION);
1770        let width = self.nodes.box_data(x).map_or(0, |b| b.width);
1771        if let Some(b) = self.nodes.box_data_mut(v) {
1772            b.height = shift_up + xh;
1773            b.depth = zd + shift_down;
1774            b.width = width;
1775        }
1776        let p;
1777        if thickness == 0 {
1778            p = self.new_kern_node((shift_up - xd) - (zh - shift_down), kern_sub::FONT);
1779            self.nodes.link(p, z);
1780        } else {
1781            let y = self.fraction_rule(thickness);
1782            let p2 = self.new_kern_node((axis - delta) - (zh - shift_down), kern_sub::FONT);
1783            self.nodes.link(y, p2);
1784            self.nodes.link(p2, z);
1785            p = self.new_kern_node((shift_up - xd) - (axis + delta), kern_sub::FONT);
1786            self.nodes.link(p, y);
1787        }
1788        self.nodes.link(x, p);
1789        self.nodes.set_list_ptr(v, x);
1790        // Put the fraction into a box with its delimiters.
1791        let dsize = self.math_par(mp::FRACTION_DEL_SIZE);
1792        let size = self.ts.math.size;
1793        // A null delimiter given explicitly (`\atopwithdelims..`) stays
1794        // on the baseline; a missing one (`\atop`) is centered on the axis
1795        // (observed).
1796        let lx = self.var_delimiter(left, size, dsize, left.is_null());
1797        self.nodes.link(lx, v);
1798        let rz = self.var_delimiter(right, size, dsize, right.is_null());
1799        self.nodes.link(v, rz);
1800        let h = self.hpack_quiet(lx);
1801        self.nodes.set_subtype(h, list_sub::FRACTION);
1802        self.set_new_hlist(q, h);
1803    }
1804
1805    /// Builds an operator (`make_op`); returns the italic correction.
1806    fn make_op(&mut self, q: NodeRef) -> i32 {
1807        let cs = self.ts.math.style;
1808        if self.nodes.subtype(q) == noad_sub::OP_DISPLAY_LIMITS && cs < 2 {
1809            self.nodes.set_subtype(q, noad_sub::OP_LIMITS);
1810        }
1811        let limits = self.nodes.subtype(q) == noad_sub::OP_LIMITS;
1812        let nuc = self.noad_field(q, Field::Nucleus);
1813        let mut delta = 0;
1814        let mut opentype = false;
1815        if !nuc.is_null() && self.nodes.id(nuc) == id::MATH_CHAR {
1816            if let Some((f, c, m)) = self.fetch(nuc) {
1817                let mut m = m;
1818                if cs < 2 && self.is_opentype_math_font(f) {
1819                    // Observed (TeX Gyre Termes Math): the first variant
1820                    // at least DisplayOperatorMinHeight high (height plus
1821                    // depth), else the largest.
1822                    let target = self.math_par(mp::OPERATOR_SIZE);
1823                    let mut c = c;
1824                    let mut guard = 0;
1825                    while m.height + m.depth < target && guard < 64 {
1826                        guard += 1;
1827                        let (tag, rem) = self.char_tag(f, c);
1828                        let Some(m2) = self
1829                            .fonts
1830                            .char_metrics(f, rem)
1831                            .copied()
1832                            .filter(|_| tag == 2)
1833                        else {
1834                            break;
1835                        };
1836                        m = m2;
1837                        c = rem;
1838                    }
1839                    if let NodeData::MathChar { chr, .. } = self.nodes.data_mut(nuc) {
1840                        *chr = c;
1841                    }
1842                } else if cs < 2 {
1843                    let (tag, rem) = self.char_tag(f, c);
1844                    if tag == 2
1845                        && let Some(m2) = self.fonts.char_metrics(f, rem).copied()
1846                    {
1847                        m = m2;
1848                        if let NodeData::MathChar { chr, .. } = self.nodes.data_mut(nuc) {
1849                            *chr = rem;
1850                        }
1851                    }
1852                }
1853                delta = m.italic;
1854                opentype = self.is_opentype_math_font(f);
1855            }
1856            let saved = std::mem::replace(&mut self.math_op_nucleus, true);
1857            let x = self.clean_box(nuc, cs, list_sub::NUCLEUS);
1858            self.math_op_nucleus = saved;
1859            let has_sub = !self.noad_field(q, Field::Sub).is_null();
1860            let no_limits = self.nodes.subtype(q) == noad_sub::OP_NO_LIMITS;
1861            let axis = self.math_par(mp::AXIS);
1862            let math_124 = self.math_124;
1863            if let Some(b) = self.nodes.box_data_mut(x) {
1864                if has_sub && !limits && !opentype {
1865                    b.width -= delta;
1866                }
1867                if opentype && math_124 {
1868                    // LuaTeX 1.24: the box includes the italic correction,
1869                    // except with \nolimits, and loses it (again) before a
1870                    // subscript that is no limit (observed).
1871                    b.width += delta;
1872                    if no_limits {
1873                        b.width -= delta;
1874                    }
1875                    if has_sub && !limits {
1876                        b.width -= delta;
1877                    }
1878                }
1879                b.shift = half(b.height - b.depth) - axis;
1880            }
1881            self.set_nucleus_box(q, x);
1882            // An OpenType operator's box has no italic correction (LuaTeX
1883            // 1.22); with \nolimits the subscript moves left by it (a font
1884            // kern; in 1.24 the narrower box) and a lone superscript is
1885            // not moved.
1886            if opentype && !limits {
1887                if !has_sub {
1888                    delta = 0;
1889                } else if !math_124 {
1890                    self.ts.math.op_kern = -delta;
1891                }
1892            }
1893        }
1894        if limits {
1895            // Construct a box with limits above and below it.
1896            let supk = self.noad_field(q, Field::Sup);
1897            let subk = self.noad_field(q, Field::Sub);
1898            let x = self.clean_box(supk, sup_style(cs), list_sub::SUP);
1899            let nk = self.noad_field(q, Field::Nucleus);
1900            let y = self.clean_box(nk, cs, list_sub::NUCLEUS);
1901            let z = self.clean_box(subk, sub_style(cs), list_sub::SUB);
1902            let v = self.new_null_box_node(false);
1903            self.nodes.set_subtype(v, list_sub::LIMITS);
1904            let wy = self.nodes.box_data(y).map_or(0, |b| b.width);
1905            let wx = self.nodes.box_data(x).map_or(0, |b| b.width);
1906            let wz = self.nodes.box_data(z).map_or(0, |b| b.width);
1907            let wv = wy.max(wx).max(wz);
1908            let x = self.rebox(x, wv);
1909            let y = self.rebox(y, wv);
1910            let z = self.rebox(z, wv);
1911            if let Some(b) = self.nodes.box_data_mut(x) {
1912                b.shift = half(delta);
1913            }
1914            if let Some(b) = self.nodes.box_data_mut(z) {
1915                b.shift = -half(delta);
1916            }
1917            let (yh, yd) = self
1918                .nodes
1919                .box_data(y)
1920                .map_or((0, 0), |b| (b.height, b.depth));
1921            let (mut vh, mut vd) = (yh, yd);
1922            let kern = self.math_par(mp::LIMIT_ABOVE_KERN);
1923            let kern_below = self.math_par(mp::LIMIT_BELOW_KERN);
1924            let head;
1925            if supk.is_null() {
1926                self.nodes.free(x);
1927                head = y;
1928            } else {
1929                let (xh, xd) = self
1930                    .nodes
1931                    .box_data(x)
1932                    .map_or((0, 0), |b| (b.height, b.depth));
1933                let mut shift_up = self.math_par(mp::LIMIT_ABOVE_BGAP) - xd;
1934                let g1 = self.math_par(mp::LIMIT_ABOVE_VGAP);
1935                if shift_up < g1 {
1936                    shift_up = g1;
1937                }
1938                let p = self.new_kern_node(shift_up, kern_sub::FONT);
1939                self.nodes.link(p, y);
1940                self.nodes.link(x, p);
1941                let p = self.new_kern_node(kern, kern_sub::FONT);
1942                self.nodes.link(p, x);
1943                head = p;
1944                vh += kern + xh + xd + shift_up;
1945            }
1946            if subk.is_null() {
1947                self.nodes.free(z);
1948            } else {
1949                let (zh, zd) = self
1950                    .nodes
1951                    .box_data(z)
1952                    .map_or((0, 0), |b| (b.height, b.depth));
1953                let mut shift_down = self.math_par(mp::LIMIT_BELOW_BGAP) - zh;
1954                let g2 = self.math_par(mp::LIMIT_BELOW_VGAP);
1955                if shift_down < g2 {
1956                    shift_down = g2;
1957                }
1958                let p = self.new_kern_node(shift_down, kern_sub::FONT);
1959                self.nodes.link(y, p);
1960                self.nodes.link(p, z);
1961                let p = self.new_kern_node(kern_below, kern_sub::FONT);
1962                self.nodes.link(z, p);
1963                vd += kern_below + zh + zd + shift_down;
1964            }
1965            if let Some(b) = self.nodes.box_data_mut(v) {
1966                b.width = wv;
1967                b.height = vh;
1968                b.depth = vd;
1969                b.head = head;
1970            }
1971            // The scripts are now part of the box.
1972            let a = self.take_field(q, Field::Sup);
1973            let b = self.take_field(q, Field::Sub);
1974            self.nodes.flush_list(a);
1975            self.nodes.flush_list(b);
1976            self.set_new_hlist(q, v);
1977        }
1978        delta
1979    }
1980
1981    /// Ligatures and kerns between ordinary characters (`make_ord`).
1982    fn make_ord(&mut self, q: NodeRef) {
1983        'restart: loop {
1984            if !self.noad_field(q, Field::Sub).is_null()
1985                || !self.noad_field(q, Field::Sup).is_null()
1986            {
1987                return;
1988            }
1989            let nuc = self.noad_field(q, Field::Nucleus);
1990            if nuc.is_null() || self.nodes.id(nuc) != id::MATH_CHAR {
1991                return;
1992            }
1993            let p = self.nodes.next(q);
1994            if p.is_null()
1995                || self.nodes.id(p) != id::NOAD
1996                || !matches!(
1997                    self.nodes.subtype(p),
1998                    noad_sub::ORD
1999                        | noad_sub::OP_DISPLAY_LIMITS
2000                        | noad_sub::OP_LIMITS
2001                        | noad_sub::OP_NO_LIMITS
2002                        | noad_sub::BIN
2003                        | noad_sub::REL
2004                        | noad_sub::OPEN
2005                        | noad_sub::CLOSE
2006                        | noad_sub::PUNCT
2007                )
2008            {
2009                return;
2010            }
2011            let pn = self.noad_field(p, Field::Nucleus);
2012            if pn.is_null() || self.nodes.id(pn) != id::MATH_CHAR {
2013                return;
2014            }
2015            let (qfam, pfam, pchr) = match (self.nodes.data(nuc), self.nodes.data(pn)) {
2016                (NodeData::MathChar { fam: a, .. }, NodeData::MathChar { fam: b, chr }) => {
2017                    (*a, *b, *chr)
2018                }
2019                _ => return,
2020            };
2021            if qfam != pfam {
2022                return;
2023            }
2024            self.nodes.get_mut(nuc).id = id::MATH_TEXT_CHAR;
2025            let Some((f, c, _)) = self.fetch(nuc) else {
2026                return;
2027            };
2028            let Some(op) = self
2029                .lig_kern_step(f, c, pchr)
2030                .or_else(|| self.base_mode_kern(f, c, pchr))
2031            else {
2032                return;
2033            };
2034            match op {
2035                LigOp::Kern(k) => {
2036                    let kn = self.new_kern_node(k, kern_sub::FONT);
2037                    let n = self.nodes.next(q);
2038                    self.nodes.link(kn, n);
2039                    self.nodes.link(q, kn);
2040                    return;
2041                }
2042                LigOp::Lig { op, chr } => {
2043                    match op {
2044                        1 | 5 => self.set_char(nuc, chr),
2045                        2 | 6 => self.set_char(pn, chr),
2046                        3 | 7 | 11 => {
2047                            let r = self.new_noad(noad_sub::ORD);
2048                            let k = self.new_math_char(qfam, chr);
2049                            if op >= 11 {
2050                                self.nodes.get_mut(k).id = id::MATH_TEXT_CHAR;
2051                            }
2052                            self.set_noad_field(r, Field::Nucleus, k);
2053                            self.nodes.link(q, r);
2054                            self.nodes.link(r, p);
2055                        }
2056                        _ => {
2057                            let n = self.nodes.next(p);
2058                            self.nodes.link(q, n);
2059                            if n.is_null() {
2060                                self.nodes.set_next(q, NodeRef::NULL);
2061                            }
2062                            self.set_char(nuc, chr);
2063                            let a = self.take_field(p, Field::Sub);
2064                            let b = self.take_field(p, Field::Sup);
2065                            self.set_noad_field(q, Field::Sub, a);
2066                            self.set_noad_field(q, Field::Sup, b);
2067                            self.nodes.set_next(p, NodeRef::NULL);
2068                            self.nodes.flush_list(p);
2069                        }
2070                    }
2071                    if op > 3 {
2072                        return;
2073                    }
2074                    self.nodes.get_mut(nuc).id = id::MATH_CHAR;
2075                    continue 'restart;
2076                }
2077            }
2078        }
2079    }
2080
2081    fn set_char(&mut self, k: NodeRef, c: u32) {
2082        if let NodeData::MathChar { chr, .. } = self.nodes.data_mut(k) {
2083            *chr = c;
2084        }
2085    }
2086
2087    /// Converts the nucleus of `q` to an hlist and attaches the scripts
2088    /// (§754–§755).
2089    fn convert_nucleus(&mut self, q: NodeRef, delta: i32) {
2090        let mut delta = delta;
2091        let nuc = self.noad_field(q, Field::Nucleus);
2092        let kid = if nuc.is_null() { 0 } else { self.nodes.id(nuc) };
2093        let has_sub = !self.noad_field(q, Field::Sub).is_null();
2094        let p = match kid {
2095            id::MATH_CHAR | id::MATH_TEXT_CHAR => match self.fetch(nuc) {
2096                Some((f, c, m)) => {
2097                    delta = m.italic;
2098                    // The glyph and its italic correction take the
2099                    // character's attributes (observed).
2100                    let saved = self.attr_from(nuc);
2101                    let p = self.new_math_glyph(f, c);
2102                    let has_sup = !self.noad_field(q, Field::Sup).is_null();
2103                    if self.is_opentype_math_font(f) {
2104                        // Observed: an OpenType math character gets its
2105                        // italic correction before a character of the same
2106                        // family (a "text char"), before a superscript and
2107                        // in its scripts, but not otherwise. LuaTeX 1.24
2108                        // adds it in all cases but an operator's nucleus.
2109                        if kid == id::MATH_CHAR
2110                            && !has_sup
2111                            && !has_sub
2112                            && (!self.math_124 || self.math_op_nucleus)
2113                        {
2114                            delta = 0;
2115                        }
2116                    } else if kid == id::MATH_TEXT_CHAR
2117                        && self.font(f).params.get(2).copied().unwrap_or(0) != 0
2118                        && !self.math_124
2119                    {
2120                        // A text character in a text font (\fontdimen2 not
2121                        // zero) has no italic correction (§752), except in
2122                        // LuaTeX 1.24.
2123                        delta = 0;
2124                    }
2125                    // Observed: an OpenType character's italic correction
2126                    // before another character takes the noad's attributes.
2127                    if kid == id::MATH_TEXT_CHAR && self.is_opentype_math_font(f) {
2128                        self.attr_restore(saved.clone());
2129                    }
2130                    if !has_sub && delta != 0 {
2131                        let k = self.new_kern_node(delta, kern_sub::ITALIC);
2132                        self.nodes.link(p, k);
2133                        delta = 0;
2134                    }
2135                    self.attr_restore(saved);
2136                    p
2137                }
2138                None => NodeRef::NULL,
2139            },
2140            id::SUB_BOX => {
2141                let b = self.nodes.list_ptr_kernel(nuc);
2142                self.nodes.set_kernel_list(nuc, NodeRef::NULL);
2143                let k = std::mem::take(&mut self.ts.math.op_kern);
2144                if k != 0 && !b.is_null() {
2145                    let kn = self.new_kern_node(k, kern_sub::FONT);
2146                    self.nodes.link(b, kn);
2147                }
2148                b
2149            }
2150            id::SUB_MLIST => {
2151                let l = self.nodes.list_ptr_kernel(nuc);
2152                self.nodes.set_kernel_list(nuc, NodeRef::NULL);
2153                let hl = self.clean_box_mlist(l, self.ts.math.style);
2154                // The box takes the sub-list's attributes (observed).
2155                let saved = self.attr_from(nuc);
2156                let b = self.hpack_quiet(hl);
2157                self.attr_restore(saved);
2158                b
2159            }
2160            _ => {
2161                // LuaTeX gives an empty nucleus with scripts an empty box.
2162                if has_sub || !self.noad_field(q, Field::Sup).is_null() {
2163                    self.new_null_box_node(true)
2164                } else {
2165                    NodeRef::NULL
2166                }
2167            }
2168        };
2169        self.set_new_hlist(q, p);
2170        let has_sup = !self.noad_field(q, Field::Sup).is_null();
2171        if !has_sub && !has_sup {
2172            return;
2173        }
2174        self.make_scripts(q, delta);
2175    }
2176
2177    /// Attaches subscript and superscript (`make_scripts`).
2178    fn make_scripts(&mut self, q: NodeRef, delta: i32) {
2179        let p = self.get_new_hlist(q);
2180        let cs = self.ts.math.style;
2181        let (mut shift_up, mut shift_down);
2182        if !p.is_null() && self.nodes.is_glyph(p) {
2183            shift_up = 0;
2184            shift_down = 0;
2185        } else {
2186            let (h, d) = self.list_height_depth(p);
2187            shift_up = h - self.math_par(mp::SUP_SHIFT_DROP);
2188            shift_down = d + self.math_par(mp::SUB_SHIFT_DROP);
2189        }
2190        let opts = self.nodes.noad(q).map_or(0, |n| n.options);
2191        let supk = self.take_field(q, Field::Sup);
2192        let subk = self.take_field(q, Field::Sub);
2193        if !p.is_null() && self.nodes.is_glyph(p) {
2194            // LuaTeX looks at character scripts in the current size (for
2195            // OpenType math kerns), which reports undefined families.
2196            for k in [supk, subk] {
2197                if !k.is_null() && self.nodes.id(k) == id::MATH_CHAR {
2198                    let _ = self.fetch(k);
2199                }
2200            }
2201        }
2202        let script_space = self.math_par(mp::SPACE_AFTER_SCRIPT);
2203        let x;
2204        if supk.is_null() {
2205            // A subscript without superscript.
2206            let st = if opts & noad_opt::NO_SUB_SCRIPT == noad_opt::NO_SUB_SCRIPT {
2207                cs
2208            } else {
2209                sub_style(cs)
2210            };
2211            x = self.clean_box(subk, st, list_sub::SUB);
2212            self.nodes.flush_list(subk);
2213            let xh = self.nodes.box_data(x).map_or(0, |b| b.height);
2214            if let Some(b) = self.nodes.box_data_mut(x) {
2215                b.width += script_space;
2216            }
2217            let s1 = self.math_par(mp::SUB_SHIFT_DOWN);
2218            if shift_down < s1 {
2219                shift_down = s1;
2220            }
2221            let clr = xh - self.math_par(mp::SUB_TOP_MAX);
2222            if shift_down < clr {
2223                shift_down = clr;
2224            }
2225            if let Some(b) = self.nodes.box_data_mut(x) {
2226                b.shift = shift_down;
2227            }
2228        } else {
2229            let st = if opts & noad_opt::NO_SUPER_SCRIPT == noad_opt::NO_SUPER_SCRIPT {
2230                cs
2231            } else {
2232                sup_style(cs)
2233            };
2234            let sx = self.clean_box(supk, st, list_sub::SUP);
2235            self.nodes.flush_list(supk);
2236            let xd = self.nodes.box_data(sx).map_or(0, |b| b.depth);
2237            if let Some(b) = self.nodes.box_data_mut(sx) {
2238                b.width += script_space;
2239            }
2240            let clr = self.math_par(mp::SUP_SHIFT_UP);
2241            if shift_up < clr {
2242                shift_up = clr;
2243            }
2244            let clr = xd + self.math_par(mp::SUP_BOTTOM_MIN);
2245            if shift_up < clr {
2246                shift_up = clr;
2247            }
2248            if subk.is_null() {
2249                if let Some(b) = self.nodes.box_data_mut(sx) {
2250                    b.shift = -shift_up;
2251                }
2252                x = sx;
2253            } else {
2254                let st = if opts & noad_opt::NO_SUB_SCRIPT == noad_opt::NO_SUB_SCRIPT {
2255                    cs
2256                } else {
2257                    sub_style(cs)
2258                };
2259                let y = self.clean_box(subk, st, list_sub::SUB);
2260                self.nodes.flush_list(subk);
2261                let yh = self.nodes.box_data(y).map_or(0, |b| b.height);
2262                if let Some(b) = self.nodes.box_data_mut(y) {
2263                    b.width += script_space;
2264                }
2265                let s2 = self.math_par(mp::SUB_SUP_SHIFT_DOWN);
2266                if shift_down < s2 {
2267                    shift_down = s2;
2268                }
2269                let mut clr =
2270                    self.math_par(mp::SUB_SUP_VGAP) - ((shift_up - xd) - (yh - shift_down));
2271                if clr > 0 {
2272                    shift_down += clr;
2273                    clr = self.math_par(mp::SUP_SUB_BOTTOM_MAX) - (shift_up - xd);
2274                    if clr > 0 {
2275                        shift_up += clr;
2276                        shift_down -= clr;
2277                    }
2278                }
2279                if let Some(b) = self.nodes.box_data_mut(sx) {
2280                    b.shift = delta;
2281                }
2282                let p = self.new_kern_node((shift_up - xd) - (yh - shift_down), kern_sub::FONT);
2283                self.nodes.link(sx, p);
2284                self.nodes.link(p, y);
2285                x = self.vpack(sx, PackSpec::NATURAL);
2286                self.nodes.set_subtype(x, list_sub::SCRIPTS);
2287                if let Some(b) = self.nodes.box_data_mut(x) {
2288                    b.shift = shift_down;
2289                }
2290            }
2291        }
2292        let nh = self.get_new_hlist(q);
2293        if nh.is_null() {
2294            self.set_new_hlist(q, x);
2295        } else {
2296            let t = self.nodes.tail_of(nh);
2297            self.nodes.link(t, x);
2298        }
2299    }
2300
2301    /// The box of `\Uvextensible`, as observed in LuaTeX: without a
2302    /// height and depth the delimiter's smallest variant, unshifted;
2303    /// otherwise the variant for their sum, centered on the baseline (on
2304    /// the axis with `axis`), in a box as high and deep as asked (raised
2305    /// by the axis with `axis`), or as the delimiter with `exact`.
2306    fn extensible_fence(
2307        &mut self,
2308        delim: NodeRef,
2309        height: i32,
2310        depth: i32,
2311        options: u32,
2312        axis: i32,
2313    ) -> NodeRef {
2314        let size = self.ts.math.size;
2315        if height == 0 && depth == 0 {
2316            let b = self.var_delimiter(delim, size, 0, false);
2317            if let Some(bd) = self.nodes.box_data_mut(b) {
2318                bd.shift = 0;
2319            }
2320            return b;
2321        }
2322        let up = if options & noad_opt::AXIS == noad_opt::AXIS {
2323            axis
2324        } else {
2325            0
2326        };
2327        let b = self.var_delimiter(delim, size, height + depth, false);
2328        if let Some(bd) = self.nodes.box_data_mut(b) {
2329            bd.shift = half(bd.height - bd.depth) - up;
2330        }
2331        let w = self.hpack_quiet(b);
2332        if options & noad_opt::EXACT != noad_opt::EXACT
2333            && let Some(bd) = self.nodes.box_data_mut(w)
2334        {
2335            bd.height = height + up;
2336            bd.depth = depth - up;
2337        }
2338        w
2339    }
2340
2341    /// Builds the delimiter of a fence (`make_left_right`).
2342    fn make_left_right(&mut self, q: NodeRef, style: i32, max_d: i32, max_h: i32) -> NoadType {
2343        self.set_cur_style(style);
2344        let (delim, height, depth, options) = match self.nodes.data(q) {
2345            NodeData::Fence(f) => (f.delim, f.height, f.depth, f.options),
2346            _ => (NodeRef::NULL, 0, 0, 0),
2347        };
2348        let axis = self.math_par(mp::AXIS);
2349        if self.nodes.subtype(q) == fence_sub::NO {
2350            let b = self.extensible_fence(delim, height, depth, options, axis);
2351            self.set_new_hlist(q, b);
2352            return NoadType::Close;
2353        }
2354        let b = if height != 0 || depth != 0 {
2355            let total = height + depth;
2356            let b = self.var_delimiter(delim, self.ts.math.size, total, true);
2357            if options & noad_opt::NO_AXIS == noad_opt::NO_AXIS
2358                && let Some(bd) = self.nodes.box_data_mut(b)
2359            {
2360                bd.shift = 0;
2361            }
2362            b
2363        } else {
2364            let mut delta2 = max_d + axis;
2365            let mut delta1 = max_h + max_d - delta2;
2366            if delta2 > delta1 {
2367                delta1 = delta2;
2368            }
2369            let mut delta = (delta1 / 500) * self.eqtb.int(ip::DELIMITERFACTOR);
2370            delta2 = delta1 + delta1 - self.eqtb.dimen(dp::DELIMITERSHORTFALL);
2371            if delta < delta2 {
2372                delta = delta2;
2373            }
2374            self.var_delimiter(delim, self.ts.math.size, delta, false)
2375        };
2376        self.set_new_hlist(q, b);
2377        if self.nodes.subtype(q) == fence_sub::LEFT {
2378            NoadType::Open
2379        } else {
2380            NoadType::Close
2381        }
2382    }
2383
2384    // ----- the second pass ---------------------------------------------
2385
2386    fn second_pass(
2387        &mut self,
2388        mlist: NodeRef,
2389        style: i32,
2390        penalties: bool,
2391        max_h: i32,
2392        max_d: i32,
2393    ) -> NodeRef {
2394        let head = self.nodes.new_temp();
2395        let mut p = head;
2396        let mut q = mlist;
2397        let mut r_type: Option<NoadType> = None;
2398        self.set_cur_style(style);
2399        let binpen = self.eqtb.int(ip::BINOPPENALTY);
2400        let relpen = self.eqtb.int(ip::RELPENALTY);
2401        while !q.is_null() {
2402            let next = self.nodes.next(q);
2403            let Some(nt) = self.noad_type(q) else {
2404                if let NodeData::Style { style } = self.nodes.data(q) {
2405                    let s = i32::from(*style);
2406                    self.set_cur_style(s);
2407                    self.nodes.free(q);
2408                } else {
2409                    self.nodes.link(p, q);
2410                    self.nodes.set_next(q, NodeRef::NULL);
2411                    p = q;
2412                }
2413                q = next;
2414                continue;
2415            };
2416            let mut t = NoadType::Ord;
2417            let mut pen = 10000;
2418            match nt {
2419                NoadType::Op
2420                | NoadType::Open
2421                | NoadType::Close
2422                | NoadType::Punct
2423                | NoadType::Inner => t = nt,
2424                NoadType::Bin => {
2425                    t = NoadType::Bin;
2426                    pen = binpen;
2427                }
2428                NoadType::Rel => {
2429                    t = NoadType::Rel;
2430                    pen = relpen;
2431                }
2432                NoadType::Fraction => t = NoadType::Inner,
2433                NoadType::Left | NoadType::Right => {
2434                    let saved = self.attr_from(q);
2435                    t = self.make_left_right(q, style, max_d, max_h);
2436                    self.attr_restore(saved);
2437                }
2438                _ => {}
2439            }
2440            // Inter-element spacing.
2441            if let Some(rt) = r_type {
2442                let style = self.ts.math.style as u32;
2443                if let Some((g, sub)) = self.math_spacing(rt as u32, t as u32, style) {
2444                    let y = self.math_glue(g, self.ts.math.mu);
2445                    // Observed: the glue takes the attributes of the node
2446                    // before it.
2447                    let saved = self.attr_from(p);
2448                    let z = self.new_glue_node(y, sub);
2449                    self.attr_restore(saved);
2450                    self.nodes.link(p, z);
2451                    p = z;
2452                }
2453            }
2454            // Append the new_hlist entries and penalties.
2455            let nh = self.get_new_hlist(q);
2456            self.set_new_hlist(q, NodeRef::NULL);
2457            if !nh.is_null() {
2458                self.nodes.link(p, nh);
2459                p = self.nodes.tail_of(nh);
2460            }
2461            if penalties && !next.is_null() && pen < 10000 {
2462                let nid = self.nodes.id(next);
2463                let next_is_rel = self.noad_type(next) == Some(NoadType::Rel);
2464                if nid != id::PENALTY && !next_is_rel {
2465                    // Observed: the penalty takes the noad's attributes.
2466                    let saved = self.attr_from(q);
2467                    let z = self.new_penalty_node(pen);
2468                    self.attr_restore(saved);
2469                    self.nodes.set_subtype(z, 5);
2470                    self.nodes.link(p, z);
2471                    p = z;
2472                }
2473            }
2474            if nt == NoadType::Right {
2475                t = NoadType::Open;
2476            }
2477            r_type = Some(t);
2478            // delete_q:
2479            self.nodes.set_next(q, NodeRef::NULL);
2480            self.nodes.flush_list(q);
2481            q = next;
2482        }
2483        let first = self.nodes.next(head);
2484        self.nodes.free(head);
2485        if !first.is_null() {
2486            self.nodes.set_prev(first, NodeRef::NULL);
2487        }
2488        first
2489    }
2490}
2491
2492#[derive(Clone, Copy, PartialEq, Eq)]
2493enum Action {
2494    Convert,
2495    CheckDimensions,
2496    DoneWithNoad,
2497}