Skip to main content

retex_lua/
otfnodes.rs

1//! luaotfload's node processor: OpenType shaping applied to node lists.
2//!
3//! A *run* is a sequence of unprocessed glyphs of one OpenType font,
4//! possibly interrupted by discretionaries whose lists hold only such
5//! glyphs. Runs are shaped with [`retex_font::Shaper`] and the glyph nodes
6//! are replaced by the result (glyphs, ligatures with their components,
7//! font kerns). Shaped glyphs are marked as processed (subtype bit 256),
8//! as LuaTeX's `node.protect_glyphs` does.
9//!
10//! Discretionaries take part in shaping as luaotfload does. Observed with
11//! `luatex` (Latin Modern and Linux Libertine, see the regression tests):
12//!
13//! * The unbroken text (each discretionary replaced by its replacement
14//!   list) is shaped as a whole. Its glyphs that come entirely from text
15//!   before a discretionary stay in front of it, those entirely from text
16//!   after it stay behind it; everything in between (kerns across the
17//!   break point, ligatures that span it) becomes the replacement list. So
18//!   a kern between `n` and `t` gives `n{-||[k]}t`, and `of\-fice` gives
19//!   `o{f-|fi|ffi}ce`.
20//! * The pre-break list is the characters the replacement took from the
21//!   left followed by the pre-break text, shaped on their own; the
22//!   post-break list likewise. The glyph outside next to them only adds a
23//!   kern at the boundary: in bold Libertine (no `ffl` ligature)
24//!   `af\-fluent` gives `a{f-|f[k]|ff[k]}luent`, without an `fl` ligature
25//!   in the post-break list.
26//! * When two discretionaries follow each other directly, nothing is
27//!   shaped across them (`W\-\-o` gets no kern).
28//! * In base mode, the discretionaries of hyphenation and of explicit
29//!   hyphens get no boundary kern before their pre-break text, nor
30//!   explicit hyphens in their replacement (as with TFM fonts).
31//!
32//! `mode=harf` fonts (polyglossia's default) differ, as HarfBuzz reports
33//! where breaking the shaped text is safe: a discretionary also takes the
34//! glyphs on either side up to a boundary without a kern (`AV\-AV` puts
35//! the whole word into it), its pre-break and post-break texts get no
36//! kern with the glyphs outside, and discretionaries whose glyphs overlap
37//! become one, with the first one's pre-break and the last one's
38//! post-break text (`W\-\-o` gives `{W-|o|W[k]o}`).
39
40use std::collections::{HashMap, HashSet};
41use std::sync::Arc;
42
43use retex_engine::Engine;
44use retex_engine::nodes::{GlyphData, NodeData, NodeRef, glyph_sub, id, kern_sub};
45use retex_font::{LoadedFont, ShapedItem};
46
47use crate::luaotfload::loaded_font;
48
49/// Protection bit LuaTeX sets in the subtype of processed glyphs.
50const PROTECTED: u16 = 256;
51
52/// The unprocessed glyph `p` in font `font`, if it is one.
53fn is_raw_glyph(e: &Engine, p: NodeRef, font: u32) -> bool {
54    let n = e.nodes.get(p);
55    matches!(&n.data, NodeData::Glyph(g) if g.font == font && n.subtype < PROTECTED)
56}
57
58/// The nodes of the list starting at `head`, if all are unprocessed glyphs
59/// of `font`.
60fn glyph_list(e: &Engine, head: NodeRef, font: u32) -> Option<Vec<NodeRef>> {
61    let mut v = Vec::new();
62    let mut p = head;
63    while !p.is_null() {
64        if !is_raw_glyph(e, p, font) {
65            return None;
66        }
67        v.push(p);
68        p = e.nodes.next(p);
69    }
70    Some(v)
71}
72
73fn disc_lists(e: &Engine, d: NodeRef) -> [NodeRef; 3] {
74    match &e.nodes.get(d).data {
75        NodeData::Disc {
76            pre, post, replace, ..
77        } => [*pre, *post, *replace],
78        _ => [NodeRef::NULL; 3],
79    }
80}
81
82fn set_disc_lists(e: &mut Engine, d: NodeRef, lists: [NodeRef; 3]) {
83    if let NodeData::Disc {
84        pre, post, replace, ..
85    } = &mut e.nodes.get_mut(d).data
86    {
87        [*pre, *post, *replace] = lists;
88    }
89}
90
91/// How many characters of context [`context`] collects on each side.
92const CONTEXT_LEN: usize = 8;
93
94/// The text around a run that contextual lookups can see: from `p`
95/// onwards (or backwards), glue counts as a space and glyphs of `font` as
96/// their characters; anything else ends it. Observed with luaotfload (Inter's
97/// `calt`: `A) v` keeps the default `)`, `A)\kern0pt v`, `A)~v` and
98/// `A)\penalty0 v` get the capital form).
99fn context(e: &Engine, mut p: NodeRef, font: u32, forward: bool) -> Vec<u32> {
100    let mut out = Vec::new();
101    while !p.is_null() && out.len() < CONTEXT_LEN {
102        match &e.nodes.get(p).data {
103            NodeData::Glue { .. } => out.push(0x20),
104            NodeData::Glyph(g) if g.font == font => out.push(g.chr),
105            _ => break,
106        }
107        p = if forward {
108            e.nodes.next(p)
109        } else {
110            e.nodes.prev(p)
111        };
112    }
113    if !forward {
114        out.reverse();
115    }
116    out
117}
118
119fn chr(e: &Engine, p: NodeRef) -> u32 {
120    match &e.nodes.get(p).data {
121        NodeData::Glyph(g) => g.chr,
122        _ => 0,
123    }
124}
125
126/// A run: glyph segments separated by discretionaries
127/// (`segs.len() == discs.len() + 1`).
128struct DiscRun {
129    segs: Vec<Vec<NodeRef>>,
130    discs: Vec<NodeRef>,
131    /// Pre, post and replacement glyphs of each discretionary.
132    lists: Vec<[Vec<NodeRef>; 3]>,
133}
134
135/// Shapes all runs of unprocessed glyphs in the list starting at `head`
136/// (luaotfload's node mode). Returns the (possibly new) head of the list.
137pub fn process_list(e: &mut Engine, head: NodeRef) -> NodeRef {
138    let mut head = head;
139    let mut marks: HashMap<u32, bool> = HashMap::new();
140    // The text direction: the paragraph's (its local_par node), changed
141    // by direction nodes; right to left (TRT) shapes mode=harf text so.
142    let mut base_rtl = false;
143    let mut dirs: Vec<bool> = Vec::new();
144    let mut p = head;
145    while !p.is_null() {
146        let node = e.nodes.get(p);
147        match &node.data {
148            NodeData::Glyph(g) if node.subtype < PROTECTED => {
149                let font = g.font;
150                let Some(lf) = loaded_font(e, font) else {
151                    p = e.nodes.next(p);
152                    continue;
153                };
154                let mut run = DiscRun {
155                    segs: vec![vec![p]],
156                    discs: Vec::new(),
157                    lists: Vec::new(),
158                };
159                let mut q = e.nodes.next(p);
160                while !q.is_null() {
161                    if is_raw_glyph(e, q, font) {
162                        run.segs.last_mut().expect("segment").push(q);
163                    } else if e.nodes.id(q) == id::DISC {
164                        let [pre, post, rep] = disc_lists(e, q);
165                        let (Some(a), Some(b), Some(c)) = (
166                            glyph_list(e, pre, font),
167                            glyph_list(e, post, font),
168                            glyph_list(e, rep, font),
169                        ) else {
170                            break;
171                        };
172                        run.discs.push(q);
173                        run.lists.push([a, b, c]);
174                        run.segs.push(Vec::new());
175                    } else {
176                        break;
177                    }
178                    q = e.nodes.next(q);
179                }
180                let first = p;
181                let marks = *marks.entry(font).or_insert_with(|| {
182                    let mut chars = Vec::new();
183                    font_chars(e, head, font, &mut chars);
184                    lf.shaper.has_marks(&lf.file, &lf.tex, &chars)
185                });
186                let rtl = dirs.last().copied().unwrap_or(base_rtl);
187                let ctx = if lf.shaper.uses_context {
188                    (
189                        context(e, e.nodes.prev(first), font, false),
190                        context(e, q, font, true),
191                        marks,
192                        rtl,
193                    )
194                } else {
195                    (Vec::new(), Vec::new(), marks, rtl)
196                };
197                let new_first = if run.discs.is_empty() {
198                    shape_plain(e, &lf, &run.segs[0], &ctx)
199                } else {
200                    shape_disc_run(e, &lf, &run, &ctx)
201                        .unwrap_or_else(|| shape_separately(e, &lf, &run, ctx.2, ctx.3))
202                };
203                if first == head {
204                    head = new_first;
205                }
206                p = q;
207            }
208            NodeData::LocalPar { dir, .. } => {
209                base_rtl = *dir == 1;
210                p = e.nodes.next(p);
211            }
212            NodeData::Dir { dir, .. } => {
213                if node.subtype == 0 {
214                    dirs.push(*dir == 1);
215                } else {
216                    dirs.pop();
217                }
218                p = e.nodes.next(p);
219            }
220            NodeData::Disc { .. } => {
221                let lists = disc_lists(e, p);
222                let new = lists.map(|l| if l.is_null() { l } else { process_list(e, l) });
223                set_disc_lists(e, p, new);
224                p = e.nodes.next(p);
225            }
226            _ => p = e.nodes.next(p),
227        }
228    }
229    head
230}
231
232/// Builds nodes for shaped `items`; `src[i]` is the original glyph node of
233/// input character `i`, whose data and attributes the new glyph inherits.
234///
235/// A glyph that comes from a single source glyph reuses that node (once;
236/// `reused` collects them), as luaotfload changes glyph nodes in place: the
237/// node keeps its identity, so Lua properties set before shaping survive
238/// (tagpdf marks discretionary hyphens in `pre_shaping_filter` and rewrites
239/// them in `post_shaping_filter`).
240fn build(
241    e: &mut Engine,
242    items: &[ShapedItem],
243    src: &[NodeRef],
244    reused: &mut HashSet<NodeRef>,
245    base: bool,
246) -> Vec<NodeRef> {
247    let mut out = Vec::with_capacity(items.len());
248    let Some(&last_src) = src.last() else {
249        return out;
250    };
251    let source = |k: usize| src.get(k).copied().unwrap_or(last_src);
252    for it in items {
253        match it {
254            ShapedItem::Glyph(g) => {
255                let s = source(g.cluster);
256                let mut data = match &e.nodes.get(s).data {
257                    NodeData::Glyph(d) => d.clone(),
258                    _ => GlyphData::default(),
259                };
260                let attr = e.nodes.get(s).attr.clone();
261                data.chr = g.code;
262                data.xoffset = g.x_offset;
263                data.yoffset = g.y_offset;
264                data.components = NodeRef::NULL;
265                // Observed: in base mode, where TeX's own ligaturing and
266                // kerning run, glyphs lose the character bit and ligatures
267                // are marked as such (box displays show "(ligature …)").
268                let mut subtype = if base {
269                    PROTECTED
270                } else {
271                    glyph_sub::CHARACTER | PROTECTED
272                };
273                if !g.components.is_empty() {
274                    let mut ch = NodeRef::NULL;
275                    let mut tail = NodeRef::NULL;
276                    for (k, &c) in g.components.iter().enumerate() {
277                        let n = e.nodes.copy_node(source(g.cluster + k));
278                        if let NodeData::Glyph(gd) = &mut e.nodes.get_mut(n).data {
279                            gd.chr = c;
280                        }
281                        if ch.is_null() {
282                            ch = n;
283                        } else {
284                            e.nodes.link(tail, n);
285                        }
286                        tail = n;
287                    }
288                    data.components = ch;
289                    // In node mode luaotfload keeps the components but does
290                    // not mark the glyph as a ligature (observed subtype
291                    // 256), so box displays show no "(ligature …)".
292                    subtype = if base {
293                        PROTECTED | glyph_sub::LIGATURE
294                    } else {
295                        PROTECTED
296                    };
297                }
298                let n = if data.components.is_null() && reused.insert(s) {
299                    let node = e.nodes.get_mut(s);
300                    node.subtype = subtype;
301                    node.data = NodeData::Glyph(data);
302                    s
303                } else {
304                    let n = e.nodes.alloc(id::GLYPH, subtype, NodeData::Glyph(data));
305                    e.nodes.get_mut(n).attr = attr;
306                    n
307                };
308                out.push(n);
309            }
310            ShapedItem::Kern(k) => {
311                let attr = e.nodes.get(last_src).attr.clone();
312                let n = e.nodes.new_kern(*k, kern_sub::FONT);
313                // luaotfload copies its kerns: they get the current line.
314                e.nodes.sync_as_copied(n);
315                e.nodes.get_mut(n).attr = attr;
316                out.push(n);
317            }
318        }
319    }
320    // Kerns take the attributes of the glyph before them.
321    for i in 1..out.len() {
322        if e.nodes.id(out[i]) == id::KERN {
323            let a = e.nodes.get(out[i - 1]).attr.clone();
324            e.nodes.get_mut(out[i]).attr = a;
325        }
326    }
327    out
328}
329
330/// Links `nodes` into a list and returns its head.
331fn link_all(e: &mut Engine, nodes: &[NodeRef]) -> NodeRef {
332    for w in nodes.windows(2) {
333        e.nodes.link(w[0], w[1]);
334    }
335    if let Some(&f) = nodes.first() {
336        e.nodes.get_mut(f).prev = NodeRef::NULL;
337    }
338    if let Some(&l) = nodes.last() {
339        e.nodes.get_mut(l).next = NodeRef::NULL;
340    }
341    nodes.first().copied().unwrap_or(NodeRef::NULL)
342}
343
344/// Frees glyph nodes (each a single node in some list), except those
345/// reused by [`build`].
346fn free_glyphs(e: &mut Engine, nodes: &[NodeRef], reused: &HashSet<NodeRef>) {
347    for &n in nodes {
348        if !reused.contains(&n) {
349            e.nodes.get_mut(n).next = NodeRef::NULL;
350            e.nodes.flush_list(n);
351        }
352    }
353}
354
355/// Replaces the nodes from `first` to `last` (inclusive) of a list by
356/// `new`; returns the node now in the place of `first`.
357/// Links `new` between `prev` and `next` (either may be null), replacing
358/// what was there. Returns the first node after `prev`.
359fn splice(e: &mut Engine, prev: NodeRef, next: NodeRef, new: &[NodeRef]) -> NodeRef {
360    let mut cur = prev;
361    for &n in new {
362        if cur.is_null() {
363            e.nodes.get_mut(n).prev = NodeRef::NULL;
364        } else {
365            e.nodes.link(cur, n);
366        }
367        cur = n;
368    }
369    if cur.is_null() {
370        return next;
371    }
372    e.nodes.link(cur, next);
373    if !next.is_null() {
374        e.nodes.get_mut(next).prev = cur;
375    }
376    new.first().copied().unwrap_or(next)
377}
378
379/// Shapes a run of glyphs without discretionaries.
380/// Context characters before and after a run (see [`context`]), and
381/// whether the list holds a combining mark of the run's font (see
382/// `Shaper::has_marks`).
383/// The text before and after a run, whether its font's characters
384/// include marks, and whether it is right to left.
385type Context = (Vec<u32>, Vec<u32>, bool, bool);
386
387/// Shapes `text`: with HarfBuzz for mode=harf fonts when the text needs it
388/// (complex scripts, right to left) and for the joining scripts (Arabic)
389/// in node mode, else with retex's own shaper.
390fn shape_text(
391    lf: &LoadedFont,
392    before: &[u32],
393    text: &[u32],
394    after: &[u32],
395    marks: bool,
396    rtl: bool,
397) -> Vec<retex_font::ShapedItem> {
398    let harf = lf.request.mode == retex_font::Mode::Harf;
399    let use_hb = if harf {
400        rtl || retex_font::harf::needs_harfbuzz(text)
401    } else {
402        lf.request.mode == retex_font::Mode::Node && retex_font::harf::joining_script(text)
403    };
404    if use_hb && let Some(h) = lf.harf() {
405        return h.shape(&lf.tex, text, rtl);
406    }
407    let mut items = lf
408        .shaper
409        .shape_marked(&lf.file, &lf.tex, before, text, after, marks);
410    if rtl {
411        mirror_mark_offsets(lf, &mut items);
412    }
413    items
414}
415
416/// Right-to-left text: a mark's horizontal offset counts from the left
417/// edge of its base, towards the left (as LuaTeX applies it there), not
418/// from the end of the base. retex's shaper gives `anchors − advances`
419/// (the advances from the base to the mark); the offset is then
420/// `−(anchors)`. Observed with node-mode Hebrew in DejaVu Sans.
421fn mirror_mark_offsets(lf: &LoadedFont, items: &mut [retex_font::ShapedItem]) {
422    use retex_font::ShapedItem;
423    let mut since_base = 0i32;
424    for it in items.iter_mut() {
425        match it {
426            ShapedItem::Glyph(g) if g.x_offset == 0 && g.y_offset == 0 => {
427                since_base = g
428                    .glyph
429                    .map(|gid| lf.tex.glyph_metrics(gid).width)
430                    .unwrap_or(0);
431            }
432            ShapedItem::Glyph(g) => {
433                if g.x_offset != 0 {
434                    g.x_offset = -(g.x_offset + since_base);
435                }
436                since_base += g
437                    .glyph
438                    .map(|gid| lf.tex.glyph_metrics(gid).width)
439                    .unwrap_or(0);
440            }
441            ShapedItem::Kern(k) => since_base += *k,
442        }
443    }
444}
445
446/// The characters of font `font` in the list `head`, discretionaries
447/// included.
448fn font_chars(e: &Engine, head: NodeRef, font: u32, out: &mut Vec<u32>) {
449    let mut p = head;
450    while !p.is_null() {
451        match &e.nodes.get(p).data {
452            NodeData::Glyph(g) if g.font == font => out.push(chr(e, p)),
453            NodeData::Disc { .. } => {
454                for l in disc_lists(e, p) {
455                    font_chars(e, l, font, out);
456                }
457            }
458            _ => {}
459        }
460        p = e.nodes.next(p);
461    }
462}
463
464fn shape_plain(e: &mut Engine, lf: &Arc<LoadedFont>, run: &[NodeRef], ctx: &Context) -> NodeRef {
465    let chars: Vec<u32> = run.iter().map(|&n| chr(e, n)).collect();
466    let items = shape_text(lf, &ctx.0, &chars, &ctx.1, ctx.2, ctx.3);
467    let unchanged = items.len() == run.len()
468        && items.iter().zip(&chars).all(|(it, &c)| {
469            matches!(it, ShapedItem::Glyph(g) if g.code == c && g.components.is_empty()
470                && g.x_offset == 0 && g.y_offset == 0)
471        });
472    let first = run[0];
473    let last = *run.last().expect("non-empty run");
474    let base = lf.request.mode == retex_font::Mode::Base;
475    if unchanged {
476        for &n in run {
477            let node = e.nodes.get_mut(n);
478            if base {
479                node.subtype &= !glyph_sub::CHARACTER;
480            }
481            node.subtype |= PROTECTED;
482        }
483        return first;
484    }
485    let mut reused = HashSet::new();
486    let (prev, next) = (e.nodes.get(first).prev, e.nodes.next(last));
487    let new = build(e, &items, run, &mut reused, base);
488    let new_first = splice(e, prev, next, &new);
489    free_glyphs(e, run, &reused);
490    new_first
491}
492
493fn n_chars(it: &ShapedItem) -> usize {
494    match it {
495        ShapedItem::Glyph(g) => g.components.len().max(1),
496        ShapedItem::Kern(_) => 0,
497    }
498}
499
500/// Shapes a run with discretionaries as described in the module
501/// documentation. Returns `None` (leaving the nodes untouched) when the
502/// run cannot be handled that way.
503fn shape_disc_run(
504    e: &mut Engine,
505    lf: &Arc<LoadedFont>,
506    run: &DiscRun,
507    ctx: &Context,
508) -> Option<NodeRef> {
509    let k = run.discs.len();
510    let base = lf.request.mode == retex_font::Mode::Base;
511    let harf = lf.request.mode == retex_font::request::Mode::Harf;
512    // The run's neighbours, before its glyphs move into new lists.
513    let first = run.segs[0][0];
514    let last = run
515        .segs
516        .last()
517        .and_then(|s| s.last().copied())
518        .unwrap_or(*run.discs.last().expect("a discretionary"));
519    let (prev, next) = (e.nodes.get(first).prev, e.nodes.next(last));
520    if !harf && run.segs[1..k].iter().any(Vec::is_empty) {
521        return None;
522    }
523    // The unbroken text and the start of each replacement in it.
524    let mut usrc: Vec<NodeRef> = Vec::new();
525    let mut starts = Vec::with_capacity(k);
526    for i in 0..k {
527        usrc.extend(&run.segs[i]);
528        starts.push(usrc.len());
529        usrc.extend(&run.lists[i][2]);
530    }
531    usrc.extend(&run.segs[k]);
532    let uchars: Vec<u32> = usrc.iter().map(|&n| chr(e, n)).collect();
533    if lf.shaper.changes_text(&lf.tex, &uchars, ctx.2) {
534        return None;
535    }
536    let shape = |src: &[NodeRef]| {
537        let chars: Vec<u32> = src.iter().map(|&n| chr(e, n)).collect();
538        shape_text(lf, &[], &chars, &[], ctx.2, ctx.3)
539    };
540    let u = shape_text(lf, &ctx.0, &uchars, &ctx.1, ctx.2, ctx.3);
541    let n = u.len();
542    let is_kern = |i: usize| matches!(u[i], ShapedItem::Kern(_));
543    struct Piece {
544        pre: (Vec<ShapedItem>, Vec<NodeRef>),
545        post: (Vec<ShapedItem>, Vec<NodeRef>),
546        lo: usize,
547        hi: usize,
548        /// Where the replacement list starts (after `lo` for an explicit
549        /// hyphen, whose kern with the glyph before it goes).
550        rep_lo: usize,
551        /// The discretionary, and those merged into it (harf mode).
552        disc: NodeRef,
553        merged: Vec<NodeRef>,
554    }
555    let glyph_id = |it: &ShapedItem| match it {
556        ShapedItem::Glyph(g) => g.glyph,
557        ShapedItem::Kern(_) => None,
558    };
559    // The items of `u` each discretionary takes: (lo, hi, a, b).
560    let mut regions: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(k);
561    for (&a, lists) in starts.iter().zip(&run.lists) {
562        let b = a + lists[2].len();
563        // The items of `u` entirely before the replacement stay in front
564        // of the discretionary; those entirely after it, behind.
565        let mut lo = 0;
566        while lo < n
567            && match &u[lo] {
568                ShapedItem::Glyph(g) => g.cluster + n_chars(&u[lo]) <= a,
569                ShapedItem::Kern(_) => true,
570            }
571        {
572            lo += 1;
573        }
574        while lo > 0 && is_kern(lo - 1) {
575            lo -= 1;
576        }
577        let mut s = 0;
578        while s < n
579            && match &u[n - 1 - s] {
580                ShapedItem::Glyph(g) => g.cluster >= b,
581                ShapedItem::Kern(_) => true,
582            }
583        {
584            s += 1;
585        }
586        while s > 0 && is_kern(n - s) {
587            s -= 1;
588        }
589        let mut hi = n - s;
590        if harf {
591            // Out to boundaries where breaking is safe: no kern there.
592            while lo > 0 && (is_kern(lo - 1) || (lo < n && is_kern(lo))) {
593                lo -= 1;
594            }
595            while hi < n && (is_kern(hi) || (hi > 0 && is_kern(hi - 1))) {
596                hi += 1;
597            }
598        }
599        if hi < lo {
600            return None;
601        }
602        regions.push((lo, hi, a, b));
603    }
604    // Groups of discretionaries that become one: overlapping ones in harf
605    // mode (elsewhere an overlap is not handled).
606    let mut groups: Vec<(usize, usize)> = Vec::new();
607    for i in 0..k {
608        match groups.last_mut() {
609            Some(g) if regions[i].0 < regions[g.1].1 => {
610                if !harf {
611                    return None;
612                }
613                g.1 = i;
614            }
615            _ => groups.push((i, i)),
616        }
617    }
618    let mut pieces: Vec<Piece> = Vec::with_capacity(groups.len());
619    for &(first, last) in &groups {
620        let lo = (first..=last).map(|i| regions[i].0).min().unwrap_or(0);
621        let hi = (first..=last).map(|i| regions[i].1).max().unwrap_or(0);
622        let (a, b) = (regions[first].2, regions[last].3);
623        let (pre, post) = (&run.lists[first][0], &run.lists[last][1]);
624        // The characters the replacement took from either side.
625        let ca = u[..lo]
626            .iter()
627            .filter_map(|it| match it {
628                ShapedItem::Glyph(g) => Some(g.cluster + n_chars(it)),
629                ShapedItem::Kern(_) => None,
630            })
631            .max()
632            .unwrap_or(0)
633            .min(a);
634        let cb = u[hi..]
635            .iter()
636            .filter_map(|it| match it {
637                ShapedItem::Glyph(g) => Some(g.cluster),
638                ShapedItem::Kern(_) => None,
639            })
640            .min()
641            .unwrap_or(usrc.len())
642            .max(b);
643        // Pre-break and post-break texts are shaped on their own; in node
644        // mode the glyphs outside add a kern at the boundary.
645        let mut psrc = usrc[ca..a].to_vec();
646        psrc.extend(pre);
647        let mut qsrc = post.clone();
648        qsrc.extend(&usrc[b..cb]);
649        let mut pre_items = shape(&psrc);
650        let mut post_items = shape(&qsrc);
651        // Observed in base mode, where TeX's own kerning runs, as with TFM
652        // fonts: the discretionaries of explicit hyphens (subtype 2) and
653        // of hyphenation (3) get no kern with the glyph before them in
654        // their pre-break text, nor (explicit hyphens) in their
655        // replacement. luaotfload's node mode kerns them.
656        let sub = e.nodes.get(run.discs[first]).subtype;
657        let left_kerns = !(base && matches!(sub, 2 | 3));
658        let mut rep_lo = lo;
659        if base && sub == 2 {
660            while rep_lo < hi && is_kern(rep_lo) {
661                rep_lo += 1;
662            }
663        }
664        if !harf {
665            if left_kerns
666                && let (Some(l), Some(r)) = (
667                    lo.checked_sub(1).and_then(|i| glyph_id(&u[i])),
668                    pre_items.first().and_then(glyph_id),
669                )
670            {
671                let k = lf.shaper.pair_kern(&lf.file, &lf.tex, l, r);
672                if k != 0 {
673                    pre_items.insert(0, ShapedItem::Kern(k));
674                }
675            }
676            if let (Some(l), Some(r)) = (
677                post_items.last().and_then(glyph_id),
678                u.get(hi).and_then(glyph_id),
679            ) {
680                let k = lf.shaper.pair_kern(&lf.file, &lf.tex, l, r);
681                if k != 0 {
682                    post_items.push(ShapedItem::Kern(k));
683                }
684            }
685        }
686        pieces.push(Piece {
687            pre: (pre_items, psrc),
688            post: (post_items, qsrc),
689            lo,
690            hi,
691            rep_lo,
692            disc: run.discs[first],
693            merged: run.discs[first + 1..=last].to_vec(),
694        });
695    }
696    // Build the new list.
697    let mut out: Vec<NodeRef> = Vec::new();
698    let mut pos = 0usize;
699    let mut reused = HashSet::new();
700    for pc in &pieces {
701        out.extend(build(e, &u[pos..pc.lo], &usrc, &mut reused, base));
702        let pre = build(e, &pc.pre.0, &pc.pre.1, &mut reused, base);
703        let post = build(e, &pc.post.0, &pc.post.1, &mut reused, base);
704        let rep = build(e, &u[pc.rep_lo..pc.hi], &usrc, &mut reused, base);
705        let d = pc.disc;
706        let lists = [link_all(e, &pre), link_all(e, &post), link_all(e, &rep)];
707        set_disc_lists(e, d, lists);
708        out.push(d);
709        pos = pc.hi;
710    }
711    out.extend(build(e, &u[pos..], &usrc, &mut reused, base));
712    // Splice, then free the old glyphs.
713    let new_first = splice(e, prev, next, &out);
714    for seg in &run.segs {
715        free_glyphs(e, seg, &reused);
716    }
717    for l in &run.lists {
718        for part in l {
719            free_glyphs(e, part, &reused);
720        }
721    }
722    // Discretionaries merged into another one (their glyphs are freed).
723    for &d in pieces.iter().flat_map(|pc| &pc.merged) {
724        set_disc_lists(e, d, [NodeRef::NULL; 3]);
725        e.nodes.get_mut(d).next = NodeRef::NULL;
726        e.nodes.flush_list(d);
727    }
728    Some(new_first)
729}
730
731/// Shapes the segments and discretionary lists of `run` independently.
732fn shape_separately(
733    e: &mut Engine,
734    lf: &Arc<LoadedFont>,
735    run: &DiscRun,
736    marks: bool,
737    rtl: bool,
738) -> NodeRef {
739    let mut first = NodeRef::NULL;
740    for (i, seg) in run.segs.iter().enumerate() {
741        if !seg.is_empty() {
742            let f = shape_plain(e, lf, seg, &(Vec::new(), Vec::new(), marks, rtl));
743            if i == 0 {
744                first = f;
745            }
746        }
747        if let Some(&d) = run.discs.get(i) {
748            let lists = disc_lists(e, d);
749            let new = lists.map(|l| if l.is_null() { l } else { process_list(e, l) });
750            set_disc_lists(e, d, new);
751        }
752    }
753    first
754}