Skip to main content

text/
segment.rs

1// segment -- Unicode text segmentation: grapheme clusters and words (UAX #29), line break
2// opportunities (UAX #14), Unicode 15.0 (segment_data.rs, tools/gen_segment.py)
3//
4//   grapheme_next(s, i) -> index     the end of the grapheme cluster that starts at or holds i
5//   grapheme_prev(s, i) -> index     the start of the cluster before i (0 at the start)
6//   cluster_end(s, i) -> index       the end of the cluster that starts at boundary i (walks
7//                                    over a text cluster by cluster)
8//   is_grapheme_boundary(s, i)
9//   graphemes(s) -> Vec<usize>       every cluster boundary, 0 and s.len() included
10//   pictographic_cluster(s, a, b)    the cluster a..b is an emoji sequence (Backspace deletes it
11//                                    whole; other clusters lose their last code point)
12//   word_bounds(s) -> Vec<usize>     every word boundary, 0 and s.len() included
13//   word_at(s, i) -> (start, end)    the word segment that holds i (at s.len(): the last one)
14//   word_kind(s, a, b) -> WK_*       WK_WORD (a letter or number in it), WK_SPACE (only white
15//                                    space), WK_PUNCT
16//   word_next(s, i) / word_prev(s, i)   Ctrl+Right / Ctrl+Left: the start of the next word
17//                                    after i (spaces skipped) / of the word before i
18//   line_breaks(s) -> Vec<(usize, bool)>   break opportunities (index where a line may start,
19//                                    mandatory), s.len() last (mandatory)
20//   wrap_end(s, pos, pe, fits) -> end   where a line from pos ends inside pos..pe: the last
21//                                    opportunity whose text (without trailing spaces) fits,
22//                                    else graphemes while they fit (at least one); a mandatory
23//                                    break inside ends it
24//   wrap_end_hyph(s, pos, pe, fits, hyph) -> (end, hyphen)   the same with hyphenation: a line
25//                                    may end after a soft hyphen (U+00AD) or, with a hook
26//                                    (hyph(word, &mut offsets): where a word may break, e.g.
27//                                    spell::Hyphenator::hyphenate), inside the word that does
28//                                    not fit; hyphen: draw a hyphen after the line.
29//                                    fits(a, b, hyphen): a..b (and a hyphen) fits
30//   char_props(c) -> u32             the packed properties (tests, other tables)
31//
32// Indices are byte offsets into UTF-8 text; an index inside a character is moved to that
33// character's start.  Rules: UAX #29 GB3..GB13 and WB3..WB16 of Unicode 15.0 (no GB9c: that came
34// with 15.1); UAX #14 LB2..LB31 with the pair rules of LB25 (numbers), LB30 for East Asian
35// parentheses and LB30b for emoji modifiers.  Grapheme and word scans around i start at the
36// nearest place before i where no rule joins or looks across (usually right there; at the
37// latest the paragraph start, the last '\n' before i) and words end at the next such place,
38// so the cost does not grow with the length of a line.
39
40use alloc::vec::Vec;
41
42use crate::segment_data::{RUN_PROPS, RUN_START};
43
44// Grapheme_Cluster_Break (bits 0..3; 0 Other)
45const G_CR: u32 = 1;
46const G_LF: u32 = 2;
47const G_CONTROL: u32 = 3;
48const G_EXTEND: u32 = 4;
49const G_ZWJ: u32 = 5;
50const G_RI: u32 = 6;
51const G_PREPEND: u32 = 7;
52const G_SPACINGMARK: u32 = 8;
53const G_L: u32 = 9;
54const G_V: u32 = 10;
55const G_T: u32 = 11;
56const G_LV: u32 = 12;
57const G_LVT: u32 = 13;
58
59// Word_Break (bits 4..8; 0 Other)
60const W_CR: u32 = 1;
61const W_LF: u32 = 2;
62const W_NEWLINE: u32 = 3;
63const W_EXTEND: u32 = 4;
64const W_ZWJ: u32 = 5;
65const W_RI: u32 = 6;
66const W_FORMAT: u32 = 7;
67const W_KATAKANA: u32 = 8;
68const W_HL: u32 = 9;
69const W_ALETTER: u32 = 10;
70const W_SQ: u32 = 11;
71const W_DQ: u32 = 12;
72const W_MIDNUMLET: u32 = 13;
73const W_MIDLETTER: u32 = 14;
74const W_MIDNUM: u32 = 15;
75const W_NUMERIC: u32 = 16;
76const W_EXTENDNUMLET: u32 = 17;
77const W_WSEG: u32 = 18;
78
79// Line_Break (bits 9..14; AI, SG, XX, SA and CJ resolved by the generator: LB1)
80const L_BK: u32 = 1;
81const L_CR: u32 = 2;
82const L_LF: u32 = 3;
83const L_CM: u32 = 4;
84const L_NL: u32 = 5;
85const L_WJ: u32 = 7;
86const L_ZW: u32 = 8;
87const L_GL: u32 = 9;
88const L_SP: u32 = 10;
89const L_ZWJ: u32 = 11;
90const L_B2: u32 = 12;
91const L_BA: u32 = 13;
92const L_BB: u32 = 14;
93const L_HY: u32 = 15;
94const L_CB: u32 = 16;
95const L_CL: u32 = 17;
96const L_CP: u32 = 18;
97const L_EX: u32 = 19;
98const L_IN: u32 = 20;
99const L_NS: u32 = 21;
100const L_OP: u32 = 22;
101const L_QU: u32 = 23;
102const L_IS: u32 = 24;
103const L_NU: u32 = 25;
104const L_PO: u32 = 26;
105const L_PR: u32 = 27;
106const L_SY: u32 = 28;
107const L_AL: u32 = 30;
108const L_EB: u32 = 32;
109const L_EM: u32 = 33;
110const L_H2: u32 = 34;
111const L_H3: u32 = 35;
112const L_HL: u32 = 36;
113const L_ID: u32 = 37;
114const L_JL: u32 = 38;
115const L_JV: u32 = 39;
116const L_JT: u32 = 40;
117const L_RI: u32 = 41;
118
119const P_EXTPICT: u32 = 1 << 15;
120const P_WIDE: u32 = 1 << 16;
121const P_ALNUM: u32 = 1 << 17;
122const P_SPACE: u32 = 1 << 18;
123const P_UNASSIGNED: u32 = 1 << 19;
124
125/// word_kind: a word (a letter or number in it)
126pub const WK_WORD: u32 = 0;
127/// word_kind: white space only
128pub const WK_SPACE: u32 = 1;
129/// word_kind: anything else (punctuation, symbols)
130pub const WK_PUNCT: u32 = 2;
131
132/// the packed properties of a character (see segment_data.rs)
133pub fn char_props(c: char) -> u32 {
134    let cp = c as u32;
135    let i = RUN_START.partition_point(|&s| s <= cp);
136    RUN_PROPS[i.saturating_sub(1)]
137}
138
139fn gcb(p: u32) -> u32 {
140    p & 15
141}
142fn wb(p: u32) -> u32 {
143    p >> 4 & 31
144}
145fn lbc(p: u32) -> u32 {
146    p >> 9 & 63
147}
148
149// i moved back to a character start
150fn floor(s: &str, mut i: usize) -> usize {
151    i = i.min(s.len());
152    while !s.is_char_boundary(i) {
153        i -= 1;
154    }
155    i
156}
157
158// the start of the paragraph of i (after the last '\n' before i)
159fn para_start(s: &str, i: usize) -> usize {
160    s.as_bytes()[..i].iter().rposition(|&b| b == b'\n').map_or(0, |k| k + 1)
161}
162
163// ------------------------------------------------------------------ grapheme clusters
164
165// the characters with properties pp (before) and p stay in one cluster: ri regional indicators
166// in a row before p, emoji the state of GB11 (2: ExtPict Extend* ZWJ before p)
167fn joins(pp: u32, p: u32, ri: u32, emoji: u8) -> bool {
168    match (gcb(pp), gcb(p)) {
169        (G_CR, G_LF) => true,                                              // GB3
170        (G_CONTROL | G_CR | G_LF, _) | (_, G_CONTROL | G_CR | G_LF) => false, // GB4, GB5
171        (G_L, G_L | G_V | G_LV | G_LVT) => true,                           // GB6
172        (G_LV | G_V, G_V | G_T) => true,                                   // GB7
173        (G_LVT | G_T, G_T) => true,                                        // GB8
174        (_, G_EXTEND | G_ZWJ) => true,                                     // GB9
175        (_, G_SPACINGMARK) => true,                                        // GB9a
176        (G_PREPEND, _) => true,                                            // GB9b
177        _ if p & P_EXTPICT != 0 && emoji == 2 => true,                     // GB11
178        (G_RI, G_RI) => ri % 2 == 1,                                       // GB12, GB13
179        _ => false,                                                        // GB999
180    }
181}
182
183// the cluster boundaries of s after `from` (a boundary) in order: at(b) for each (s.len() last),
184// true stops
185fn grapheme_walk(s: &str, from: usize, at: &mut dyn FnMut(usize) -> bool) {
186    let mut prev: Option<u32> = None;
187    let mut ri = 0u32; // regional indicators in a row before this character
188    let mut emoji = 0u8; // 1: ExtPict Extend*, 2: ExtPict Extend* ZWJ
189    for (k, c) in s[from..].char_indices() {
190        let i = from + k;
191        let p = char_props(c);
192        let g = gcb(p);
193        if let Some(pp) = prev {
194            if !joins(pp, p, ri, emoji) && at(i) {
195                return;
196            }
197        }
198        // the state after this character
199        ri = if g == G_RI { ri + 1 } else { 0 };
200        emoji = if p & P_EXTPICT != 0 {
201            1
202        } else if emoji != 0 && g == G_EXTEND {
203            1
204        } else if emoji == 1 && g == G_ZWJ {
205            2
206        } else {
207            0
208        };
209        prev = Some(p);
210    }
211    if from < s.len() {
212        at(s.len());
213    }
214}
215
216// a cluster boundary at or before i that a scan may start from: back from i the first place
217// whose two characters stay apart in every state (GB11 only after a ZWJ, GB12 only between
218// regional indicators), usually i itself; a scan from there sees the boundaries a scan from
219// the paragraph start sees (a scan from any boundary does: the state there is the same)
220fn cluster_start(s: &str, i: usize) -> usize {
221    let mut k = floor(s, i);
222    while k > 0 && k < s.len() {
223        let (Some(a), Some(c)) = (s[..k].chars().next_back(), s[k..].chars().next()) else { break };
224        let pp = char_props(a);
225        if !joins(pp, char_props(c), 1, if gcb(pp) == G_ZWJ { 2 } else { 0 }) {
226            break;
227        }
228        k -= a.len_utf8();
229    }
230    k
231}
232
233/// the end of the grapheme cluster that starts at or holds i (s.len() at the end)
234pub fn grapheme_next(s: &str, i: usize) -> usize {
235    let i = floor(s, i);
236    if i >= s.len() {
237        return s.len();
238    }
239    cluster_after(s, cluster_start(s, i), i)
240}
241
242/// the end of the grapheme cluster that starts at i, a cluster boundary (grapheme_next without
243/// looking back: walks from boundary to boundary)
244pub fn cluster_end(s: &str, i: usize) -> usize {
245    let i = floor(s, i);
246    let Some(c) = s.get(i..).and_then(|r| r.chars().next()) else { return s.len() };
247    let n = i + c.len_utf8();
248    // the common case: ASCII after a character that does not hold on to what follows (only a
249    // Prepend character or CR before LF join an ASCII character)
250    let pc = char_props(c);
251    match s.as_bytes().get(n) {
252        None => n,
253        Some(&b) if b < 0x80 && gcb(pc) != G_PREPEND && !(gcb(pc) == G_CR && b == b'\n') => n,
254        _ => cluster_after(s, i, i),
255    }
256}
257
258// the first cluster boundary after i, walking from boundary `from` <= i
259fn cluster_after(s: &str, from: usize, i: usize) -> usize {
260    let mut end = s.len();
261    grapheme_walk(s, from, &mut |b| {
262        end = b;
263        b > i
264    });
265    end
266}
267
268/// the start of the grapheme cluster before i (0 at the start)
269pub fn grapheme_prev(s: &str, i: usize) -> usize {
270    let i = floor(s, i);
271    if i == 0 {
272        return 0;
273    }
274    let from = cluster_start(s, i - 1);
275    let mut last = from;
276    grapheme_walk(s, from, &mut |b| {
277        if b < i {
278            last = b;
279        }
280        b >= i
281    });
282    last
283}
284
285/// i is a cluster boundary
286pub fn is_grapheme_boundary(s: &str, i: usize) -> bool {
287    if i == 0 || i >= s.len() {
288        return i <= s.len();
289    }
290    if !s.is_char_boundary(i) {
291        return false;
292    }
293    let from = cluster_start(s, i);
294    let mut hit = from == i;
295    if !hit {
296        grapheme_walk(s, from, &mut |b| {
297            hit = b == i;
298            b >= i
299        });
300    }
301    hit
302}
303
304/// every cluster boundary of s (0 and s.len() included)
305pub fn graphemes(s: &str) -> Vec<usize> {
306    let mut b = alloc::vec![0];
307    grapheme_walk(s, 0, &mut |x| {
308        b.push(x);
309        false
310    });
311    b
312}
313
314/// the cluster a..b is an emoji sequence (pictographs, flags, keycaps, ZWJ sequences)
315pub fn pictographic_cluster(s: &str, a: usize, b: usize) -> bool {
316    let t = &s[floor(s, a)..floor(s, b)];
317    t.chars().count() > 1
318        && t.chars().any(|c| {
319            let p = char_props(c);
320            p & P_EXTPICT != 0 || gcb(p) == G_RI || gcb(p) == G_ZWJ || c == '\u{20E3}' || c == '\u{FE0F}'
321        })
322}
323
324// ------------------------------------------------------------------ words
325
326fn ignorable(w: u32) -> bool {
327    matches!(w, W_EXTEND | W_FORMAT | W_ZWJ)
328}
329
330fn ahletter(w: u32) -> bool {
331    w == W_ALETTER || w == W_HL
332}
333
334fn midnumletq(w: u32) -> bool {
335    w == W_MIDNUMLET || w == W_SQ
336}
337
338/// every word boundary of s (0 and s.len() included)
339pub fn word_bounds(s: &str) -> Vec<usize> {
340    let ch: Vec<(usize, u32)> = s.char_indices().map(|(i, c)| (i, char_props(c))).collect();
341    let n = ch.len();
342    let w = |k: usize| wb(ch[k].1);
343    // the non-ignorable character before index k (WB4), and the one after k
344    let before = |k: usize| -> Option<usize> { (0..k).rev().find(|&j| !ignorable(w(j))) };
345    let after = |k: usize| -> Option<usize> { (k + 1..n).find(|&j| !ignorable(w(j))) };
346    let mut out = alloc::vec![0];
347    for k in 1..n {
348        let a = w(k - 1);
349        let b = w(k);
350        let keep = if a == W_CR && b == W_LF {
351            true // WB3
352        } else if matches!(a, W_NEWLINE | W_CR | W_LF) || matches!(b, W_NEWLINE | W_CR | W_LF) {
353            false // WB3a, WB3b
354        } else if a == W_ZWJ && ch[k].1 & P_EXTPICT != 0 {
355            true // WB3c
356        } else if a == W_WSEG && b == W_WSEG {
357            true // WB3d
358        } else if ignorable(b) {
359            true // WB4
360        } else {
361            // the rules see the characters without Extend / Format / ZWJ
362            let p1 = before(k);
363            let x = p1.map_or(a, w);
364            let x0 = p1.and_then(before).map(w); // the one before x
365            let y = b;
366            let y2 = after(k).map(w); // the one after y
367            let ri_odd = || {
368                let mut c = 0;
369                let mut j = p1;
370                while let Some(jj) = j {
371                    if w(jj) != W_RI {
372                        break;
373                    }
374                    c += 1;
375                    j = before(jj);
376                }
377                c % 2 == 1
378            };
379            (ahletter(x) && ahletter(y))                                                      // WB5
380                || (ahletter(x) && (y == W_MIDLETTER || midnumletq(y)) && y2.is_some_and(ahletter)) // WB6
381                || (x0.is_some_and(ahletter) && (x == W_MIDLETTER || midnumletq(x)) && ahletter(y)) // WB7
382                || (x == W_HL && y == W_SQ)                                                   // WB7a
383                || (x == W_HL && y == W_DQ && y2 == Some(W_HL))                               // WB7b
384                || (x0 == Some(W_HL) && x == W_DQ && y == W_HL)                               // WB7c
385                || (x == W_NUMERIC && y == W_NUMERIC)                                         // WB8
386                || (ahletter(x) && y == W_NUMERIC)                                            // WB9
387                || (x == W_NUMERIC && ahletter(y))                                            // WB10
388                || (x0 == Some(W_NUMERIC) && (x == W_MIDNUM || midnumletq(x)) && y == W_NUMERIC) // WB11
389                || (x == W_NUMERIC && (y == W_MIDNUM || midnumletq(y)) && y2 == Some(W_NUMERIC)) // WB12
390                || (x == W_KATAKANA && y == W_KATAKANA)                                       // WB13
391                || ((ahletter(x) || matches!(x, W_NUMERIC | W_KATAKANA | W_EXTENDNUMLET)) && y == W_EXTENDNUMLET) // WB13a
392                || (x == W_EXTENDNUMLET && (ahletter(y) || matches!(y, W_NUMERIC | W_KATAKANA))) // WB13b
393                || (x == W_RI && y == W_RI && ri_odd())                                       // WB15, WB16
394        };
395        if !keep {
396            out.push(ch[k].0);
397        }
398    }
399    if s.is_empty() {
400        return out;
401    }
402    out.push(s.len());
403    out
404}
405
406/// the kind of the segment a..b (WK_WORD, WK_SPACE, WK_PUNCT)
407pub fn word_kind(s: &str, a: usize, b: usize) -> u32 {
408    let t = &s[floor(s, a)..floor(s, b)];
409    let mut space = !t.is_empty();
410    for c in t.chars() {
411        let p = char_props(c);
412        // letters, numbers and the joiners of a word (_, ' in can't) make a word
413        if p & P_ALNUM != 0 || wb(p) == W_EXTENDNUMLET || matches!(lbc(p), L_ID | L_H2 | L_H3 | L_JL | L_JV | L_JT) {
414            return WK_WORD;
415        }
416        if p & P_SPACE == 0 && !ignorable(wb(p)) {
417            space = false;
418        }
419    }
420    if space { WK_SPACE } else { WK_PUNCT }
421}
422
423// a word boundary at k (a character start inside s) that no rule looks across: neither
424// character is ignored (WB4); a line break on one side, or Other or white space next to
425// something else -- every rule that keeps two characters together or looks past one needs
426// letters, digits, joiners, quotes or regional indicators on both sides
427fn word_hard(s: &str, k: usize) -> bool {
428    let (Some(a), Some(b)) = (s[..k].chars().next_back(), s[k..].chars().next()) else { return true };
429    let (x, y) = (wb(char_props(a)), wb(char_props(b)));
430    if ignorable(x) || ignorable(y) {
431        return false;
432    }
433    if matches!(x, W_NEWLINE | W_CR | W_LF) || matches!(y, W_NEWLINE | W_CR | W_LF) {
434        return !(x == W_CR && y == W_LF);
435    }
436    x == 0 || y == 0 || (x == W_WSEG) != (y == W_WSEG)
437}
438
439// how far word_window looks for a boundary no rule crosses before it takes the paragraph
440const WORD_REACH: usize = 256;
441
442// the word segments around i: (bounds, offset) between boundaries no rule looks across (a word
443// scan from there finds the bounds a scan of the whole paragraph finds), else the paragraph
444fn para_words(s: &str, i: usize) -> (Vec<usize>, usize) {
445    let mut a = i;
446    let mut n = 0;
447    while a > 0 && !word_hard(s, a) && n < WORD_REACH {
448        a = floor(s, a - 1);
449        n += 1;
450    }
451    if a > 0 && !word_hard(s, a) {
452        a = para_start(s, i);
453    }
454    let mut e = i + s[i..].chars().next().map_or(0, |c| c.len_utf8());
455    n = 0;
456    while e < s.len() && !word_hard(s, e) && n < WORD_REACH {
457        e += s[e..].chars().next().map_or(1, |c| c.len_utf8());
458        n += 1;
459    }
460    if e < s.len() && !word_hard(s, e) {
461        e = s.as_bytes()[i..].iter().position(|&b| b == b'\n').map_or(s.len(), |k| i + k + 1);
462    }
463    (word_bounds(&s[a..e]).into_iter().map(|x| x + a).collect(), a)
464}
465
466/// the word segment that holds i (at the end of the text: the last one)
467pub fn word_at(s: &str, i: usize) -> (usize, usize) {
468    let i = floor(s, i);
469    if s.is_empty() {
470        return (0, 0);
471    }
472    let i = if i >= s.len() { floor(s, s.len() - 1) } else { i };
473    let (b, _) = para_words(s, i);
474    let k = b.partition_point(|&x| x <= i);
475    let start = b[k.saturating_sub(1)];
476    let end = b.get(k).copied().unwrap_or(s.len());
477    (start, end)
478}
479
480/// Ctrl+Right: the start of the next word after i (the rest of this word and the spaces after
481/// it skipped)
482pub fn word_next(s: &str, i: usize) -> usize {
483    let mut p = floor(s, i);
484    if p >= s.len() {
485        return s.len();
486    }
487    let (_, e) = word_at(s, p);
488    p = e;
489    while p < s.len() {
490        let (a, e) = word_at(s, p);
491        if word_kind(s, a, e) != WK_SPACE || s[a..e].contains('\n') {
492            break;
493        }
494        p = e;
495    }
496    p
497}
498
499/// Ctrl+Left: the start of the word before i (spaces before i skipped)
500pub fn word_prev(s: &str, i: usize) -> usize {
501    let mut p = floor(s, i);
502    while p > 0 {
503        let (a, e) = word_at(s, p - 1);
504        if word_kind(s, a, e) != WK_SPACE || s[a..e].contains('\n') {
505            return a;
506        }
507        p = a;
508    }
509    0
510}
511
512// ------------------------------------------------------------------ line breaks (UAX #14)
513
514/// the break opportunities of s: (index where a line may start, mandatory); the end of the
515/// text is the last entry (mandatory)
516pub fn line_breaks(s: &str) -> Vec<(usize, bool)> {
517    let ch: Vec<(usize, u32)> = s.char_indices().map(|(i, c)| (i, char_props(c))).collect();
518    let n = ch.len();
519    let mut out = Vec::new();
520    if n == 0 {
521        return out;
522    }
523    // LB9 / LB10: combining marks and ZWJ take the class of their base; alone they are AL
524    let mut cls: Vec<u32> = Vec::with_capacity(n);
525    let mut attached: Vec<bool> = Vec::with_capacity(n);
526    for k in 0..n {
527        let c = lbc(ch[k].1);
528        if matches!(c, L_CM | L_ZWJ) {
529            if let Some(&base) = cls.last() {
530                if !matches!(base, L_BK | L_CR | L_LF | L_NL | L_SP | L_ZW) {
531                    cls.push(base);
532                    attached.push(true);
533                    continue;
534                }
535            }
536            cls.push(L_AL);
537            attached.push(false);
538            continue;
539        }
540        cls.push(c);
541        attached.push(false);
542    }
543    let wide = |k: usize| ch[k].1 & P_WIDE != 0;
544    let mut ri = 0u32; // regional indicators in a row before k (attached marks skipped)
545    let mut sp_before: Option<u32> = None; // the class before a run of spaces ending at k-1
546    for k in 1..n {
547        let a = cls[k - 1];
548        let b = cls[k];
549        let raw_a = lbc(ch[k - 1].1);
550        // the class before the spaces (LB8, LB14 .. LB17)
551        if a == L_SP {
552            if k >= 2 && cls[k - 2] != L_SP {
553                sp_before = Some(cls[k - 2]);
554            } else if k < 2 {
555                sp_before = None;
556            }
557        } else {
558            sp_before = None;
559        }
560        if cls[k - 1] == L_RI && !attached[k - 1] {
561            ri += 1;
562        } else if !attached[k - 1] {
563            ri = 0;
564        }
565        // mandatory breaks (LB4, LB5)
566        let mandatory = match a {
567            L_BK | L_NL | L_LF => true,
568            L_CR => b != L_LF,
569            _ => false,
570        };
571        if mandatory {
572            out.push((ch[k].0, true));
573            continue;
574        }
575        let brk = if a == L_CR && b == L_LF {
576            false // LB5
577        } else if matches!(b, L_BK | L_CR | L_LF | L_NL) {
578            false // LB6
579        } else if b == L_SP || b == L_ZW {
580            false // LB7
581        } else if a == L_ZW || (a == L_SP && sp_before == Some(L_ZW)) {
582            true // LB8
583        } else if raw_a == L_ZWJ {
584            false // LB8a
585        } else if attached[k] {
586            false // LB9
587        } else if a == L_WJ || b == L_WJ {
588            false // LB11
589        } else if a == L_GL {
590            false // LB12
591        } else if b == L_GL && !matches!(a, L_SP | L_BA | L_HY) {
592            false // LB12a
593        } else if matches!(b, L_CL | L_CP | L_EX | L_IS | L_SY) {
594            false // LB13
595        } else if a == L_OP || (a == L_SP && sp_before == Some(L_OP)) {
596            false // LB14
597        } else if b == L_OP && (a == L_QU || (a == L_SP && sp_before == Some(L_QU))) {
598            false // LB15
599        } else if b == L_NS && (matches!(a, L_CL | L_CP) || (a == L_SP && matches!(sp_before, Some(L_CL | L_CP)))) {
600            false // LB16
601        } else if b == L_B2 && (a == L_B2 || (a == L_SP && sp_before == Some(L_B2))) {
602            false // LB17
603        } else if a == L_SP {
604            true // LB18
605        } else if a == L_QU || b == L_QU {
606            false // LB19
607        } else if a == L_CB || b == L_CB {
608            true // LB20
609        } else if matches!(b, L_BA | L_HY | L_NS) || a == L_BB {
610            false // LB21
611        } else if matches!(a, L_HY | L_BA) && k >= 2 && cls[k - 2] == L_HL {
612            false // LB21a
613        } else if a == L_SY && b == L_HL {
614            false // LB21b
615        } else if b == L_IN {
616            false // LB22
617        } else if (matches!(a, L_AL | L_HL) && b == L_NU) || (a == L_NU && matches!(b, L_AL | L_HL)) {
618            false // LB23
619        } else if (a == L_PR && matches!(b, L_ID | L_EB | L_EM)) || (matches!(a, L_ID | L_EB | L_EM) && b == L_PO) {
620            false // LB23a
621        } else if (matches!(a, L_PR | L_PO) && matches!(b, L_AL | L_HL)) || (matches!(a, L_AL | L_HL) && matches!(b, L_PR | L_PO)) {
622            false // LB24
623        } else if matches!(
624            (a, b),
625            (L_CL | L_CP | L_NU, L_PO | L_PR) | (L_PO | L_PR, L_OP | L_NU) | (L_HY | L_IS | L_NU | L_SY, L_NU)
626        ) {
627            false // LB25 (the pair rules)
628        } else if (a == L_JL && matches!(b, L_JL | L_JV | L_H2 | L_H3))
629            || (matches!(a, L_JV | L_H2) && matches!(b, L_JV | L_JT))
630            || (matches!(a, L_JT | L_H3) && b == L_JT)
631        {
632            false // LB26
633        } else if (matches!(a, L_JL | L_JV | L_JT | L_H2 | L_H3) && b == L_PO) || (a == L_PR && matches!(b, L_JL | L_JV | L_JT | L_H2 | L_H3)) {
634            false // LB27
635        } else if matches!(a, L_AL | L_HL) && matches!(b, L_AL | L_HL) {
636            false // LB28
637        } else if a == L_IS && matches!(b, L_AL | L_HL) {
638            false // LB29
639        } else if (matches!(a, L_AL | L_HL | L_NU) && b == L_OP && !wide(k)) || (a == L_CP && !wide(k - 1) && matches!(b, L_AL | L_HL | L_NU)) {
640            false // LB30
641        } else if a == L_RI && b == L_RI && ri % 2 == 1 {
642            false // LB30a
643        } else if (a == L_EB || (ch[k - 1].1 & P_EXTPICT != 0 && ch[k - 1].1 & P_UNASSIGNED != 0)) && b == L_EM {
644            false // LB30b
645        } else {
646            true // LB31
647        };
648        if brk {
649            out.push((ch[k].0, false));
650        }
651    }
652    out.push((s.len(), true));
653    out
654}
655
656/// where a line starting at pos ends inside pos..pe (pos < end <= pe; pe when pos == pe): the
657/// whole rest when it fits, else the last break opportunity whose text before it (without
658/// trailing spaces) fits, else grapheme clusters while they fit (at least one); a mandatory
659/// break inside ends the line there.  fits(a, b): the text a..b fits.
660pub fn wrap_end(s: &str, pos: usize, pe: usize, fits: &mut dyn FnMut(usize, usize) -> bool) -> usize {
661    let pe = floor(s, pe);
662    if pos >= pe {
663        return pe;
664    }
665    let seg = &s[pos..pe];
666    let ops = line_breaks(seg);
667    // a mandatory break before the end
668    let hard = ops.iter().find(|o| o.1 && pos + o.0 < pe).map(|o| pos + o.0);
669    let limit = hard.unwrap_or(pe);
670    let visible = |e: usize| {
671        let mut v = e;
672        while v > pos {
673            let c = s[..v].chars().next_back().unwrap_or('a');
674            if char_props(c) & P_SPACE == 0 {
675                break;
676            }
677            v -= c.len_utf8();
678        }
679        v
680    };
681    if fits(pos, visible(limit)) {
682        return limit;
683    }
684    let mut best = pos;
685    for &(o, _) in &ops {
686        let e = pos + o;
687        if e > limit || e == pos {
688            continue;
689        }
690        if !fits(pos, visible(e)) {
691            break;
692        }
693        best = e;
694    }
695    if best > pos {
696        return best;
697    }
698    // one long piece: grapheme clusters while they fit (at least one)
699    let mut best = grapheme_next(s, pos).min(limit);
700    while best < limit {
701        let n = grapheme_next(s, best).min(limit);
702        if !fits(pos, n) {
703            break;
704        }
705        best = n;
706    }
707    best
708}
709
710/// wrap_end with hyphenation: where a line starting at pos ends inside pos..pe, and whether
711/// it ends with a hyphen (to be drawn after its text).  As wrap_end, and: an opportunity
712/// after a soft hyphen (U+00AD) counts with the hyphen's width; when the next word does not
713/// fit, hyph(word, &mut offsets) gives the places where that whole word (UAX #29) may break
714/// (byte offsets inside it, ascending) and the line ends at the last one that fits with a
715/// hyphen -- also inside a single word too long for a line, before graphemes are cut.  Words
716/// with a digit or a soft hyphen of their own are never hyphenated.  fits(a, b, hyphen):
717/// the text a..b fits (with a hyphen after it).
718pub fn wrap_end_hyph(
719    s: &str,
720    pos: usize,
721    pe: usize,
722    fits: &mut dyn FnMut(usize, usize, bool) -> bool,
723    mut hyph: Option<&mut dyn FnMut(&str, &mut Vec<usize>)>,
724) -> (usize, bool) {
725    let pe = floor(s, pe);
726    if pos >= pe {
727        return (pe, false);
728    }
729    let seg = &s[pos..pe];
730    let ops = line_breaks(seg);
731    let hard = ops.iter().find(|o| o.1 && pos + o.0 < pe).map(|o| pos + o.0);
732    let limit = hard.unwrap_or(pe);
733    let visible = |e: usize| {
734        let mut v = e;
735        while v > pos {
736            let c = s[..v].chars().next_back().unwrap_or('a');
737            if char_props(c) & P_SPACE == 0 {
738                break;
739            }
740            v -= c.len_utf8();
741        }
742        v
743    };
744    let shy = |e: usize| s[..e].ends_with('\u{AD}');
745    if fits(pos, visible(limit), false) {
746        return (limit, false);
747    }
748    // the last opportunity that fits (after a soft hyphen: with the hyphen); the piece after
749    // it ends at the next opportunity
750    let mut best = pos;
751    let mut best_h = false;
752    let mut next = limit;
753    for &(o, _) in &ops {
754        let e = pos + o;
755        if e > limit || e == pos {
756            continue;
757        }
758        let h = shy(e);
759        if fits(pos, visible(e), h) {
760            best = e;
761            best_h = h;
762        } else if !h {
763            next = e;
764            break;
765        }
766    }
767    if next <= best {
768        next = ops.iter().map(|o| pos + o.0).find(|&e| e > best && e <= limit).unwrap_or(limit);
769    }
770    // hyphenation inside the words of the piece best..next
771    if let Some(h) = hyph.as_mut() {
772        let mut words: Vec<(usize, usize)> = Vec::new();
773        let mut q = best;
774        while q < next {
775            let (a, b) = word_at(s, q);
776            if b <= q {
777                break;
778            }
779            let w = &s[a..b];
780            if word_kind(s, a, b) == WK_WORD && !w.contains('\u{AD}') && !w.chars().any(|c| c.is_numeric()) {
781                words.push((a, b));
782            }
783            q = b;
784        }
785        let mut offs = Vec::new();
786        for &(a, b) in words.iter().rev() {
787            offs.clear();
788            h(&s[a..b], &mut offs);
789            for &o in offs.iter().rev() {
790                let e = a + o;
791                if e > pos && e > best && e < b && e <= next && s.is_char_boundary(e) && fits(pos, e, true) {
792                    return (e, true);
793                }
794            }
795        }
796    }
797    if best > pos {
798        return (best, best_h);
799    }
800    // one long piece: grapheme clusters while they fit (at least one)
801    let mut end = grapheme_next(s, pos).min(limit);
802    while end < limit {
803        let n = grapheme_next(s, end).min(limit);
804        if !fits(pos, n, false) {
805            break;
806        }
807        end = n;
808    }
809    (end, false)
810}
811
812/// the Unicode version of the tables
813pub fn unicode_version() -> &'static str {
814    crate::segment_data::UNICODE_VERSION
815}