Skip to main content

retex_engine/
nodeproc.rs

1//! LuaTeX's node processing before packaging and line breaking: the
2//! hyphenation, ligaturing and kerning passes (LuaTeX manual §5.5–§5.6).
3//!
4//! Characters are appended to lists as glyph nodes without any font
5//! processing. When an `\hbox` is packaged or a paragraph is broken into
6//! lines, LuaTeX first inserts discretionaries (explicit hyphens become
7//! "automatic" discretionaries; patterns insert "regular" ones), then
8//! builds ligatures, then inserts font kerns. Each step can be replaced by
9//! a Lua callback; without callbacks
10//! the built-in TeX-style processing below applies, using the font's
11//! lig/kern program.
12//!
13//! Observed behaviour reproduced here:
14//!
15//! * a single `\exhyphenchar` glyph immediately followed by another
16//!   glyph becomes an automatic discretionary (subtype 2) whose pre-break
17//!   and replacement texts are the hyphen, with `\exhyphenpenalty` as
18//!   current at processing time. Of a run of two or more hyphens (`--`,
19//!   `---`) only the last one becomes such a discretionary, and only when
20//!   the run follows a glyph with a non-zero hyphenation code and is
21//!   followed by a glyph, and at most once between two glues; a kern also
22//!   prevents it until the next glue. So `a--b` becomes `a-{-||-}b`, while
23//!   `16--19`, `1--a` and the second dash of `a--b--c` stay as they are
24//!   (observed with luatex);
25//! * ligatures form within the main list and within each list of a
26//!   discretionary; a glyph next to a discretionary moves into it when it
27//!   would form a ligature across it;
28//! * kerns between a glyph and a discretionary go into the discretionary's
29//!   lists (pre-break and replacement at the left, post-break and
30//!   replacement at the right);
31//! * boundary nodes are removed; glyphs lose their "character" subtype.
32
33use crate::Engine;
34use crate::fonts::LigOp;
35use crate::nodes::{GlyphData, NodeData, NodeRef, glyph_sub, id, kern_sub};
36use crate::primitives_gen::assign_int as ip;
37
38/// What a list is processed for (the callback group).
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum Context {
41    /// An `\hbox` (or local box) being packaged.
42    Hbox,
43    /// A paragraph before line breaking.
44    Paragraph,
45}
46
47/// One of the node processing passes.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum NodePass {
50    /// Hyphenation (`lang.hyphenate`).
51    Hyphenate,
52    /// Ligatures (`node.ligaturing`).
53    Ligaturing,
54    /// Font kerns (`node.kerning`).
55    Kerning,
56}
57
58impl Engine {
59    /// The hyphenation parameters of language `l`.
60    pub fn lang_params(&self, l: u32) -> retex_hyph::Settings {
61        self.language(l).map(|x| x.settings).unwrap_or_default()
62    }
63
64    /// A new glyph node in font `f` with the current language data
65    /// (LuaTeX's `new_char`).
66    pub fn new_char_node(&mut self, f: u32, c: u32) -> NodeRef {
67        let lang = self.cur_lang() as u16;
68        let left = self.eqtb.int(ip::LEFTHYPHENMIN).clamp(0, 63) as u8;
69        let right = self.eqtb.int(ip::RIGHTHYPHENMIN).clamp(0, 63) as u8;
70        let uchyph = self.eqtb.int(ip::UCHYPH) > 0;
71        self.new_node(
72            id::GLYPH,
73            glyph_sub::CHARACTER,
74            NodeData::Glyph(GlyphData {
75                chr: c,
76                font: f,
77                lang,
78                left,
79                right,
80                uchyph,
81                ..GlyphData::default()
82            }),
83        )
84    }
85
86    /// Runs the hyphenation, ligaturing and kerning passes over the list
87    /// starting at `first`; returns the new first node.
88    pub fn node_process_list(&mut self, first: NodeRef, ctx: Context) -> NodeRef {
89        if first.is_null() {
90            return first;
91        }
92        self.fonts.sync_dense();
93        let head = self.nodes.new_temp();
94        self.nodes.link(head, first);
95        let saved_par_temp = self.nodes.par_temp;
96        if ctx == Context::Paragraph {
97            self.nodes.par_temp = head;
98        }
99        // The hyphenate, ligaturing and kerning callbacks replace the
100        // built-in passes (a registered false skips them).
101        let host = self.lua.clone();
102        let pass = |e: &mut Engine, cb: &str| match &host {
103            Some(h) => {
104                if e.figures.recording() && h.has_callback(cb) {
105                    e.figure_callback(cb);
106                }
107                h.node_pass(e, cb, head)
108            }
109            None => false,
110        };
111        if !pass(self, "hyphenate") {
112            self.hyphenate_list(head);
113        }
114        if !pass(self, "ligaturing") {
115            self.ligature_list(head, true);
116        }
117        if !pass(self, "kerning") {
118            self.kern_list(head);
119        }
120        self.nodes.par_temp = saved_par_temp;
121        let first = self.nodes.next(head);
122        if !first.is_null() {
123            self.nodes.set_prev(first, NodeRef::NULL);
124        }
125        self.nodes.free(head);
126        first
127    }
128
129    /// Runs one built-in pass (`node.ligaturing`, `node.kerning`,
130    /// `lang.hyphenate`) over the list `head`, which may start with a
131    /// temporary node (as in the callbacks); returns the new head and the
132    /// tail.
133    pub fn lua_node_pass(&mut self, pass: NodePass, head: NodeRef) -> (NodeRef, NodeRef) {
134        if head.is_null() {
135            return (head, head);
136        }
137        self.fonts.sync_dense();
138        let temp = self.nodes.id(head) == id::TEMP;
139        let h = if temp {
140            head
141        } else {
142            let t = self.nodes.new_temp();
143            self.nodes.link(t, head);
144            t
145        };
146        match pass {
147            NodePass::Hyphenate => self.hyphenate_list(h),
148            NodePass::Ligaturing => self.ligature_list(h, true),
149            NodePass::Kerning => self.kern_list(h),
150        }
151        let first = if temp {
152            head
153        } else {
154            let f = self.nodes.next(h);
155            if !f.is_null() {
156                self.nodes.set_prev(f, NodeRef::NULL);
157            }
158            self.nodes.free(h);
159            f
160        };
161        let tail = self.nodes.tail_of(first);
162        (first, tail)
163    }
164
165    // ----- hyphenation ---------------------------------------------------
166
167    /// The built-in `hyphenate` step: explicit hyphens become automatic
168    /// discretionaries; patterns (through the [`crate::typeset::Hyphenator`])
169    /// insert regular ones.
170    fn hyphenate_list(&mut self, head: NodeRef) {
171        let ex = self.eqtb.int(ip::EXHYPHENCHAR);
172        let pen = self.eqtb.int(ip::EXHYPHENPENALTY);
173        if ex > 0 {
174            let ex = ex as u32;
175            let is_hyphen = |e: &Engine, n: NodeRef| e.nodes.glyph(n).is_some_and(|g| g.chr == ex);
176            // Whether a run of several hyphens may still become a
177            // discretionary before the next glue.
178            let mut armed = true;
179            let mut prev = head;
180            let mut p = self.nodes.next(head);
181            while !p.is_null() {
182                // Observed: hyphens in a formula stay as they are (a URL
183                // set as math, with apacite's `\mathbin{}\mathchar"2D`).
184                if self.nodes.id(p) == id::MATH && self.nodes.subtype(p) == 0 {
185                    let mut q = self.nodes.next(p);
186                    while !(q.is_null()
187                        || (self.nodes.id(q) == id::MATH && self.nodes.subtype(q) == 1))
188                    {
189                        q = self.nodes.next(q);
190                    }
191                    if q.is_null() {
192                        break;
193                    }
194                    prev = q;
195                    p = self.nodes.next(q);
196                    continue;
197                }
198                if !is_hyphen(self, p) {
199                    match self.nodes.id(p) {
200                        id::GLUE => armed = true,
201                        id::KERN => armed = false,
202                        _ => {}
203                    }
204                    prev = p;
205                    p = self.nodes.next(p);
206                    continue;
207                }
208                // Find the run of hyphens.
209                let mut last = p;
210                let mut len = 1;
211                let mut q = self.nodes.next(p);
212                while !q.is_null() && is_hyphen(self, q) {
213                    last = q;
214                    len += 1;
215                    q = self.nodes.next(q);
216                }
217                let followed = !q.is_null() && self.nodes.is_glyph(q);
218                let convert =
219                    followed && (len == 1 || (armed && prev != head && self.has_hj_code(prev)));
220                if !convert {
221                    prev = last;
222                    p = q;
223                    continue;
224                }
225                if len > 1 {
226                    armed = false;
227                }
228                // Replace the last hyphen by an automatic discretionary.
229                let before = if len == 1 {
230                    prev
231                } else {
232                    self.nodes.prev(last)
233                };
234                self.nodes.set_next(last, NodeRef::NULL);
235                let d = self.nodes.new_disc(2, pen);
236                if let Some(a) = self.nodes.attr(last).cloned() {
237                    self.nodes.set_attr(d, Some(a));
238                }
239                let pre = self.nodes.copy_list(last);
240                self.nodes.set_prev(last, NodeRef::NULL);
241                self.nodes.set_disc_lists(d, (pre, NodeRef::NULL, last));
242                self.nodes.link(before, d);
243                self.nodes.link(d, q);
244                prev = d;
245                p = q;
246            }
247        }
248        if !self.languages.is_empty() {
249            self.pattern_hyphenate(head);
250        }
251    }
252
253    /// Whether `p` is a glyph whose character has a non-zero hyphenation
254    /// code in its language.
255    fn has_hj_code(&self, p: NodeRef) -> bool {
256        let Some(g) = self.nodes.glyph(p) else {
257            return false;
258        };
259        let lang = u32::from(g.lang);
260        let c = g.chr;
261        let code = match self.language(lang) {
262            Some(l) if l.has_hj_codes() => l.hj_code(c),
263            _ => self.eqtb.lc.get(c).max(0) as u32,
264        };
265        code != 0
266    }
267
268    /// Inserts pattern discretionaries into the words of the list
269    /// (LuaTeX's hyphenation pass, using `retex-hyph`).
270    ///
271    /// Observed: a word is hyphenated only if what precedes it lets a word
272    /// start and what follows it lets a word end. The list start and glue
273    /// let words start; once the list has had a glyph, kerns, penalties,
274    /// discretionaries, marks and `\vadjust`s stop that until the next
275    /// glue; other glyphs, boxes, rules, whatsits, formulas (with all
276    /// they hold) and direction nodes leave it as it is. A word followed by a discretionary, a math
277    /// node or a mark is not hyphenated; nor is any word of a run of glyphs
278    /// (with the automatic discretionaries of explicit hyphens) that ends
279    /// at a math node, a mark or another discretionary, even one followed
280    /// by punctuation or by other words of the run (`AUROC($x$)`,
281    /// `word,word-word\mark{}`). With `\automatichyphenmode` > 0 automatic
282    /// discretionaries (from explicit hyphens) count as letting words
283    /// start and end. Otherwise the automatic discretionary of a single
284    /// explicit hyphen lets words start again, but not the word attached
285    /// to it (`ab-online/on-line`); after `--` none does.
286    fn pattern_hyphenate(&mut self, head: NodeRef) {
287        let hpen = self.eqtb.int(ip::HYPHENPENALTY);
288        let auto_ok = self.eqtb.int(ip::AUTOMATICHYPHENMODE) != 0;
289        let mut run: Vec<NodeRef> = Vec::new();
290        let mut word: Vec<NodeRef> = Vec::new();
291        let mut codes: Vec<u32> = Vec::new();
292        let mut pos: Vec<usize> = Vec::new();
293        let mut can_start = true;
294        let mut seen_glyph = false;
295        // The run starts right after the automatic discretionary of a
296        // single explicit hyphen: its first word is not hyphenated.
297        let mut skip_first = false;
298        // Whether the current unit (runs joined by automatic
299        // discretionaries) ends where its words cannot be hyphenated.
300        let mut unit_kill: Option<bool> = None;
301        let mut p = self.nodes.next(head);
302        while !p.is_null() {
303            if self.nodes.is_glyph(p) && self.nodes.subtype(p) & glyph_sub::CHARACTER != 0 {
304                // A run of character glyphs.
305                run.clear();
306                let mut q = p;
307                while !q.is_null()
308                    && self.nodes.is_glyph(q)
309                    && self.nodes.subtype(q) & glyph_sub::CHARACTER != 0
310                {
311                    run.push(q);
312                    q = self.nodes.next(q);
313                }
314                let end_ok = q.is_null()
315                    || match self.nodes.id(q) {
316                        id::DISC => auto_ok && self.nodes.subtype(q) == 2,
317                        id::MATH | id::MARK => false,
318                        _ => true,
319                    };
320                let kill = *unit_kill.get_or_insert_with(|| self.unit_ends_badly(q));
321                let bounds = (can_start && !kill, end_ok, skip_first);
322                self.hyphenate_run(&run, hpen, bounds, &mut word, &mut codes, &mut pos);
323                seen_glyph = true;
324                skip_first = false;
325                p = q;
326                continue;
327            }
328            if !(self.nodes.id(p) == id::DISC && self.nodes.subtype(p) == 2) {
329                unit_kill = None;
330            }
331            // Observed: a formula's contents leave the state as it is
332            // (`$T-$dependencies` is hyphenated after the kern and the
333            // glyphs of the formula).
334            if self.nodes.id(p) == id::MATH && self.nodes.subtype(p) == 0 {
335                let mut q = self.nodes.next(p);
336                while !(q.is_null() || (self.nodes.id(q) == id::MATH && self.nodes.subtype(q) == 1))
337                {
338                    q = self.nodes.next(q);
339                }
340                p = if q.is_null() { q } else { self.nodes.next(q) };
341                continue;
342            }
343            match self.nodes.id(p) {
344                id::GLUE => can_start = true,
345                id::KERN | id::PENALTY | id::MARK | id::ADJUST => can_start &= !seen_glyph,
346                id::DISC if auto_ok && self.nodes.subtype(p) == 2 => can_start = true,
347                id::DISC if self.nodes.subtype(p) == 2 && self.single_hyphen_disc(p) => {
348                    can_start = true;
349                    skip_first = true;
350                }
351                id::DISC => can_start &= !seen_glyph,
352                _ => {}
353            }
354            p = self.nodes.next(p);
355        }
356    }
357
358    /// Whether automatic discretionary `d` comes from a single explicit
359    /// hyphen: after a glyph other than the hyphen character.
360    fn single_hyphen_disc(&self, d: NodeRef) -> bool {
361        let ex = self.eqtb.int(ip::EXHYPHENCHAR);
362        let prev = self.nodes.prev(d);
363        !prev.is_null()
364            && self
365                .nodes
366                .glyph(prev)
367                .is_some_and(|g| ex <= 0 || g.chr != ex as u32)
368    }
369
370    /// Whether the unit of glyph runs and automatic discretionaries that
371    /// goes on at `q` (the node after a run) ends at a math node, a mark or
372    /// a discretionary other than an automatic one.
373    fn unit_ends_badly(&self, mut q: NodeRef) -> bool {
374        loop {
375            if q.is_null() {
376                return false;
377            }
378            match self.nodes.id(q) {
379                id::DISC if self.nodes.subtype(q) == 2 => {}
380                id::DISC | id::MATH | id::MARK => return true,
381                _ if self.nodes.is_glyph(q)
382                    && self.nodes.subtype(q) & glyph_sub::CHARACTER != 0 => {}
383                _ => return false,
384            }
385            q = self.nodes.next(q);
386        }
387    }
388
389    /// Hyphenates the words in a run of character glyphs; `bounds` tells
390    /// whether words may start in the run and end at its end.
391    fn hyphenate_run(
392        &mut self,
393        run: &[NodeRef],
394        hpen: i32,
395        bounds: (bool, bool, bool),
396        word: &mut Vec<NodeRef>,
397        codes: &mut Vec<u32>,
398        pos: &mut Vec<usize>,
399    ) {
400        if !bounds.0 {
401            return;
402        }
403        let mut i = 0;
404        while i < run.len() {
405            // A word: glyphs of one language with non-zero hj codes.
406            let Some(g0) = self.nodes.glyph(run[i]).cloned() else {
407                i += 1;
408                continue;
409            };
410            let lang = u32::from(g0.lang);
411            word.clear();
412            codes.clear();
413            // Observed: a word next to or with an explicit hyphen that did
414            // not become a discretionary (`--word` after a space, `word--`;
415            // LaTeX gives the hyphen an hj code) is not hyphenated.
416            let ex = self.eqtb.int(ip::EXHYPHENCHAR);
417            let mut with_hyphen = false;
418            while i < run.len() {
419                let Some(g) = self.nodes.glyph(run[i]) else {
420                    break;
421                };
422                if u32::from(g.lang) != lang {
423                    break;
424                }
425                let c = g.chr;
426                let code = match self.language(lang) {
427                    Some(l) if l.has_hj_codes() => l.hj_code(c),
428                    _ => self.eqtb.lc.get(c).max(0) as u32,
429                };
430                if code == 0 {
431                    if word.is_empty() {
432                        with_hyphen = ex > 0 && c == ex as u32;
433                        i += 1;
434                        continue;
435                    }
436                    with_hyphen |= ex > 0 && c == ex as u32;
437                    break;
438                }
439                with_hyphen |= ex > 0 && c == ex as u32;
440                word.push(run[i]);
441                codes.push(code);
442                i += 1;
443            }
444            if word.len() < 2 || with_hyphen || (bounds.2 && word.first() == run.first()) {
445                continue;
446            }
447            if !bounds.1 && word.last() == run.last() {
448                continue;
449            }
450            let Some(first) = self.nodes.glyph(word[0]).cloned() else {
451                continue;
452            };
453            if first.font == 0 || self.font(first.font).hyphen_char < 0 {
454                continue;
455            }
456            if !first.uchyph && codes[0] != first.chr {
457                continue;
458            }
459            let Some(language) = self.language(lang) else {
460                continue;
461            };
462            pos.clear();
463            let _ =
464                language.hyphenate_into(codes, u32::from(first.left), u32::from(first.right), pos);
465            if pos.is_empty() {
466                continue;
467            }
468            let settings = language.settings;
469            pos.sort_unstable();
470            pos.dedup();
471            for &k in pos.iter().rev() {
472                if k == 0 || k >= word.len() {
473                    continue;
474                }
475                let left = word[k - 1];
476                let right = word[k];
477                let f = self.nodes.glyph(left).map_or(0, |g| g.font);
478                let d = self.nodes.new_disc(3, hpen);
479                if let Some(a) = self.nodes.attr(left).cloned() {
480                    self.nodes.set_attr(d, Some(a));
481                }
482                let mk = |e: &mut Engine, c: i32| {
483                    if c > 0 {
484                        let mut g = e.nodes.glyph(left).cloned().unwrap_or_default();
485                        g.chr = c as u32;
486                        g.font = f;
487                        g.components = NodeRef::NULL;
488                        // Observed: with the attributes of the glyph
489                        // before the break, like the discretionary.
490                        let a = e.nodes.attr(left).cloned();
491                        let n = e
492                            .nodes
493                            .alloc(id::GLYPH, glyph_sub::CHARACTER, NodeData::Glyph(g));
494                        e.nodes.set_attr(n, a);
495                        n
496                    } else {
497                        NodeRef::NULL
498                    }
499                };
500                let pre = mk(self, settings.pre_hyphen_char);
501                let post = mk(self, settings.post_hyphen_char);
502                self.nodes.set_disc_lists(d, (pre, post, NodeRef::NULL));
503                self.nodes.link(left, d);
504                self.nodes.link(d, right);
505            }
506        }
507    }
508
509    // ----- ligaturing ----------------------------------------------------
510
511    /// Whether `p` is a glyph of font `f` (any subtype except ghosts).
512    #[inline]
513    fn glyph_of_font(&self, p: NodeRef, f: u32) -> Option<u32> {
514        if p.is_null() {
515            return None;
516        }
517        match self.nodes.data(p) {
518            NodeData::Glyph(g) if g.font == f && self.nodes.subtype(p) & glyph_sub::GHOST == 0 => {
519                Some(g.chr)
520            }
521            _ => None,
522        }
523    }
524
525    /// The ligature step for (`l`, `r`) in font `f`, if it is a ligature.
526    /// A font with `\ignoreligaturesinfont` has none (its kerns stay).
527    #[inline]
528    fn lig_step(&self, f: u32, l: u32, r: u32) -> Option<(u8, u32)> {
529        if self.font(f).ignore_ligatures != 0 {
530            return None;
531        }
532        match self.lig_kern_step(f, l, r) {
533            Some(LigOp::Lig { op, chr }) => Some((op, chr)),
534            _ => None,
535        }
536    }
537
538    /// The components of a glyph for a new ligature: the glyph itself, or
539    /// its components if it is already a ligature.
540    fn take_components(&mut self, p: NodeRef) -> NodeRef {
541        let is_lig = self.nodes.subtype(p) & glyph_sub::LIGATURE != 0;
542        if is_lig {
543            let c = self
544                .nodes
545                .glyph_mut(p)
546                .map(|g| std::mem::take(&mut g.components));
547            let c = c.unwrap_or(NodeRef::NULL);
548            self.nodes.free(p);
549            if c.is_null() {
550                return NodeRef::NULL;
551            }
552            c
553        } else {
554            self.nodes.set_next(p, NodeRef::NULL);
555            self.nodes.set_prev(p, NodeRef::NULL);
556            let s = self.nodes.subtype(p) & !glyph_sub::CHARACTER;
557            self.nodes.set_subtype(p, s);
558            p
559        }
560    }
561
562    /// Makes a ligature glyph like `model` with character `c` and the given
563    /// components.
564    fn new_ligature(&mut self, model: NodeRef, c: u32, comps: NodeRef) -> NodeRef {
565        let mut g = self.nodes.glyph(model).cloned().unwrap_or_default();
566        g.chr = c;
567        g.components = comps;
568        let a = self.nodes.attr(model).cloned();
569        let n = self
570            .nodes
571            .alloc(id::GLYPH, glyph_sub::LIGATURE, NodeData::Glyph(g));
572        self.nodes.set_attr(n, a);
573        n
574    }
575
576    /// Concatenates two node lists.
577    fn concat(&mut self, a: NodeRef, b: NodeRef) -> NodeRef {
578        if a.is_null() {
579            return b;
580        }
581        if !b.is_null() {
582            let t = self.nodes.tail_of(a);
583            self.nodes.link(t, b);
584        }
585        a
586    }
587
588    /// Replaces `old` (whose predecessor is `prev`) by `new` in a list.
589    fn replace_node(&mut self, prev: NodeRef, old_next: NodeRef, new: NodeRef) {
590        self.nodes.link(prev, new);
591        self.nodes.link(new, old_next);
592        if old_next.is_null() {
593            self.nodes.set_next(new, NodeRef::NULL);
594        }
595    }
596
597    /// Builds ligatures in the list after `head`. `main` is true for the
598    /// main list (boundary nodes are removed there).
599    fn ligature_list(&mut self, head: NodeRef, main: bool) {
600        let mut prev = head;
601        let mut p = self.nodes.next(head);
602        while !p.is_null() {
603            match self.nodes.id(p) {
604                id::BOUNDARY if main => {
605                    let n = self.nodes.next(p);
606                    self.nodes.set_next(prev, n);
607                    if !n.is_null() {
608                        self.nodes.set_prev(n, prev);
609                    }
610                    self.nodes.free(p);
611                    p = n;
612                }
613                id::GLYPH => {
614                    let (last_prev, last) = self.ligature_run(prev, p);
615                    // `last` is the final glyph of the run; it may be
616                    // followed by a discretionary.
617                    let n = self.nodes.next(last);
618                    if main
619                        && (n.is_null()
620                            || !matches!(self.nodes.id(n), id::GLYPH | id::DISC | id::BOUNDARY))
621                    {
622                        self.right_boundary_kern(last);
623                    }
624                    if !n.is_null() && self.nodes.id(n) == id::DISC {
625                        let (np, nd) = self.lig_disc(last_prev, last, n);
626                        prev = np;
627                        p = nd;
628                    } else {
629                        prev = last;
630                        p = n;
631                    }
632                }
633                id::DISC => {
634                    self.lig_disc_lists(p);
635                    prev = p;
636                    p = self.nodes.next(p);
637                }
638                _ => {
639                    prev = p;
640                    p = self.nodes.next(p);
641                }
642            }
643        }
644    }
645
646    /// Appends the font kern between glyph `p` and its font's right
647    /// boundary character, if there is one. Observed: LuaTeX applies it
648    /// in the main list when the next node is not a glyph, a
649    /// discretionary or a boundary node (`\noboundary`, `\boundary`, ...),
650    /// even after a font kern (an unboxed list gets a second one).
651    fn right_boundary_kern(&mut self, p: NodeRef) {
652        let Some((f, l)) = self.nodes.glyph(p).map(|g| (g.font, g.chr)) else {
653            return;
654        };
655        let Some(b) = self.font(f).right_boundary else {
656            return;
657        };
658        if let Some(k) = self.kern_between(f, l, b) {
659            let kn = self.font_kern(k, p);
660            let n = self.nodes.next(p);
661            self.nodes.link(p, kn);
662            self.nodes.link(kn, n);
663        }
664    }
665
666    /// Ligatures the three lists of a discretionary independently.
667    fn lig_disc_lists(&mut self, d: NodeRef) {
668        let (pre, post, rep) = self.nodes.disc_lists(d);
669        let pre = self.ligature_sublist(pre);
670        let post = self.ligature_sublist(post);
671        let rep = self.ligature_sublist(rep);
672        self.nodes.set_disc_lists(d, (pre, post, rep));
673    }
674
675    /// Ligatures a detached list; returns its new head.
676    fn ligature_sublist(&mut self, l: NodeRef) -> NodeRef {
677        if l.is_null() {
678            return l;
679        }
680        let h = self.nodes.new_temp();
681        self.nodes.link(h, l);
682        self.ligature_list(h, false);
683        let r = self.nodes.next(h);
684        if !r.is_null() {
685            self.nodes.set_prev(r, NodeRef::NULL);
686        }
687        self.nodes.free(h);
688        r
689    }
690
691    /// Handles a discretionary `d` that follows the glyph `last` (whose
692    /// predecessor is found through `prev` links): moves neighbouring
693    /// glyphs into it when ligatures cross it, then ligatures its lists.
694    /// Returns (prev, next) for continuing the scan after `d`.
695    fn lig_disc(&mut self, before: NodeRef, last: NodeRef, d: NodeRef) -> (NodeRef, NodeRef) {
696        let f = self.nodes.glyph(last).map_or(0, |g| g.font);
697        if self.font(f).lig_kern.is_empty() {
698            self.lig_disc_lists(d);
699            return (d, self.nodes.next(d));
700        }
701        let lc = self.nodes.glyph(last).map_or(0, |g| g.chr);
702        let (pre, post, rep) = self.nodes.disc_lists(d);
703        let after = self.nodes.next(d);
704        let crosses =
705            |e: &Engine, l: NodeRef| e.glyph_of_font(l, f).and_then(|c| e.lig_step(f, lc, c));
706        let left = crosses(self, pre).is_some()
707            || (!rep.is_null() && crosses(self, rep).is_some())
708            || (rep.is_null() && crosses(self, after).is_some());
709        let (mut pre, mut post, mut rep) = (pre, post, rep);
710        if left {
711            // Move `last` into the discretionary.
712            self.nodes.link(before, d);
713            self.nodes.set_next(last, NodeRef::NULL);
714            self.nodes.set_prev(last, NodeRef::NULL);
715            let copy = self.nodes.copy_node(last);
716            pre = self.concat(copy, pre);
717            rep = self.concat(last, rep);
718        }
719        // Move following glyphs into the discretionary while a ligature
720        // crosses the right edge.
721        loop {
722            let n = self.nodes.next(d);
723            let Some(nc) = self.glyph_of_font(n, f) else {
724                break;
725            };
726            let ends = |e: &Engine, l: NodeRef| {
727                if l.is_null() {
728                    return false;
729                }
730                let t = e.nodes.tail_of(l);
731                e.glyph_of_font(t, f)
732                    .is_some_and(|c| e.lig_step(f, c, nc).is_some())
733            };
734            if !(ends(self, post) || ends(self, rep)) {
735                break;
736            }
737            let nn = self.nodes.next(n);
738            self.nodes.link(d, nn);
739            if nn.is_null() {
740                self.nodes.set_next(d, NodeRef::NULL);
741            }
742            self.nodes.set_next(n, NodeRef::NULL);
743            self.nodes.set_prev(n, NodeRef::NULL);
744            let copy = self.nodes.copy_node(n);
745            post = self.concat(post, copy);
746            rep = self.concat(rep, n);
747        }
748        self.nodes.set_disc_lists(d, (pre, post, rep));
749        self.lig_disc_lists(d);
750        (d, self.nodes.next(d))
751    }
752
753    /// Builds ligatures in the run of glyphs starting at `p` (with
754    /// predecessor `prev`); returns the last glyph of the processed run
755    /// and its predecessor.
756    fn ligature_run(&mut self, prev: NodeRef, p: NodeRef) -> (NodeRef, NodeRef) {
757        let f = self.nodes.glyph(p).map_or(0, |g| g.font);
758        if self.font(f).lig_kern.is_empty() {
759            // No ligatures in this font: skip the run.
760            let (mut qp, mut q) = (prev, p);
761            loop {
762                let n = self.nodes.next(q);
763                if self.glyph_of_font(n, f).is_none() {
764                    return (qp, q);
765                }
766                qp = q;
767                q = n;
768            }
769        }
770        let mut cur_prev = prev;
771        let mut cur = p;
772        let mut guard = 0u32;
773        loop {
774            let nxt = self.nodes.next(cur);
775            let (Some(l), Some(r)) = (self.glyph_of_font(cur, f), self.glyph_of_font(nxt, f))
776            else {
777                return (cur_prev, cur);
778            };
779            guard += 1;
780            if guard > 100_000 {
781                // An infinite ligature loop in a broken font.
782                return (cur_prev, cur);
783            }
784            let Some((op, c)) = self.lig_step(f, l, r) else {
785                cur_prev = cur;
786                cur = nxt;
787                continue;
788            };
789            let after = self.nodes.next(nxt);
790            match op {
791                0 => {
792                    // =: — both characters replaced.
793                    let a = self.take_components(cur);
794                    let b = self.take_components(nxt);
795                    let comps = self.concat(a, b);
796                    let model = if comps.is_null() { cur } else { comps };
797                    let lig = self.new_ligature(model, c, comps);
798                    self.replace_node(cur_prev, after, lig);
799                    cur = lig;
800                }
801                1 | 5 => {
802                    // =:| and =:|> — the left character is replaced.
803                    let a = self.take_components(cur);
804                    let model = if a.is_null() { nxt } else { a };
805                    let lig = self.new_ligature(model, c, a);
806                    self.replace_node(cur_prev, nxt, lig);
807                    if op == 5 {
808                        cur_prev = lig;
809                        cur = nxt;
810                    } else {
811                        cur = lig;
812                    }
813                }
814                2 | 6 => {
815                    // |=: and |=:> — the right character is replaced.
816                    let b = self.take_components(nxt);
817                    let model = if b.is_null() { cur } else { b };
818                    let lig = self.new_ligature(model, c, b);
819                    self.replace_node(cur, after, lig);
820                    if op == 6 {
821                        cur_prev = cur;
822                        cur = lig;
823                    }
824                }
825                _ => {
826                    // |=:|, |=:|>, |=:|>> — a character is inserted.
827                    let lig = self.new_ligature(cur, c, NodeRef::NULL);
828                    self.nodes.set_subtype(lig, 0);
829                    self.replace_node(cur, nxt, lig);
830                    match op {
831                        7 => {
832                            cur_prev = cur;
833                            cur = lig;
834                        }
835                        11 => {
836                            cur_prev = lig;
837                            cur = nxt;
838                        }
839                        _ => {}
840                    }
841                }
842            }
843        }
844    }
845
846    // ----- kerning -------------------------------------------------------
847
848    /// The font kern between (`l`, `r`) in font `f`.
849    #[inline]
850    fn kern_between(&self, f: u32, l: u32, r: u32) -> Option<i32> {
851        match self.lig_kern_step(f, l, r) {
852            Some(LigOp::Kern(k)) => Some(k),
853            _ => None,
854        }
855    }
856
857    /// A font kern node with the attributes of `model`.
858    fn font_kern(&mut self, k: i32, model: NodeRef) -> NodeRef {
859        let n = self.nodes.new_kern(k, kern_sub::FONT);
860        let a = self.nodes.attr(model).cloned();
861        self.nodes.set_attr(n, a);
862        n
863    }
864
865    /// Inserts font kerns into the list after `head` and clears the
866    /// character subtype of glyphs.
867    fn kern_list(&mut self, head: NodeRef) {
868        let mut p = self.nodes.next(head);
869        while !p.is_null() {
870            match self.nodes.id(p) {
871                id::GLYPH => {
872                    let s = self.nodes.subtype(p) & !glyph_sub::CHARACTER;
873                    self.nodes.set_subtype(p, s);
874                    let n = self.nodes.next(p);
875                    if n.is_null() {
876                        break;
877                    }
878                    let (f, l) = match self.nodes.glyph(p) {
879                        Some(g) => (g.font, g.chr),
880                        None => break,
881                    };
882                    if self.nodes.id(n) == id::GLYPH {
883                        if let Some(r) = self.glyph_of_font(n, f)
884                            && let Some(k) = self.kern_between(f, l, r)
885                        {
886                            let kn = self.font_kern(k, p);
887                            self.nodes.link(p, kn);
888                            self.nodes.link(kn, n);
889                        }
890                        p = n;
891                    } else if self.nodes.id(n) == id::DISC {
892                        self.kern_disc(p, n);
893                        p = self.nodes.next(n);
894                    } else {
895                        p = n;
896                    }
897                }
898                id::DISC => {
899                    self.kern_disc(NodeRef::NULL, p);
900                    p = self.nodes.next(p);
901                }
902                _ => p = self.nodes.next(p),
903            }
904        }
905    }
906
907    /// Kerns a discretionary with its neighbours (`before` may be null)
908    /// and kerns its lists.
909    fn kern_disc(&mut self, before: NodeRef, d: NodeRef) {
910        let (mut pre, mut post, mut rep) = self.nodes.disc_lists(d);
911        pre = self.kern_sublist(pre);
912        post = self.kern_sublist(post);
913        let rep_was_empty = rep.is_null();
914        rep = self.kern_sublist(rep);
915        let after = self.nodes.next(d);
916        let bg = if before.is_null() {
917            None
918        } else {
919            self.nodes.glyph(before).map(|g| (g.font, g.chr))
920        };
921        let ag = if after.is_null() || self.nodes.id(after) != id::GLYPH {
922            None
923        } else {
924            self.nodes.glyph(after).map(|g| (g.font, g.chr))
925        };
926        // Observed: the discretionaries of an explicit hyphen (subtype 2)
927        // and of hyphenation (3) are not kerned with the character before
928        // them in their pre-break text, nor (an explicit hyphen's) in
929        // their replacement; those of `\-` and `\discretionary` are.
930        let sub = self.nodes.subtype(d);
931        let left_kerns = !matches!(sub, 2 | 3);
932        if let Some((f, l)) = bg {
933            if left_kerns
934                && let Some(r) = self.glyph_of_font(pre, f)
935                && let Some(k) = self.kern_between(f, l, r)
936            {
937                let kn = self.font_kern(k, before);
938                self.nodes.link(kn, pre);
939                pre = kn;
940            }
941            if sub == 2 {
942                // No kern in the replacement.
943            } else if !rep_was_empty {
944                if let Some(r) = self.glyph_of_font(rep, f)
945                    && let Some(k) = self.kern_between(f, l, r)
946                {
947                    let kn = self.font_kern(k, before);
948                    self.nodes.link(kn, rep);
949                    rep = kn;
950                }
951            } else if let Some((af, ac)) = ag
952                && af == f
953                && let Some(k) = self.kern_between(f, l, ac)
954            {
955                rep = self.font_kern(k, before);
956            }
957        }
958        if let Some((f, r)) = ag {
959            if !post.is_null() {
960                let t = self.nodes.tail_of(post);
961                if let Some(l) = self.glyph_of_font(t, f)
962                    && let Some(k) = self.kern_between(f, l, r)
963                {
964                    let kn = self.font_kern(k, after);
965                    self.nodes.link(t, kn);
966                }
967            }
968            if !rep_was_empty {
969                let t = self.nodes.tail_of(rep);
970                if let Some(l) = self.glyph_of_font(t, f)
971                    && let Some(k) = self.kern_between(f, l, r)
972                {
973                    let kn = self.font_kern(k, after);
974                    self.nodes.link(t, kn);
975                }
976            }
977        }
978        self.nodes.set_disc_lists(d, (pre, post, rep));
979    }
980
981    /// Kerns a detached list; returns its new head.
982    fn kern_sublist(&mut self, l: NodeRef) -> NodeRef {
983        if l.is_null() {
984            return l;
985        }
986        let h = self.nodes.new_temp();
987        self.nodes.link(h, l);
988        self.kern_list(h);
989        let r = self.nodes.next(h);
990        if !r.is_null() {
991            self.nodes.set_prev(r, NodeRef::NULL);
992        }
993        self.nodes.free(h);
994        r
995    }
996}