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 box of accent character `c` of font `f` (the variant chosen
1356    /// for width `w`): if it is the last variant, narrower than `w`, and
1357    /// the font has an assembly for it, that assembly stretched to `w`
1358    /// (observed: LuaTeX's `\overbracket` and `\underbracket` of
1359    /// unicode-math over wide formulas). The flag tells which.
1360    fn accent_char_box(&mut self, f: u32, c: u32, w: i32, fixed: bool) -> (NodeRef, bool) {
1361        if !fixed
1362            && self.is_opentype_math_font(f)
1363            && self.char_tag(f, c).0 != 2
1364            && self.fonts.char_metrics(f, c).is_some_and(|m| m.width < w)
1365            && let Some(parts) = self.math_char(f, c).and_then(|mc| mc.horiz.clone())
1366        {
1367            let overlap = self.math_par(mp::CONNECTOR_OVERLAP_MIN);
1368            return (self.ot_h_extensible_box(f, &parts, w, overlap), true);
1369        }
1370        (self.char_box(f, c), false)
1371    }
1372
1373    /// The horizontal assembly of `parts` (left first) in font `f`, `w`
1374    /// wide (see [`Engine::ot_extensible_box`]).
1375    fn ot_h_extensible_box(
1376        &mut self,
1377        f: u32,
1378        parts: &[crate::fonts::ExtPart],
1379        w: i32,
1380        overlap: i32,
1381    ) -> NodeRef {
1382        let expand = |n: usize| -> Vec<crate::fonts::ExtPart> {
1383            let mut out = Vec::new();
1384            for p in parts.iter() {
1385                let k = if p.extender { n } else { 1 };
1386                for _ in 0..k {
1387                    out.push(*p);
1388                }
1389            }
1390            out
1391        };
1392        let joint = |a: &crate::fonts::ExtPart, b: &crate::fonts::ExtPart| {
1393            let max = a.end.min(b.start).max(0);
1394            (max, overlap.min(max))
1395        };
1396        let largest = |seq: &[crate::fonts::ExtPart]| -> i64 {
1397            let mut t: i64 = seq.iter().map(|p| i64::from(p.advance)).sum();
1398            for win in seq.windows(2) {
1399                t -= i64::from(joint(&win[0], &win[1]).1);
1400            }
1401            t
1402        };
1403        let has_extender = parts.iter().any(|p| p.extender);
1404        let mut n = 0;
1405        let mut seq = expand(n);
1406        while has_extender && largest(&seq) < i64::from(w) && n < 1000 {
1407            n += 1;
1408            seq = expand(n);
1409        }
1410        let mut head = NodeRef::NULL;
1411        let mut tail = NodeRef::NULL;
1412        for (i, p) in seq.iter().enumerate() {
1413            if i > 0 {
1414                let (max, min) = joint(&seq[i - 1], p);
1415                let g = crate::glue::Glue {
1416                    width: -max,
1417                    stretch: max - min,
1418                    ..crate::glue::Glue::ZERO
1419                };
1420                let gn = self.new_glue_node(g, 0);
1421                self.nodes.link(tail, gn);
1422                tail = gn;
1423            }
1424            let b = self.char_box(f, p.code);
1425            if head.is_null() {
1426                head = b;
1427            } else {
1428                self.nodes.link(tail, b);
1429            }
1430            tail = b;
1431        }
1432        let natural = largest(&seq) as i32;
1433        let target = w.max(natural.min(w));
1434        let b = self.hpack(head, PackSpec::Exactly(target));
1435        self.nodes.set_subtype(b, list_sub::H_EXTENSIBLE);
1436        b
1437    }
1438
1439    /// Places a math accent (`make_math_accent`).
1440    fn make_math_accent(&mut self, q: NodeRef) {
1441        let acc = self.nodes.noad(q).map_or(NodeRef::NULL, |n| n.accent);
1442        if acc.is_null() {
1443            // A bottom accent only: place it below the nucleus.
1444            self.make_bottom_accent(q);
1445            return;
1446        }
1447        let Some((f, mut c, mut i)) = self.fetch(acc) else {
1448            return;
1449        };
1450        // Compute the amount of skew.
1451        let mut s = 0;
1452        let nuc = self.noad_field(q, Field::Nucleus);
1453        let mut nuc_char = None;
1454        if !nuc.is_null()
1455            && self.nodes.id(nuc) == id::MATH_CHAR
1456            && let Some((nf, nc, _)) = self.fetch(nuc)
1457        {
1458            nuc_char = Some((nf, nc));
1459            let sk = self.font(nf).skew_char;
1460            if sk >= 0
1461                && let Some(LigOp::Kern(k)) = self.lig_kern_step(nf, nc, sk as u32)
1462            {
1463                s = k;
1464            }
1465        }
1466        let st = cramped_style(self.ts.math.style);
1467        let mut char_nucleus = !nuc.is_null() && self.nodes.id(nuc) == id::MATH_CHAR;
1468        if nuc_char.is_none()
1469            && self.is_opentype_math_font(f)
1470            && let Some(fc) = self.nested_accent_char(nuc)
1471        {
1472            // An accent on an accented character attaches like one on the
1473            // character (observed with \hat{\hat{x}}).
1474            nuc_char = Some(fc);
1475            char_nucleus = true;
1476        }
1477        let mut x = self.clean_box(nuc, st, list_sub::NUCLEUS);
1478        let (w, mut h) = self
1479            .nodes
1480            .box_data(x)
1481            .map_or((0, 0), |b| (b.width, b.height));
1482        // Switch to a larger accent if available and appropriate.
1483        let fixed = matches!(
1484            self.nodes.subtype(q),
1485            crate::nodes::accent_sub::FIXED_TOP | crate::nodes::accent_sub::FIXED_BOTH
1486        );
1487        if !fixed {
1488            loop {
1489                let (tag, rem) = self.char_tag(f, c);
1490                if tag != 2 {
1491                    break;
1492                }
1493                let Some(m) = self.fonts.char_metrics(f, rem).copied() else {
1494                    break;
1495                };
1496                if m.width > w {
1497                    break;
1498                }
1499                c = rem;
1500                i = m;
1501            }
1502        }
1503        let _ = i;
1504        let opentype = self.is_opentype_math_font(f);
1505        // OpenType fonts lower the accent by at most AccentBaseHeight.
1506        let xh = if opentype {
1507            self.font(f)
1508                .math_constants
1509                .as_ref()
1510                .and_then(|m| m.get("AccentBaseHeight").copied())
1511                .unwrap_or_else(|| self.x_height(f))
1512        } else {
1513            self.x_height(f)
1514        };
1515        let mut delta = if h < xh { h } else { xh };
1516        let sup = self.noad_field(q, Field::Sup);
1517        let sub = self.noad_field(q, Field::Sub);
1518        if (!sup.is_null() || !sub.is_null())
1519            && !nuc.is_null()
1520            && self.nodes.id(nuc) == id::MATH_CHAR
1521        {
1522            // Swap the subscript and superscript into box x.
1523            self.nodes.flush_list(x);
1524            let n = self.new_noad(noad_sub::ORD);
1525            let nk = self.take_field(q, Field::Nucleus);
1526            let a = self.take_field(q, Field::Sup);
1527            let b = self.take_field(q, Field::Sub);
1528            self.set_noad_field(n, Field::Nucleus, nk);
1529            self.set_noad_field(n, Field::Sup, a);
1530            self.set_noad_field(n, Field::Sub, b);
1531            let k = self.new_sub_list(true, n);
1532            self.set_noad_field(q, Field::Nucleus, k);
1533            x = self.clean_box(k, self.ts.math.style, list_sub::NUCLEUS);
1534            let nh = self.nodes.box_data(x).map_or(0, |b| b.height);
1535            delta = delta + nh - h;
1536            h = nh;
1537        }
1538        // The accent's box takes the accent's attributes (observed).
1539        let saved = self.attr_from(acc);
1540        let (y, assembled) = self.accent_char_box(f, c, w, fixed);
1541        self.attr_restore(saved);
1542        let yw = self.nodes.box_data(y).map_or(0, |b| b.width);
1543        let shift = if opentype {
1544            // Top accent attachments: that of a character nucleus (else
1545            // half its width) minus that of the accent (else half its
1546            // width).
1547            let a = if char_nucleus {
1548                nuc_char
1549                    .and_then(|(nf, nc)| self.math_char(nf, nc).and_then(|m| m.top_accent))
1550                    .unwrap_or(half(w))
1551            } else {
1552                half(w)
1553            };
1554            let b = self
1555                .math_char(f, c)
1556                .and_then(|m| m.top_accent)
1557                .filter(|_| !assembled)
1558                .unwrap_or(half(yw));
1559            a - b
1560        } else if char_nucleus {
1561            // For a character nucleus LuaTeX follows TeX82; otherwise it
1562            // centers one unit to the right (observed: half(w - wa) + 1).
1563            s + half(w - yw)
1564        } else {
1565            s + half(w - yw) + 1
1566        };
1567        if let Some(b) = self.nodes.box_data_mut(y) {
1568            b.shift = shift;
1569            b.width = 0;
1570        }
1571        let p = self.new_kern_node(-delta, kern_sub::FONT);
1572        self.nodes.link(p, x);
1573        self.nodes.link(y, p);
1574        let y = self.vpack(y, PackSpec::NATURAL);
1575        self.nodes.set_subtype(y, list_sub::ACCENT);
1576        let mut xw = self.nodes.box_data(x).map_or(0, |b| b.width);
1577        // Observed: an OpenType accent on a character includes the
1578        // character's italic correction unless it ends the list (LuaTeX
1579        // 1.24: always, through the character's box).
1580        if opentype
1581            && !self.math_124
1582            && !self.nodes.next(q).is_null()
1583            && let Some((nf, nc)) = nuc_char
1584            && self.nodes.id(nuc) == id::MATH_CHAR
1585        {
1586            xw += self.fonts.char_metrics(nf, nc).map_or(0, |m| m.italic);
1587        }
1588        let yh = self.nodes.box_data(y).map_or(0, |b| b.height);
1589        if let Some(b) = self.nodes.box_data_mut(y) {
1590            b.width = xw;
1591        }
1592        if yh < h {
1593            let p = self.new_kern_node(h - yh, kern_sub::FONT);
1594            let l = self.nodes.list_ptr(y);
1595            self.nodes.link(p, l);
1596            self.nodes.set_list_ptr(y, p);
1597            if let Some(b) = self.nodes.box_data_mut(y) {
1598                b.height = h;
1599            }
1600        }
1601        self.set_nucleus_box(q, y);
1602        let bot = self.nodes.noad(q).map_or(NodeRef::NULL, |n| n.bot_accent);
1603        if !bot.is_null() {
1604            self.make_bottom_accent(q);
1605        }
1606    }
1607
1608    /// A bottom accent (`\Umathaccent bottom`): centered below the
1609    /// nucleus.
1610    fn make_bottom_accent(&mut self, q: NodeRef) {
1611        let bot = match self.nodes.noad_mut(q) {
1612            Some(n) => std::mem::replace(&mut n.bot_accent, NodeRef::NULL),
1613            None => return,
1614        };
1615        let Some((f, mut c, _)) = self.fetch(bot) else {
1616            self.nodes.flush_list(bot);
1617            return;
1618        };
1619        let bot_attr = self.attr_of(bot);
1620        self.nodes.flush_list(bot);
1621        let fixed = matches!(
1622            self.nodes.subtype(q),
1623            crate::nodes::accent_sub::FIXED_BOTTOM | crate::nodes::accent_sub::FIXED_BOTH
1624        );
1625        let nuc = self.noad_field(q, Field::Nucleus);
1626        let char_nucleus = !nuc.is_null() && self.nodes.id(nuc) == id::MATH_CHAR;
1627        let nuc_char = if char_nucleus {
1628            self.fetch(nuc).map(|(nf, nc, _)| (nf, nc))
1629        } else {
1630            None
1631        };
1632        // As for top accents, the scripts of a character go inside.
1633        let sup = self.noad_field(q, Field::Sup);
1634        let sub = self.noad_field(q, Field::Sub);
1635        let x = if char_nucleus && (!sup.is_null() || !sub.is_null()) {
1636            let n = self.new_noad(noad_sub::ORD);
1637            let nk = self.take_field(q, Field::Nucleus);
1638            let a = self.take_field(q, Field::Sup);
1639            let b = self.take_field(q, Field::Sub);
1640            self.set_noad_field(n, Field::Nucleus, nk);
1641            self.set_noad_field(n, Field::Sup, a);
1642            self.set_noad_field(n, Field::Sub, b);
1643            let k = self.new_sub_list(true, n);
1644            self.set_noad_field(q, Field::Nucleus, k);
1645            self.clean_box(k, self.ts.math.style, list_sub::NUCLEUS)
1646        } else {
1647            self.clean_box(nuc, self.ts.math.style, list_sub::NUCLEUS)
1648        };
1649        let (bw, xh) = self
1650            .nodes
1651            .box_data(x)
1652            .map_or((0, 0), |b| (b.width, b.height));
1653        // The accent is centred on the character itself, not on the box
1654        // with its scripts (observed).
1655        let w = match nuc_char {
1656            Some((nf, nc)) => self.fonts.char_metrics(nf, nc).map_or(bw, |m| {
1657                if self.is_opentype_math_font(nf) {
1658                    m.width
1659                } else {
1660                    m.width + m.italic
1661                }
1662            }),
1663            None => bw,
1664        };
1665        // Switch to a wider accent if available, as for top accents.
1666        if !fixed {
1667            loop {
1668                let (tag, rem) = self.char_tag(f, c);
1669                if tag != 2 {
1670                    break;
1671                }
1672                match self.fonts.char_metrics(f, rem) {
1673                    Some(m) if m.width <= w => c = rem,
1674                    _ => break,
1675                }
1676            }
1677        }
1678        let saved = self.attr_set(bot_attr);
1679        let (y, assembled) = self.accent_char_box(f, c, w, fixed);
1680        self.attr_restore(saved);
1681        let yw = self.nodes.box_data(y).map_or(0, |b| b.width);
1682        let shift = if self.is_opentype_math_font(f) {
1683            let a = nuc_char
1684                .filter(|_| self.noad_field(q, Field::Sup).is_null())
1685                .and_then(|(nf, nc)| self.math_char(nf, nc).and_then(|m| m.top_accent))
1686                .unwrap_or(half(w));
1687            let b = self
1688                .math_char(f, c)
1689                .and_then(|m| m.top_accent)
1690                .filter(|_| !assembled)
1691                .unwrap_or(half(yw));
1692            a - b
1693        } else if nuc_char.is_some() {
1694            half(w - yw)
1695        } else {
1696            w / 2 - yw / 2 + 1
1697        };
1698        if let Some(b) = self.nodes.box_data_mut(y) {
1699            b.shift = shift;
1700            b.width = 0;
1701        }
1702        self.nodes.link(x, y);
1703        // Observed: the nucleus and the accent are packed naturally and
1704        // the box is lowered so that the nucleus keeps its baseline.
1705        let v = self.vpack(x, PackSpec::NATURAL);
1706        self.nodes.set_subtype(v, list_sub::ACCENT);
1707        if let Some(b) = self.nodes.box_data_mut(v) {
1708            b.shift = b.height - xh;
1709            b.width = bw;
1710        }
1711        self.set_nucleus_box(q, v);
1712    }
1713
1714    /// Builds a generalized fraction (`make_fraction`).
1715    fn make_fraction(&mut self, q: NodeRef) {
1716        let (mut thickness, num, denom, left, right, options) = match self.nodes.data(q) {
1717            NodeData::Fraction(f) => (f.thickness, f.num, f.denom, f.left, f.right, f.options),
1718            _ => return,
1719        };
1720        if thickness == DEFAULT_CODE {
1721            thickness = self.math_par(mp::FRACTION_RULE);
1722        }
1723        let cs = self.ts.math.style;
1724        let mut x = self.clean_box(num, num_style(cs), list_sub::NUMERATOR);
1725        let mut z = self.clean_box(denom, denom_style(cs), list_sub::DENOMINATOR);
1726        let (xw, zw) = (
1727            self.nodes.box_data(x).map_or(0, |b| b.width),
1728            self.nodes.box_data(z).map_or(0, |b| b.width),
1729        );
1730        if xw < zw {
1731            x = self.rebox(x, zw);
1732        } else {
1733            z = self.rebox(z, xw);
1734        }
1735        let (mut shift_up, mut shift_down) = if thickness == 0 {
1736            (
1737                self.math_par(mp::STACK_NUM_UP),
1738                self.math_par(mp::STACK_DENOM_DOWN),
1739            )
1740        } else {
1741            (
1742                self.math_par(mp::FRACTION_NUM_UP),
1743                self.math_par(mp::FRACTION_DENOM_DOWN),
1744            )
1745        };
1746        let (xh, xd) = self
1747            .nodes
1748            .box_data(x)
1749            .map_or((0, 0), |b| (b.height, b.depth));
1750        let (zh, zd) = self
1751            .nodes
1752            .box_data(z)
1753            .map_or((0, 0), |b| (b.height, b.depth));
1754        let axis = self.math_par(mp::AXIS);
1755        let delta;
1756        if thickness == 0 {
1757            let clr = self.math_par(mp::STACK_VGAP);
1758            delta = half(clr - ((shift_up - xd) - (zh - shift_down)));
1759            if delta > 0 {
1760                shift_up += delta;
1761                shift_down += delta;
1762            }
1763        } else {
1764            let exact = options & noad_opt::EXACT == noad_opt::EXACT;
1765            let (clr_n, clr_d) = if exact {
1766                (0, 0)
1767            } else if thickness == self.math_par(mp::FRACTION_RULE) {
1768                (
1769                    self.math_par(mp::FRACTION_NUM_VGAP),
1770                    self.math_par(mp::FRACTION_DENOM_VGAP),
1771                )
1772            } else if cs < 2 {
1773                (3 * thickness, 3 * thickness)
1774            } else {
1775                (thickness, thickness)
1776            };
1777            delta = half(thickness);
1778            let delta1 = clr_n - ((shift_up - xd) - (axis + delta));
1779            let delta2 = clr_d - ((axis - delta) - (zh - shift_down));
1780            if delta1 > 0 {
1781                shift_up += delta1;
1782            }
1783            if delta2 > 0 {
1784                shift_down += delta2;
1785            }
1786        }
1787        // Construct a vlist box for the fraction.
1788        let v = self.new_null_box_node(false);
1789        self.nodes.set_subtype(v, list_sub::FRACTION);
1790        let width = self.nodes.box_data(x).map_or(0, |b| b.width);
1791        if let Some(b) = self.nodes.box_data_mut(v) {
1792            b.height = shift_up + xh;
1793            b.depth = zd + shift_down;
1794            b.width = width;
1795        }
1796        let p;
1797        if thickness == 0 {
1798            p = self.new_kern_node((shift_up - xd) - (zh - shift_down), kern_sub::FONT);
1799            self.nodes.link(p, z);
1800        } else {
1801            let y = self.fraction_rule(thickness);
1802            let p2 = self.new_kern_node((axis - delta) - (zh - shift_down), kern_sub::FONT);
1803            self.nodes.link(y, p2);
1804            self.nodes.link(p2, z);
1805            p = self.new_kern_node((shift_up - xd) - (axis + delta), kern_sub::FONT);
1806            self.nodes.link(p, y);
1807        }
1808        self.nodes.link(x, p);
1809        self.nodes.set_list_ptr(v, x);
1810        // Put the fraction into a box with its delimiters.
1811        let dsize = self.math_par(mp::FRACTION_DEL_SIZE);
1812        let size = self.ts.math.size;
1813        // A null delimiter given explicitly (`\atopwithdelims..`) stays
1814        // on the baseline; a missing one (`\atop`) is centered on the axis
1815        // (observed).
1816        let lx = self.var_delimiter(left, size, dsize, left.is_null());
1817        self.nodes.link(lx, v);
1818        let rz = self.var_delimiter(right, size, dsize, right.is_null());
1819        self.nodes.link(v, rz);
1820        let h = self.hpack_quiet(lx);
1821        self.nodes.set_subtype(h, list_sub::FRACTION);
1822        self.set_new_hlist(q, h);
1823    }
1824
1825    /// Builds an operator (`make_op`); returns the italic correction.
1826    fn make_op(&mut self, q: NodeRef) -> i32 {
1827        let cs = self.ts.math.style;
1828        if self.nodes.subtype(q) == noad_sub::OP_DISPLAY_LIMITS && cs < 2 {
1829            self.nodes.set_subtype(q, noad_sub::OP_LIMITS);
1830        }
1831        let limits = self.nodes.subtype(q) == noad_sub::OP_LIMITS;
1832        let nuc = self.noad_field(q, Field::Nucleus);
1833        let mut delta = 0;
1834        let mut opentype = false;
1835        if !nuc.is_null() && self.nodes.id(nuc) == id::MATH_CHAR {
1836            if let Some((f, c, m)) = self.fetch(nuc) {
1837                let mut m = m;
1838                if cs < 2 && self.is_opentype_math_font(f) {
1839                    // Observed (TeX Gyre Termes Math): the first variant
1840                    // at least DisplayOperatorMinHeight high (height plus
1841                    // depth), else the largest.
1842                    let target = self.math_par(mp::OPERATOR_SIZE);
1843                    let mut c = c;
1844                    let mut guard = 0;
1845                    while m.height + m.depth < target && guard < 64 {
1846                        guard += 1;
1847                        let (tag, rem) = self.char_tag(f, c);
1848                        let Some(m2) = self
1849                            .fonts
1850                            .char_metrics(f, rem)
1851                            .copied()
1852                            .filter(|_| tag == 2)
1853                        else {
1854                            break;
1855                        };
1856                        m = m2;
1857                        c = rem;
1858                    }
1859                    if let NodeData::MathChar { chr, .. } = self.nodes.data_mut(nuc) {
1860                        *chr = c;
1861                    }
1862                } else if cs < 2 {
1863                    let (tag, rem) = self.char_tag(f, c);
1864                    if tag == 2
1865                        && let Some(m2) = self.fonts.char_metrics(f, rem).copied()
1866                    {
1867                        m = m2;
1868                        if let NodeData::MathChar { chr, .. } = self.nodes.data_mut(nuc) {
1869                            *chr = rem;
1870                        }
1871                    }
1872                }
1873                delta = m.italic;
1874                opentype = self.is_opentype_math_font(f);
1875            }
1876            let saved = std::mem::replace(&mut self.math_op_nucleus, true);
1877            let x = self.clean_box(nuc, cs, list_sub::NUCLEUS);
1878            self.math_op_nucleus = saved;
1879            let has_sub = !self.noad_field(q, Field::Sub).is_null();
1880            let no_limits = self.nodes.subtype(q) == noad_sub::OP_NO_LIMITS;
1881            let axis = self.math_par(mp::AXIS);
1882            let math_124 = self.math_124;
1883            if let Some(b) = self.nodes.box_data_mut(x) {
1884                if has_sub && !limits && !opentype {
1885                    b.width -= delta;
1886                }
1887                if opentype && math_124 {
1888                    // LuaTeX 1.24: the box includes the italic correction,
1889                    // except with \nolimits, and loses it (again) before a
1890                    // subscript that is no limit (observed).
1891                    b.width += delta;
1892                    if no_limits {
1893                        b.width -= delta;
1894                    }
1895                    if has_sub && !limits {
1896                        b.width -= delta;
1897                    }
1898                }
1899                b.shift = half(b.height - b.depth) - axis;
1900            }
1901            self.set_nucleus_box(q, x);
1902            // An OpenType operator's box has no italic correction (LuaTeX
1903            // 1.22); with \nolimits the subscript moves left by it (a font
1904            // kern; in 1.24 the narrower box) and a lone superscript is
1905            // not moved.
1906            if opentype && !limits {
1907                if !has_sub {
1908                    delta = 0;
1909                } else if !math_124 {
1910                    self.ts.math.op_kern = -delta;
1911                }
1912            }
1913        }
1914        if limits {
1915            // Construct a box with limits above and below it.
1916            let supk = self.noad_field(q, Field::Sup);
1917            let subk = self.noad_field(q, Field::Sub);
1918            let x = self.clean_box(supk, sup_style(cs), list_sub::SUP);
1919            let nk = self.noad_field(q, Field::Nucleus);
1920            let y = self.clean_box(nk, cs, list_sub::NUCLEUS);
1921            let z = self.clean_box(subk, sub_style(cs), list_sub::SUB);
1922            let v = self.new_null_box_node(false);
1923            self.nodes.set_subtype(v, list_sub::LIMITS);
1924            let wy = self.nodes.box_data(y).map_or(0, |b| b.width);
1925            let wx = self.nodes.box_data(x).map_or(0, |b| b.width);
1926            let wz = self.nodes.box_data(z).map_or(0, |b| b.width);
1927            let wv = wy.max(wx).max(wz);
1928            let x = self.rebox(x, wv);
1929            let y = self.rebox(y, wv);
1930            let z = self.rebox(z, wv);
1931            if let Some(b) = self.nodes.box_data_mut(x) {
1932                b.shift = half(delta);
1933            }
1934            if let Some(b) = self.nodes.box_data_mut(z) {
1935                b.shift = -half(delta);
1936            }
1937            let (yh, yd) = self
1938                .nodes
1939                .box_data(y)
1940                .map_or((0, 0), |b| (b.height, b.depth));
1941            let (mut vh, mut vd) = (yh, yd);
1942            let kern = self.math_par(mp::LIMIT_ABOVE_KERN);
1943            let kern_below = self.math_par(mp::LIMIT_BELOW_KERN);
1944            let head;
1945            if supk.is_null() {
1946                self.nodes.free(x);
1947                head = y;
1948            } else {
1949                let (xh, xd) = self
1950                    .nodes
1951                    .box_data(x)
1952                    .map_or((0, 0), |b| (b.height, b.depth));
1953                let mut shift_up = self.math_par(mp::LIMIT_ABOVE_BGAP) - xd;
1954                let g1 = self.math_par(mp::LIMIT_ABOVE_VGAP);
1955                if shift_up < g1 {
1956                    shift_up = g1;
1957                }
1958                let p = self.new_kern_node(shift_up, kern_sub::FONT);
1959                self.nodes.link(p, y);
1960                self.nodes.link(x, p);
1961                let p = self.new_kern_node(kern, kern_sub::FONT);
1962                self.nodes.link(p, x);
1963                head = p;
1964                vh += kern + xh + xd + shift_up;
1965            }
1966            if subk.is_null() {
1967                self.nodes.free(z);
1968            } else {
1969                let (zh, zd) = self
1970                    .nodes
1971                    .box_data(z)
1972                    .map_or((0, 0), |b| (b.height, b.depth));
1973                let mut shift_down = self.math_par(mp::LIMIT_BELOW_BGAP) - zh;
1974                let g2 = self.math_par(mp::LIMIT_BELOW_VGAP);
1975                if shift_down < g2 {
1976                    shift_down = g2;
1977                }
1978                let p = self.new_kern_node(shift_down, kern_sub::FONT);
1979                self.nodes.link(y, p);
1980                self.nodes.link(p, z);
1981                let p = self.new_kern_node(kern_below, kern_sub::FONT);
1982                self.nodes.link(z, p);
1983                vd += kern_below + zh + zd + shift_down;
1984            }
1985            if let Some(b) = self.nodes.box_data_mut(v) {
1986                b.width = wv;
1987                b.height = vh;
1988                b.depth = vd;
1989                b.head = head;
1990            }
1991            // The scripts are now part of the box.
1992            let a = self.take_field(q, Field::Sup);
1993            let b = self.take_field(q, Field::Sub);
1994            self.nodes.flush_list(a);
1995            self.nodes.flush_list(b);
1996            self.set_new_hlist(q, v);
1997        }
1998        delta
1999    }
2000
2001    /// Ligatures and kerns between ordinary characters (`make_ord`).
2002    fn make_ord(&mut self, q: NodeRef) {
2003        'restart: loop {
2004            if !self.noad_field(q, Field::Sub).is_null()
2005                || !self.noad_field(q, Field::Sup).is_null()
2006            {
2007                return;
2008            }
2009            let nuc = self.noad_field(q, Field::Nucleus);
2010            if nuc.is_null() || self.nodes.id(nuc) != id::MATH_CHAR {
2011                return;
2012            }
2013            let p = self.nodes.next(q);
2014            if p.is_null()
2015                || self.nodes.id(p) != id::NOAD
2016                || !matches!(
2017                    self.nodes.subtype(p),
2018                    noad_sub::ORD
2019                        | noad_sub::OP_DISPLAY_LIMITS
2020                        | noad_sub::OP_LIMITS
2021                        | noad_sub::OP_NO_LIMITS
2022                        | noad_sub::BIN
2023                        | noad_sub::REL
2024                        | noad_sub::OPEN
2025                        | noad_sub::CLOSE
2026                        | noad_sub::PUNCT
2027                )
2028            {
2029                return;
2030            }
2031            let pn = self.noad_field(p, Field::Nucleus);
2032            if pn.is_null() || self.nodes.id(pn) != id::MATH_CHAR {
2033                return;
2034            }
2035            let (qfam, pfam, pchr) = match (self.nodes.data(nuc), self.nodes.data(pn)) {
2036                (NodeData::MathChar { fam: a, .. }, NodeData::MathChar { fam: b, chr }) => {
2037                    (*a, *b, *chr)
2038                }
2039                _ => return,
2040            };
2041            if qfam != pfam {
2042                return;
2043            }
2044            self.nodes.get_mut(nuc).id = id::MATH_TEXT_CHAR;
2045            let Some((f, c, _)) = self.fetch(nuc) else {
2046                return;
2047            };
2048            let Some(op) = self
2049                .lig_kern_step(f, c, pchr)
2050                .or_else(|| self.base_mode_kern(f, c, pchr))
2051            else {
2052                return;
2053            };
2054            match op {
2055                LigOp::Kern(k) => {
2056                    let kn = self.new_kern_node(k, kern_sub::FONT);
2057                    let n = self.nodes.next(q);
2058                    self.nodes.link(kn, n);
2059                    self.nodes.link(q, kn);
2060                    return;
2061                }
2062                LigOp::Lig { op, chr } => {
2063                    match op {
2064                        1 | 5 => self.set_char(nuc, chr),
2065                        2 | 6 => self.set_char(pn, chr),
2066                        3 | 7 | 11 => {
2067                            let r = self.new_noad(noad_sub::ORD);
2068                            let k = self.new_math_char(qfam, chr);
2069                            if op >= 11 {
2070                                self.nodes.get_mut(k).id = id::MATH_TEXT_CHAR;
2071                            }
2072                            self.set_noad_field(r, Field::Nucleus, k);
2073                            self.nodes.link(q, r);
2074                            self.nodes.link(r, p);
2075                        }
2076                        _ => {
2077                            let n = self.nodes.next(p);
2078                            self.nodes.link(q, n);
2079                            if n.is_null() {
2080                                self.nodes.set_next(q, NodeRef::NULL);
2081                            }
2082                            self.set_char(nuc, chr);
2083                            let a = self.take_field(p, Field::Sub);
2084                            let b = self.take_field(p, Field::Sup);
2085                            self.set_noad_field(q, Field::Sub, a);
2086                            self.set_noad_field(q, Field::Sup, b);
2087                            self.nodes.set_next(p, NodeRef::NULL);
2088                            self.nodes.flush_list(p);
2089                        }
2090                    }
2091                    if op > 3 {
2092                        return;
2093                    }
2094                    self.nodes.get_mut(nuc).id = id::MATH_CHAR;
2095                    continue 'restart;
2096                }
2097            }
2098        }
2099    }
2100
2101    fn set_char(&mut self, k: NodeRef, c: u32) {
2102        if let NodeData::MathChar { chr, .. } = self.nodes.data_mut(k) {
2103            *chr = c;
2104        }
2105    }
2106
2107    /// Converts the nucleus of `q` to an hlist and attaches the scripts
2108    /// (§754–§755).
2109    fn convert_nucleus(&mut self, q: NodeRef, delta: i32) {
2110        let mut delta = delta;
2111        let nuc = self.noad_field(q, Field::Nucleus);
2112        let kid = if nuc.is_null() { 0 } else { self.nodes.id(nuc) };
2113        let has_sub = !self.noad_field(q, Field::Sub).is_null();
2114        let p = match kid {
2115            id::MATH_CHAR | id::MATH_TEXT_CHAR => match self.fetch(nuc) {
2116                Some((f, c, m)) => {
2117                    delta = m.italic;
2118                    // The glyph and its italic correction take the
2119                    // character's attributes (observed).
2120                    let saved = self.attr_from(nuc);
2121                    let p = self.new_math_glyph(f, c);
2122                    let has_sup = !self.noad_field(q, Field::Sup).is_null();
2123                    if self.is_opentype_math_font(f) {
2124                        // Observed: an OpenType math character gets its
2125                        // italic correction before a character of the same
2126                        // family (a "text char"), before a superscript and
2127                        // in its scripts, but not otherwise. LuaTeX 1.24
2128                        // adds it in all cases but an operator's nucleus.
2129                        if kid == id::MATH_CHAR
2130                            && !has_sup
2131                            && !has_sub
2132                            && (!self.math_124 || self.math_op_nucleus)
2133                        {
2134                            delta = 0;
2135                        }
2136                    } else if kid == id::MATH_TEXT_CHAR
2137                        && self.font(f).params.get(2).copied().unwrap_or(0) != 0
2138                        && !self.math_124
2139                    {
2140                        // A text character in a text font (\fontdimen2 not
2141                        // zero) has no italic correction (§752), except in
2142                        // LuaTeX 1.24.
2143                        delta = 0;
2144                    }
2145                    // Observed: an OpenType character's italic correction
2146                    // before another character takes the noad's attributes.
2147                    if kid == id::MATH_TEXT_CHAR && self.is_opentype_math_font(f) {
2148                        self.attr_restore(saved.clone());
2149                    }
2150                    if !has_sub && delta != 0 {
2151                        let k = self.new_kern_node(delta, kern_sub::ITALIC);
2152                        self.nodes.link(p, k);
2153                        delta = 0;
2154                    }
2155                    self.attr_restore(saved);
2156                    p
2157                }
2158                None => NodeRef::NULL,
2159            },
2160            id::SUB_BOX => {
2161                let b = self.nodes.list_ptr_kernel(nuc);
2162                self.nodes.set_kernel_list(nuc, NodeRef::NULL);
2163                let k = std::mem::take(&mut self.ts.math.op_kern);
2164                if k != 0 && !b.is_null() {
2165                    let kn = self.new_kern_node(k, kern_sub::FONT);
2166                    self.nodes.link(b, kn);
2167                }
2168                b
2169            }
2170            id::SUB_MLIST => {
2171                let l = self.nodes.list_ptr_kernel(nuc);
2172                self.nodes.set_kernel_list(nuc, NodeRef::NULL);
2173                let hl = self.clean_box_mlist(l, self.ts.math.style);
2174                // The box takes the sub-list's attributes (observed).
2175                let saved = self.attr_from(nuc);
2176                let b = self.hpack_quiet(hl);
2177                self.attr_restore(saved);
2178                b
2179            }
2180            _ => {
2181                // LuaTeX gives an empty nucleus with scripts an empty box.
2182                if has_sub || !self.noad_field(q, Field::Sup).is_null() {
2183                    self.new_null_box_node(true)
2184                } else {
2185                    NodeRef::NULL
2186                }
2187            }
2188        };
2189        self.set_new_hlist(q, p);
2190        let has_sup = !self.noad_field(q, Field::Sup).is_null();
2191        if !has_sub && !has_sup {
2192            return;
2193        }
2194        self.make_scripts(q, delta);
2195    }
2196
2197    /// Attaches subscript and superscript (`make_scripts`).
2198    fn make_scripts(&mut self, q: NodeRef, delta: i32) {
2199        let p = self.get_new_hlist(q);
2200        let cs = self.ts.math.style;
2201        let (mut shift_up, mut shift_down);
2202        if !p.is_null() && self.nodes.is_glyph(p) {
2203            shift_up = 0;
2204            shift_down = 0;
2205        } else {
2206            let (h, d) = self.list_height_depth(p);
2207            shift_up = h - self.math_par(mp::SUP_SHIFT_DROP);
2208            shift_down = d + self.math_par(mp::SUB_SHIFT_DROP);
2209        }
2210        let opts = self.nodes.noad(q).map_or(0, |n| n.options);
2211        let supk = self.take_field(q, Field::Sup);
2212        let subk = self.take_field(q, Field::Sub);
2213        if !p.is_null() && self.nodes.is_glyph(p) {
2214            // LuaTeX looks at character scripts in the current size (for
2215            // OpenType math kerns), which reports undefined families.
2216            for k in [supk, subk] {
2217                if !k.is_null() && self.nodes.id(k) == id::MATH_CHAR {
2218                    let _ = self.fetch(k);
2219                }
2220            }
2221        }
2222        let script_space = self.math_par(mp::SPACE_AFTER_SCRIPT);
2223        let x;
2224        if supk.is_null() {
2225            // A subscript without superscript.
2226            let st = if opts & noad_opt::NO_SUB_SCRIPT == noad_opt::NO_SUB_SCRIPT {
2227                cs
2228            } else {
2229                sub_style(cs)
2230            };
2231            x = self.clean_box(subk, st, list_sub::SUB);
2232            self.nodes.flush_list(subk);
2233            let xh = self.nodes.box_data(x).map_or(0, |b| b.height);
2234            if let Some(b) = self.nodes.box_data_mut(x) {
2235                b.width += script_space;
2236            }
2237            let s1 = self.math_par(mp::SUB_SHIFT_DOWN);
2238            if shift_down < s1 {
2239                shift_down = s1;
2240            }
2241            let clr = xh - self.math_par(mp::SUB_TOP_MAX);
2242            if shift_down < clr {
2243                shift_down = clr;
2244            }
2245            if let Some(b) = self.nodes.box_data_mut(x) {
2246                b.shift = shift_down;
2247            }
2248        } else {
2249            let st = if opts & noad_opt::NO_SUPER_SCRIPT == noad_opt::NO_SUPER_SCRIPT {
2250                cs
2251            } else {
2252                sup_style(cs)
2253            };
2254            let sx = self.clean_box(supk, st, list_sub::SUP);
2255            self.nodes.flush_list(supk);
2256            let xd = self.nodes.box_data(sx).map_or(0, |b| b.depth);
2257            if let Some(b) = self.nodes.box_data_mut(sx) {
2258                b.width += script_space;
2259            }
2260            let clr = self.math_par(mp::SUP_SHIFT_UP);
2261            if shift_up < clr {
2262                shift_up = clr;
2263            }
2264            let clr = xd + self.math_par(mp::SUP_BOTTOM_MIN);
2265            if shift_up < clr {
2266                shift_up = clr;
2267            }
2268            if subk.is_null() {
2269                if let Some(b) = self.nodes.box_data_mut(sx) {
2270                    b.shift = -shift_up;
2271                }
2272                x = sx;
2273            } else {
2274                let st = if opts & noad_opt::NO_SUB_SCRIPT == noad_opt::NO_SUB_SCRIPT {
2275                    cs
2276                } else {
2277                    sub_style(cs)
2278                };
2279                let y = self.clean_box(subk, st, list_sub::SUB);
2280                self.nodes.flush_list(subk);
2281                let yh = self.nodes.box_data(y).map_or(0, |b| b.height);
2282                if let Some(b) = self.nodes.box_data_mut(y) {
2283                    b.width += script_space;
2284                }
2285                let s2 = self.math_par(mp::SUB_SUP_SHIFT_DOWN);
2286                if shift_down < s2 {
2287                    shift_down = s2;
2288                }
2289                let mut clr =
2290                    self.math_par(mp::SUB_SUP_VGAP) - ((shift_up - xd) - (yh - shift_down));
2291                if clr > 0 {
2292                    shift_down += clr;
2293                    clr = self.math_par(mp::SUP_SUB_BOTTOM_MAX) - (shift_up - xd);
2294                    if clr > 0 {
2295                        shift_up += clr;
2296                        shift_down -= clr;
2297                    }
2298                }
2299                if let Some(b) = self.nodes.box_data_mut(sx) {
2300                    b.shift = delta;
2301                }
2302                let p = self.new_kern_node((shift_up - xd) - (yh - shift_down), kern_sub::FONT);
2303                self.nodes.link(sx, p);
2304                self.nodes.link(p, y);
2305                x = self.vpack(sx, PackSpec::NATURAL);
2306                self.nodes.set_subtype(x, list_sub::SCRIPTS);
2307                if let Some(b) = self.nodes.box_data_mut(x) {
2308                    b.shift = shift_down;
2309                }
2310            }
2311        }
2312        let nh = self.get_new_hlist(q);
2313        if nh.is_null() {
2314            self.set_new_hlist(q, x);
2315        } else {
2316            let t = self.nodes.tail_of(nh);
2317            self.nodes.link(t, x);
2318        }
2319    }
2320
2321    /// The box of `\Uvextensible`, as observed in LuaTeX: without a
2322    /// height and depth the delimiter's smallest variant, unshifted;
2323    /// otherwise the variant for their sum, centered on the baseline (on
2324    /// the axis with `axis`), in a box as high and deep as asked (raised
2325    /// by the axis with `axis`), or as the delimiter with `exact`.
2326    fn extensible_fence(
2327        &mut self,
2328        delim: NodeRef,
2329        height: i32,
2330        depth: i32,
2331        options: u32,
2332        axis: i32,
2333    ) -> NodeRef {
2334        let size = self.ts.math.size;
2335        if height == 0 && depth == 0 {
2336            let b = self.var_delimiter(delim, size, 0, false);
2337            if let Some(bd) = self.nodes.box_data_mut(b) {
2338                bd.shift = 0;
2339            }
2340            return b;
2341        }
2342        let up = if options & noad_opt::AXIS == noad_opt::AXIS {
2343            axis
2344        } else {
2345            0
2346        };
2347        let b = self.var_delimiter(delim, size, height + depth, false);
2348        if let Some(bd) = self.nodes.box_data_mut(b) {
2349            bd.shift = half(bd.height - bd.depth) - up;
2350        }
2351        let w = self.hpack_quiet(b);
2352        if options & noad_opt::EXACT != noad_opt::EXACT
2353            && let Some(bd) = self.nodes.box_data_mut(w)
2354        {
2355            bd.height = height + up;
2356            bd.depth = depth - up;
2357        }
2358        w
2359    }
2360
2361    /// Builds the delimiter of a fence (`make_left_right`).
2362    fn make_left_right(&mut self, q: NodeRef, style: i32, max_d: i32, max_h: i32) -> NoadType {
2363        self.set_cur_style(style);
2364        let (delim, height, depth, options) = match self.nodes.data(q) {
2365            NodeData::Fence(f) => (f.delim, f.height, f.depth, f.options),
2366            _ => (NodeRef::NULL, 0, 0, 0),
2367        };
2368        let axis = self.math_par(mp::AXIS);
2369        if self.nodes.subtype(q) == fence_sub::NO {
2370            let b = self.extensible_fence(delim, height, depth, options, axis);
2371            self.set_new_hlist(q, b);
2372            return NoadType::Close;
2373        }
2374        let b = if height != 0 || depth != 0 {
2375            let total = height + depth;
2376            let b = self.var_delimiter(delim, self.ts.math.size, total, true);
2377            if options & noad_opt::NO_AXIS == noad_opt::NO_AXIS
2378                && let Some(bd) = self.nodes.box_data_mut(b)
2379            {
2380                bd.shift = 0;
2381            }
2382            b
2383        } else {
2384            let mut delta2 = max_d + axis;
2385            let mut delta1 = max_h + max_d - delta2;
2386            if delta2 > delta1 {
2387                delta1 = delta2;
2388            }
2389            let mut delta = (delta1 / 500) * self.eqtb.int(ip::DELIMITERFACTOR);
2390            delta2 = delta1 + delta1 - self.eqtb.dimen(dp::DELIMITERSHORTFALL);
2391            if delta < delta2 {
2392                delta = delta2;
2393            }
2394            self.var_delimiter(delim, self.ts.math.size, delta, false)
2395        };
2396        self.set_new_hlist(q, b);
2397        if self.nodes.subtype(q) == fence_sub::LEFT {
2398            NoadType::Open
2399        } else {
2400            NoadType::Close
2401        }
2402    }
2403
2404    // ----- the second pass ---------------------------------------------
2405
2406    fn second_pass(
2407        &mut self,
2408        mlist: NodeRef,
2409        style: i32,
2410        penalties: bool,
2411        max_h: i32,
2412        max_d: i32,
2413    ) -> NodeRef {
2414        let head = self.nodes.new_temp();
2415        let mut p = head;
2416        let mut q = mlist;
2417        let mut r_type: Option<NoadType> = None;
2418        self.set_cur_style(style);
2419        let binpen = self.eqtb.int(ip::BINOPPENALTY);
2420        let relpen = self.eqtb.int(ip::RELPENALTY);
2421        while !q.is_null() {
2422            let next = self.nodes.next(q);
2423            let Some(nt) = self.noad_type(q) else {
2424                if let NodeData::Style { style } = self.nodes.data(q) {
2425                    let s = i32::from(*style);
2426                    self.set_cur_style(s);
2427                    self.nodes.free(q);
2428                } else {
2429                    self.nodes.link(p, q);
2430                    self.nodes.set_next(q, NodeRef::NULL);
2431                    p = q;
2432                }
2433                q = next;
2434                continue;
2435            };
2436            let mut t = NoadType::Ord;
2437            let mut pen = 10000;
2438            match nt {
2439                NoadType::Op
2440                | NoadType::Open
2441                | NoadType::Close
2442                | NoadType::Punct
2443                | NoadType::Inner => t = nt,
2444                NoadType::Bin => {
2445                    t = NoadType::Bin;
2446                    pen = binpen;
2447                }
2448                NoadType::Rel => {
2449                    t = NoadType::Rel;
2450                    pen = relpen;
2451                }
2452                NoadType::Fraction => t = NoadType::Inner,
2453                NoadType::Left | NoadType::Right => {
2454                    let saved = self.attr_from(q);
2455                    t = self.make_left_right(q, style, max_d, max_h);
2456                    self.attr_restore(saved);
2457                }
2458                _ => {}
2459            }
2460            // Inter-element spacing.
2461            if let Some(rt) = r_type {
2462                let style = self.ts.math.style as u32;
2463                if let Some((g, sub)) = self.math_spacing(rt as u32, t as u32, style) {
2464                    let y = self.math_glue(g, self.ts.math.mu);
2465                    // Observed: the glue takes the attributes of the node
2466                    // before it.
2467                    let saved = self.attr_from(p);
2468                    let z = self.new_glue_node(y, sub);
2469                    self.attr_restore(saved);
2470                    self.nodes.link(p, z);
2471                    p = z;
2472                }
2473            }
2474            // Append the new_hlist entries and penalties.
2475            let nh = self.get_new_hlist(q);
2476            self.set_new_hlist(q, NodeRef::NULL);
2477            if !nh.is_null() {
2478                self.nodes.link(p, nh);
2479                p = self.nodes.tail_of(nh);
2480            }
2481            if penalties && !next.is_null() && pen < 10000 {
2482                let nid = self.nodes.id(next);
2483                let next_is_rel = self.noad_type(next) == Some(NoadType::Rel);
2484                if nid != id::PENALTY && !next_is_rel {
2485                    // Observed: the penalty takes the noad's attributes.
2486                    let saved = self.attr_from(q);
2487                    let z = self.new_penalty_node(pen);
2488                    self.attr_restore(saved);
2489                    self.nodes.set_subtype(z, 5);
2490                    self.nodes.link(p, z);
2491                    p = z;
2492                }
2493            }
2494            // e-TeX spaces the atom after a \middle as after an opening
2495            // delimiter; LuaTeX keeps the closing class on both sides
2496            // (observed: `\middle|\mathrel{=}` gets a thick space).
2497            r_type = Some(t);
2498            // delete_q:
2499            self.nodes.set_next(q, NodeRef::NULL);
2500            self.nodes.flush_list(q);
2501            q = next;
2502        }
2503        let first = self.nodes.next(head);
2504        self.nodes.free(head);
2505        if !first.is_null() {
2506            self.nodes.set_prev(first, NodeRef::NULL);
2507        }
2508        first
2509    }
2510}
2511
2512#[derive(Clone, Copy, PartialEq, Eq)]
2513enum Action {
2514    Convert,
2515    CheckDimensions,
2516    DoneWithNoad,
2517}