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