Skip to main content

retex_engine/
mlist.rs

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