Skip to main content

uax14_linebreak/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3#![deny(missing_docs)]
4
5//! Unicode 17.0.0 line break opportunities.
6
7use core::iter::FusedIterator;
8use core::str::CharIndices;
9
10mod unicode_tables;
11
12/// Unicode version of the property tables.
13pub const UNICODE_VERSION: (u8, u8, u8) = (17, 0, 0);
14
15/// A Unicode `Line_Break` property value.
16#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
17#[non_exhaustive]
18#[repr(u8)]
19pub enum BreakClass {
20    /// Ambiguous.
21    Ambiguous,
22    /// Aksara.
23    Aksara,
24    /// Alphabetic.
25    Alphabetic,
26    /// Aksara prebase.
27    AksaraPrebase,
28    /// Aksara start.
29    AksaraStart,
30    /// Break on either side.
31    BreakBoth,
32    /// Break after.
33    BreakAfter,
34    /// Break before.
35    BreakBefore,
36    /// Mandatory break.
37    MandatoryBreak,
38    /// Contingent break.
39    ContingentBreak,
40    /// Conditional Japanese starter.
41    ConditionalJapaneseStarter,
42    /// Closing punctuation.
43    ClosePunctuation,
44    /// Combining mark.
45    CombiningMark,
46    /// Closing parenthesis.
47    CloseParenthesis,
48    /// Carriage return.
49    CarriageReturn,
50    /// Emoji base.
51    EBase,
52    /// Emoji modifier.
53    EModifier,
54    /// Exclamation or interrogation mark.
55    Exclamation,
56    /// Nonbreaking glue.
57    Glue,
58    /// Hangul LV syllable.
59    H2,
60    /// Hangul LVT syllable.
61    H3,
62    /// Unambiguous hyphen.
63    UnambiguousHyphen,
64    /// Hebrew letter.
65    HebrewLetter,
66    /// Hyphen.
67    Hyphen,
68    /// Ideographic.
69    Ideographic,
70    /// Inseparable.
71    Inseparable,
72    /// Infix numeric separator.
73    InfixNumeric,
74    /// Hangul leading jamo.
75    JL,
76    /// Hangul trailing jamo.
77    JT,
78    /// Hangul vowel jamo.
79    JV,
80    /// Line feed.
81    LineFeed,
82    /// Next line.
83    NextLine,
84    /// Nonstarter.
85    Nonstarter,
86    /// Numeric.
87    Numeric,
88    /// Opening punctuation.
89    OpenPunctuation,
90    /// Postfix numeric.
91    PostfixNumeric,
92    /// Prefix numeric.
93    PrefixNumeric,
94    /// Quotation mark.
95    Quotation,
96    /// Regional indicator.
97    RegionalIndicator,
98    /// Complex-context character.
99    ComplexContext,
100    /// Surrogate.
101    Surrogate,
102    /// Space.
103    Space,
104    /// Break symbol.
105    BreakSymbols,
106    /// Final virama.
107    ViramaFinal,
108    /// Virama.
109    Virama,
110    /// Word joiner.
111    WordJoiner,
112    /// Unknown.
113    Unknown,
114    /// Zero-width space.
115    ZWSpace,
116    /// Zero-width joiner.
117    ZWJ,
118}
119
120/// A line break opportunity.
121#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
122pub enum BreakOpportunity {
123    /// A required line break.
124    Mandatory,
125    /// A permitted line break.
126    Allowed,
127}
128
129/// Returns the raw `Line_Break` property of a code point.
130///
131/// Values above `U+10FFFF` return [`BreakClass::Unknown`].
132pub fn break_property(codepoint: u32) -> BreakClass {
133    property(codepoint).class.public()
134}
135
136/// An iterator over line break opportunities in a string.
137#[derive(Clone, Debug)]
138pub struct Linebreaks<'a> {
139    inner: LinebreaksIter<usize, CharIndices<'a>>,
140}
141
142impl Iterator for Linebreaks<'_> {
143    type Item = (usize, BreakOpportunity);
144
145    fn next(&mut self) -> Option<Self::Item> {
146        self.inner.next()
147    }
148
149    fn size_hint(&self) -> (usize, Option<usize>) {
150        self.inner.size_hint()
151    }
152}
153
154impl FusedIterator for Linebreaks<'_> {}
155
156/// Returns the line break opportunities in `text`.
157pub fn linebreaks(text: &str) -> Linebreaks<'_> {
158    Linebreaks {
159        inner: linebreaks_iter(text.char_indices(), text.len()),
160    }
161}
162
163/// An iterator over line break opportunities in indexed characters.
164///
165/// The caller supplies the value reported for the end of the input. Indices
166/// pass through unchanged and do not need to be numeric.
167#[derive(Clone, Debug)]
168pub struct LinebreaksIter<I, It>
169where
170    It: Iterator<Item = (I, char)> + Clone,
171    I: Clone,
172{
173    chars: It,
174    eot: I,
175    state: Option<State>,
176    finished: bool,
177}
178
179/// Returns line break opportunities from indexed characters.
180pub fn linebreaks_iter<I, It>(chars: It, eot: I) -> LinebreaksIter<I, It>
181where
182    It: Iterator<Item = (I, char)> + Clone,
183    I: Clone,
184{
185    LinebreaksIter {
186        chars,
187        eot,
188        state: None,
189        finished: false,
190    }
191}
192
193impl<I, It> Iterator for LinebreaksIter<I, It>
194where
195    It: Iterator<Item = (I, char)> + Clone,
196    I: Clone,
197{
198    type Item = (I, BreakOpportunity);
199
200    fn next(&mut self) -> Option<Self::Item> {
201        if self.finished {
202            return None;
203        }
204
205        if self.state.is_none() {
206            let (_, first) = match self.chars.next() {
207                Some(first) => first,
208                None => {
209                    self.finished = true;
210                    return None;
211                }
212            };
213            let raw = resolved_property(first as u32);
214            let significant = normalize(raw, None).expect("initial property is significant");
215            self.state = Some(State::new(significant, raw.class == Class::Zwj));
216        }
217
218        loop {
219            let (index, character) = match self.chars.next() {
220                Some(value) => value,
221                None => {
222                    self.finished = true;
223                    return Some((self.eot.clone(), BreakOpportunity::Mandatory));
224                }
225            };
226            let raw = resolved_property(character as u32);
227            let state = self.state.as_mut().expect("state was initialized");
228            let right = match normalize(raw, Some(state.last)) {
229                Some(right) => right,
230                None => {
231                    state.last_was_zwj = raw.class == Class::Zwj;
232                    continue;
233                }
234            };
235            let raw_zwj = raw.class == Class::Zwj;
236            let opportunity = match decide_pair(state, right, raw_zwj) {
237                Ok(opportunity) => opportunity,
238                Err(()) => {
239                    let lookahead = if needs_lookahead(state, right) {
240                        significant_lookahead(self.chars.clone(), right)
241                    } else {
242                        [None, None]
243                    };
244                    let opportunity = decide(state, right, lookahead);
245                    state.push(right, raw_zwj);
246                    opportunity
247                }
248            };
249            if let Some(opportunity) = opportunity {
250                return Some((index, opportunity));
251            }
252        }
253    }
254
255    fn size_hint(&self) -> (usize, Option<usize>) {
256        if self.finished {
257            (0, Some(0))
258        } else {
259            (
260                0,
261                self.chars
262                    .size_hint()
263                    .1
264                    .and_then(|count| count.checked_add(1)),
265            )
266        }
267    }
268}
269
270impl<I, It> FusedIterator for LinebreaksIter<I, It>
271where
272    It: Iterator<Item = (I, char)> + Clone + FusedIterator,
273    I: Clone,
274{
275}
276
277/// Splits a string at its largest safe restart point.
278///
279/// The second slice can be analyzed without carrying line-breaking state from
280/// the first slice. The selected point is always below `text.len()`.
281pub fn split_at_safe(text: &str) -> (&str, &str) {
282    let split = text
283        .char_indices()
284        .rev()
285        .find(|&(index, character)| {
286            index < text.len() && is_safe_restart(property(character as u32))
287        })
288        .map(|(index, _)| index)
289        .unwrap_or(0);
290    text.split_at(split)
291}
292
293#[derive(Copy, Clone, Debug, Eq, PartialEq)]
294struct Property {
295    class: Class,
296    flags: u8,
297}
298
299impl Property {
300    const EAST_ASIAN: u8 = 1;
301    const PI: u8 = 2;
302    const PF: u8 = 4;
303    const EXTENDED_PICTOGRAPHIC_CN: u8 = 8;
304    const DOTTED_CIRCLE: u8 = 16;
305    const COMBINING_CATEGORY: u8 = 32;
306
307    fn has(self, flag: u8) -> bool {
308        self.flags & flag != 0
309    }
310
311    fn is_aksara_or_dotted(self) -> bool {
312        matches!(self.class, Class::Ak | Class::As) || self.has(Self::DOTTED_CIRCLE)
313    }
314}
315
316#[derive(Copy, Clone, Debug, Eq, PartialEq)]
317#[repr(u8)]
318enum Class {
319    Ambiguous,
320    Ak,
321    Al,
322    Ap,
323    As,
324    B2,
325    Ba,
326    Bb,
327    Bk,
328    Cb,
329    Cj,
330    Cl,
331    Cm,
332    Cp,
333    Cr,
334    Eb,
335    Em,
336    Ex,
337    Gl,
338    H2,
339    H3,
340    Hh,
341    Hl,
342    Hy,
343    Id,
344    In,
345    Is,
346    Jl,
347    Jt,
348    Jv,
349    Lf,
350    Nl,
351    Ns,
352    Nu,
353    Op,
354    Po,
355    Pr,
356    Qu,
357    Ri,
358    Sa,
359    Sg,
360    Sp,
361    Sy,
362    Vf,
363    Vi,
364    Wj,
365    Xx,
366    Zw,
367    Zwj,
368}
369
370impl Class {
371    fn from_u8(value: u8) -> Self {
372        match value {
373            0 => Self::Ambiguous,
374            1 => Self::Ak,
375            2 => Self::Al,
376            3 => Self::Ap,
377            4 => Self::As,
378            5 => Self::B2,
379            6 => Self::Ba,
380            7 => Self::Bb,
381            8 => Self::Bk,
382            9 => Self::Cb,
383            10 => Self::Cj,
384            11 => Self::Cl,
385            12 => Self::Cm,
386            13 => Self::Cp,
387            14 => Self::Cr,
388            15 => Self::Eb,
389            16 => Self::Em,
390            17 => Self::Ex,
391            18 => Self::Gl,
392            19 => Self::H2,
393            20 => Self::H3,
394            21 => Self::Hh,
395            22 => Self::Hl,
396            23 => Self::Hy,
397            24 => Self::Id,
398            25 => Self::In,
399            26 => Self::Is,
400            27 => Self::Jl,
401            28 => Self::Jt,
402            29 => Self::Jv,
403            30 => Self::Lf,
404            31 => Self::Nl,
405            32 => Self::Ns,
406            33 => Self::Nu,
407            34 => Self::Op,
408            35 => Self::Po,
409            36 => Self::Pr,
410            37 => Self::Qu,
411            38 => Self::Ri,
412            39 => Self::Sa,
413            40 => Self::Sg,
414            41 => Self::Sp,
415            42 => Self::Sy,
416            43 => Self::Vf,
417            44 => Self::Vi,
418            45 => Self::Wj,
419            46 => Self::Xx,
420            47 => Self::Zw,
421            48 => Self::Zwj,
422            _ => Self::Xx,
423        }
424    }
425
426    fn public(self) -> BreakClass {
427        match self {
428            Self::Ambiguous => BreakClass::Ambiguous,
429            Self::Ak => BreakClass::Aksara,
430            Self::Al => BreakClass::Alphabetic,
431            Self::Ap => BreakClass::AksaraPrebase,
432            Self::As => BreakClass::AksaraStart,
433            Self::B2 => BreakClass::BreakBoth,
434            Self::Ba => BreakClass::BreakAfter,
435            Self::Bb => BreakClass::BreakBefore,
436            Self::Bk => BreakClass::MandatoryBreak,
437            Self::Cb => BreakClass::ContingentBreak,
438            Self::Cj => BreakClass::ConditionalJapaneseStarter,
439            Self::Cl => BreakClass::ClosePunctuation,
440            Self::Cm => BreakClass::CombiningMark,
441            Self::Cp => BreakClass::CloseParenthesis,
442            Self::Cr => BreakClass::CarriageReturn,
443            Self::Eb => BreakClass::EBase,
444            Self::Em => BreakClass::EModifier,
445            Self::Ex => BreakClass::Exclamation,
446            Self::Gl => BreakClass::Glue,
447            Self::H2 => BreakClass::H2,
448            Self::H3 => BreakClass::H3,
449            Self::Hh => BreakClass::UnambiguousHyphen,
450            Self::Hl => BreakClass::HebrewLetter,
451            Self::Hy => BreakClass::Hyphen,
452            Self::Id => BreakClass::Ideographic,
453            Self::In => BreakClass::Inseparable,
454            Self::Is => BreakClass::InfixNumeric,
455            Self::Jl => BreakClass::JL,
456            Self::Jt => BreakClass::JT,
457            Self::Jv => BreakClass::JV,
458            Self::Lf => BreakClass::LineFeed,
459            Self::Nl => BreakClass::NextLine,
460            Self::Ns => BreakClass::Nonstarter,
461            Self::Nu => BreakClass::Numeric,
462            Self::Op => BreakClass::OpenPunctuation,
463            Self::Po => BreakClass::PostfixNumeric,
464            Self::Pr => BreakClass::PrefixNumeric,
465            Self::Qu => BreakClass::Quotation,
466            Self::Ri => BreakClass::RegionalIndicator,
467            Self::Sa => BreakClass::ComplexContext,
468            Self::Sg => BreakClass::Surrogate,
469            Self::Sp => BreakClass::Space,
470            Self::Sy => BreakClass::BreakSymbols,
471            Self::Vf => BreakClass::ViramaFinal,
472            Self::Vi => BreakClass::Virama,
473            Self::Wj => BreakClass::WordJoiner,
474            Self::Xx => BreakClass::Unknown,
475            Self::Zw => BreakClass::ZWSpace,
476            Self::Zwj => BreakClass::ZWJ,
477        }
478    }
479}
480
481#[derive(Copy, Clone, Debug)]
482struct State {
483    last: Property,
484    previous: Option<Property>,
485    space_anchor: Option<Property>,
486    pi_run: bool,
487    numeric: NumericState,
488    regional_indicator_odd: bool,
489    last_was_zwj: bool,
490}
491
492impl State {
493    #[inline(always)]
494    fn new(first: Property, raw_zwj: bool) -> Self {
495        Self {
496            last: first,
497            previous: None,
498            space_anchor: None,
499            pi_run: first.class == Class::Qu && first.has(Property::PI),
500            numeric: if first.class == Class::Nu {
501                NumericState::Number
502            } else {
503                NumericState::None
504            },
505            regional_indicator_odd: first.class == Class::Ri,
506            last_was_zwj: raw_zwj,
507        }
508    }
509
510    #[inline(always)]
511    fn push(&mut self, next: Property, raw_zwj: bool) {
512        let old_last = self.last;
513        let old_pi_run = self.pi_run;
514        if next.class == Class::Sp {
515            if old_last.class != Class::Sp {
516                self.space_anchor = Some(old_last);
517            }
518        } else {
519            self.space_anchor = None;
520        }
521        self.pi_run = if next.class == Class::Sp {
522            old_pi_run
523        } else if next.class == Class::Qu && next.has(Property::PI) {
524            pi_prefix(old_last.class)
525        } else {
526            false
527        };
528        self.numeric = match (self.numeric, next.class) {
529            (_, Class::Nu) => NumericState::Number,
530            (NumericState::Number, Class::Sy | Class::Is) => NumericState::Number,
531            (NumericState::Number, Class::Cl | Class::Cp) => NumericState::Closed,
532            _ => NumericState::None,
533        };
534        self.regional_indicator_odd = if next.class == Class::Ri {
535            !self.regional_indicator_odd
536        } else {
537            false
538        };
539        self.previous = Some(old_last);
540        self.last = next;
541        self.last_was_zwj = raw_zwj;
542    }
543
544    #[inline(always)]
545    fn push_common(&mut self, next: Property) {
546        self.last = next;
547        self.previous = None;
548        self.space_anchor = None;
549        self.pi_run = false;
550        self.numeric = NumericState::None;
551        self.regional_indicator_odd = false;
552        self.last_was_zwj = false;
553    }
554
555    #[inline(always)]
556    fn ends_with_spaced(&self, class: Class) -> bool {
557        self.last.class == class
558            || (self.last.class == Class::Sp
559                && self.space_anchor.map(|anchor| anchor.class) == Some(class))
560    }
561}
562
563#[derive(Copy, Clone, Debug, Eq, PartialEq)]
564enum NumericState {
565    None,
566    Number,
567    Closed,
568}
569
570#[inline(always)]
571fn property(codepoint: u32) -> Property {
572    if codepoint < 128 {
573        let packed = unicode_tables::ASCII_PROPERTY[codepoint as usize];
574        return Property {
575            class: Class::from_u8(packed as u8),
576            flags: (packed >> 8) as u8,
577        };
578    }
579    if (0x4e00..=0x9fff).contains(&codepoint) {
580        return Property {
581            class: Class::Id,
582            flags: Property::EAST_ASIAN,
583        };
584    }
585    if (0xac00..=0xd7a3).contains(&codepoint) {
586        return Property {
587            class: if (codepoint - 0xac00) % 28 == 0 {
588                Class::H2
589            } else {
590                Class::H3
591            },
592            flags: Property::EAST_ASIAN,
593        };
594    }
595    if matches!(
596        codepoint,
597        0x304a..=0x3062 | 0x3064..=0x3082 | 0x3084 | 0x3086 | 0x3088..=0x308d | 0x308f..=0x3094
598    ) {
599        return Property {
600            class: Class::Id,
601            flags: Property::EAST_ASIAN,
602        };
603    }
604    if codepoint == 0x3002 {
605        return Property {
606            class: Class::Cl,
607            flags: Property::EAST_ASIAN,
608        };
609    }
610    if codepoint == 0x0308 {
611        return Property {
612            class: Class::Cm,
613            flags: 0,
614        };
615    }
616    if codepoint > 0x10ffff {
617        return Property {
618            class: Class::Xx,
619            flags: 0,
620        };
621    }
622    let point = codepoint as usize;
623    let group = unicode_tables::GROUP_MAP[point >> 10] as usize;
624    let page = unicode_tables::GROUPS[group][(point >> 6) & 15] as usize;
625    let id = unicode_tables::PAGES[page][point & 63] as usize;
626    let packed = unicode_tables::PROPERTIES[id];
627    Property {
628        class: Class::from_u8(packed as u8),
629        flags: (packed >> 8) as u8,
630    }
631}
632
633#[inline(always)]
634fn resolved_property(codepoint: u32) -> Property {
635    let mut value = property(codepoint);
636    value.class = match value.class {
637        Class::Ambiguous | Class::Sg | Class::Xx => Class::Al,
638        Class::Sa if value.has(Property::COMBINING_CATEGORY) => Class::Cm,
639        Class::Sa => Class::Al,
640        Class::Cj => Class::Ns,
641        class => class,
642    };
643    value
644}
645
646#[inline(always)]
647fn normalize(value: Property, left: Option<Property>) -> Option<Property> {
648    if matches!(value.class, Class::Cm | Class::Zwj) {
649        if left
650            .map(|left| accepts_combining(left.class))
651            .unwrap_or(false)
652        {
653            return None;
654        }
655        return Some(Property {
656            class: Class::Al,
657            flags: 0,
658        });
659    }
660    Some(value)
661}
662
663#[inline(always)]
664fn accepts_combining(class: Class) -> bool {
665    !matches!(
666        class,
667        Class::Bk | Class::Cr | Class::Lf | Class::Nl | Class::Sp | Class::Zw
668    )
669}
670
671#[inline(always)]
672fn significant_lookahead<I, It>(mut chars: It, mut left: Property) -> [Option<Property>; 2]
673where
674    It: Iterator<Item = (I, char)>,
675{
676    let mut found = [None, None];
677    let mut count = 0;
678    while count < found.len() {
679        let (_, character) = match chars.next() {
680            Some(value) => value,
681            None => break,
682        };
683        let raw = resolved_property(character as u32);
684        if let Some(value) = normalize(raw, Some(left)) {
685            found[count] = Some(value);
686            left = value;
687            count += 1;
688        }
689    }
690    found
691}
692
693#[inline(always)]
694fn needs_lookahead(state: &State, right: Property) -> bool {
695    right.class == Class::Qu
696        || (state.last.class == Class::Sp && right.class == Class::Is)
697        || (matches!(state.last.class, Class::Po | Class::Pr) && right.class == Class::Op)
698        || (state.last.is_aksara_or_dotted() && right.is_aksara_or_dotted())
699}
700
701const PAIR_NO_BREAK: u8 = 0;
702const PAIR_ALLOWED: u8 = 1;
703const PAIR_MANDATORY: u8 = 2;
704const PAIR_CONTEXT: u8 = 3;
705
706static PAIR_BREAKS: [u8; 49 * 49] = build_pair_breaks();
707
708#[inline(always)]
709fn decide_pair(
710    state: &mut State,
711    right: Property,
712    raw_zwj: bool,
713) -> Result<Option<BreakOpportunity>, ()> {
714    use BreakOpportunity::{Allowed, Mandatory};
715    use Class::*;
716
717    let left = state.last;
718    if state.last_was_zwj {
719        state.push(right, raw_zwj);
720        return Ok(None);
721    }
722    if left.class == Sp {
723        let opportunity = if matches!(right.class, Bk | Cr | Lf | Nl | Sp | Zw) {
724            None
725        } else if state.ends_with_spaced(Zw) {
726            Some(Allowed)
727        } else if matches!(right.class, Wj | Cl | Cp | Ex | Sy)
728            || state.ends_with_spaced(Op)
729            || state.pi_run
730        {
731            None
732        } else if right.class == Qu {
733            if right.has(Property::PF) {
734                return Err(());
735            }
736            Some(Allowed)
737        } else if right.class == Is {
738            return Err(());
739        } else if (right.class == Ns && (state.ends_with_spaced(Cl) || state.ends_with_spaced(Cp)))
740            || (right.class == B2 && state.ends_with_spaced(B2))
741        {
742            None
743        } else {
744            Some(Allowed)
745        };
746        state.push(right, raw_zwj);
747        return Ok(opportunity);
748    }
749    if matches!(left.class, Al | Hl) && matches!(right.class, Al | Hl) {
750        state.push_common(right);
751        return Ok(None);
752    }
753    if (left.class == Id && right.class == Id)
754        || (matches!(left.class, H2 | H3) && matches!(right.class, H2 | H3))
755    {
756        state.push_common(right);
757        return Ok(Some(Allowed));
758    }
759    let opportunity = match PAIR_BREAKS[left.class as usize * 49 + right.class as usize] {
760        PAIR_NO_BREAK => None,
761        PAIR_ALLOWED => Some(Allowed),
762        PAIR_MANDATORY => Some(Mandatory),
763        _ => return Err(()),
764    };
765    state.push(right, raw_zwj);
766    Ok(opportunity)
767}
768
769const fn build_pair_breaks() -> [u8; 49 * 49] {
770    let mut table = [PAIR_CONTEXT; 49 * 49];
771    let mut left = 0;
772    while left < 49 {
773        let mut right = 0;
774        while right < 49 {
775            table[left * 49 + right] = pair_break(left as u8, right as u8);
776            right += 1;
777        }
778        left += 1;
779    }
780    table
781}
782
783const fn pair_break(left: u8, right: u8) -> u8 {
784    use Class::*;
785
786    if pair_has_context(left, right) {
787        return PAIR_CONTEXT;
788    }
789    if left == Bk as u8
790        || (left == Cr as u8 && right != Lf as u8)
791        || left == Lf as u8
792        || left == Nl as u8
793    {
794        return PAIR_MANDATORY;
795    }
796    if (left == Cr as u8 && right == Lf as u8)
797        || right == Bk as u8
798        || right == Cr as u8
799        || right == Lf as u8
800        || right == Nl as u8
801        || right == Sp as u8
802        || right == Zw as u8
803    {
804        return PAIR_NO_BREAK;
805    }
806    if left == Zw as u8 {
807        return PAIR_ALLOWED;
808    }
809    if left == Wj as u8 || right == Wj as u8 || left == Gl as u8 {
810        return PAIR_NO_BREAK;
811    }
812    if right == Gl as u8
813        && left != Sp as u8
814        && left != Ba as u8
815        && left != Hy as u8
816        && left != Hh as u8
817    {
818        return PAIR_NO_BREAK;
819    }
820    if right == Cl as u8 || right == Cp as u8 || right == Ex as u8 || right == Sy as u8 {
821        return PAIR_NO_BREAK;
822    }
823    if left == Op as u8 || right == Is as u8 {
824        return PAIR_NO_BREAK;
825    }
826    if (right == Ns as u8 && (left == Cl as u8 || left == Cp as u8))
827        || (left == B2 as u8 && right == B2 as u8)
828    {
829        return PAIR_NO_BREAK;
830    }
831    if left == Cb as u8 || right == Cb as u8 {
832        return PAIR_ALLOWED;
833    }
834    if right == Ba as u8
835        || right == Hh as u8
836        || right == Hy as u8
837        || right == Ns as u8
838        || left == Bb as u8
839        || (left == Sy as u8 && right == Hl as u8)
840        || right == In as u8
841    {
842        return PAIR_NO_BREAK;
843    }
844    if ((left == Al as u8 || left == Hl as u8) && right == Nu as u8)
845        || (left == Nu as u8 && (right == Al as u8 || right == Hl as u8))
846        || (left == Pr as u8 && (right == Id as u8 || right == Eb as u8 || right == Em as u8))
847        || ((left == Id as u8 || left == Eb as u8 || left == Em as u8) && right == Po as u8)
848        || ((left == Pr as u8 || left == Po as u8) && (right == Al as u8 || right == Hl as u8))
849        || ((left == Al as u8 || left == Hl as u8) && (right == Pr as u8 || right == Po as u8))
850        || ((left == Po as u8 || left == Pr as u8) && right == Nu as u8)
851        || ((left == Hy as u8 || left == Is as u8) && right == Nu as u8)
852        || (left == Nu as u8 && right == Nu as u8)
853    {
854        return PAIR_NO_BREAK;
855    }
856    if (left == Jl as u8
857        && (right == Jl as u8 || right == Jv as u8 || right == H2 as u8 || right == H3 as u8))
858        || ((left == Jv as u8 || left == H2 as u8) && (right == Jv as u8 || right == Jt as u8))
859        || ((left == Jt as u8 || left == H3 as u8) && right == Jt as u8)
860        || ((left == Jl as u8
861            || left == Jv as u8
862            || left == Jt as u8
863            || left == H2 as u8
864            || left == H3 as u8)
865            && right == Po as u8)
866        || (left == Pr as u8
867            && (right == Jl as u8
868                || right == Jv as u8
869                || right == Jt as u8
870                || right == H2 as u8
871                || right == H3 as u8))
872        || ((left == Al as u8 || left == Hl as u8) && (right == Al as u8 || right == Hl as u8))
873        || (left == Ap as u8 && (right == Ak as u8 || right == As as u8))
874        || ((left == Ak as u8 || left == As as u8) && (right == Vf as u8 || right == Vi as u8))
875        || (left == Is as u8 && (right == Al as u8 || right == Hl as u8))
876        || (left == Eb as u8 && right == Em as u8)
877    {
878        return PAIR_NO_BREAK;
879    }
880    PAIR_ALLOWED
881}
882
883const fn pair_has_context(left: u8, right: u8) -> bool {
884    use Class::*;
885
886    left == Sp as u8
887        || left == Qu as u8
888        || right == Qu as u8
889        || left == Hy as u8
890        || left == Hh as u8
891        || ((right == Po as u8 || right == Pr as u8)
892            && (left == Nu as u8
893                || left == Sy as u8
894                || left == Is as u8
895                || left == Cl as u8
896                || left == Cp as u8))
897        || (left == Sy as u8 && right == Nu as u8)
898        || ((left == Po as u8 || left == Pr as u8) && right == Op as u8)
899        || (left == Ri as u8 && right == Ri as u8)
900        || right == Em as u8
901        || ((left == Al as u8 || left == Hl as u8 || left == Nu as u8) && right == Op as u8)
902        || (left == Cp as u8 && (right == Al as u8 || right == Hl as u8 || right == Nu as u8))
903        || (left == Ap as u8 && right == Al as u8)
904        || ((left == Ak as u8 || left == As as u8) && right == Al as u8)
905        || (left == Al as u8
906            && (right == Ak as u8 || right == As as u8 || right == Vf as u8 || right == Vi as u8))
907        || ((left == Ak as u8 || left == As as u8) && (right == Ak as u8 || right == As as u8))
908        || (left == Vi as u8 && (right == Ak as u8 || right == As as u8 || right == Al as u8))
909        || left == Cm as u8
910        || right == Cm as u8
911        || left == Zwj as u8
912        || right == Zwj as u8
913        || left == Ambiguous as u8
914        || right == Ambiguous as u8
915        || left == Cj as u8
916        || right == Cj as u8
917        || left == Sa as u8
918        || right == Sa as u8
919        || left == Sg as u8
920        || right == Sg as u8
921        || left == Xx as u8
922        || right == Xx as u8
923}
924
925#[inline(never)]
926fn decide(state: &State, right: Property, next: [Option<Property>; 2]) -> Option<BreakOpportunity> {
927    use BreakOpportunity::{Allowed, Mandatory};
928    use Class::*;
929
930    let left = state.last;
931    if state.last_was_zwj {
932        return None;
933    }
934    if left.class == Sp {
935        if matches!(right.class, Bk | Cr | Lf | Nl | Sp | Zw) {
936            return None;
937        }
938        if state.ends_with_spaced(Zw) {
939            return Some(Allowed);
940        }
941        if matches!(right.class, Wj | Cl | Cp | Ex | Sy) {
942            return None;
943        }
944        if state.ends_with_spaced(Op) || state.pi_run {
945            return None;
946        }
947        if right.class == Qu {
948            return if right.has(Property::PF)
949                && next[0].map(|value| pf_suffix(value.class)).unwrap_or(true)
950            {
951                None
952            } else {
953                Some(Allowed)
954            };
955        }
956        if right.class == Is {
957            return if next[0].map(|value| value.class) == Some(Nu) {
958                Some(Allowed)
959            } else {
960                None
961            };
962        }
963        if (right.class == Ns && (state.ends_with_spaced(Cl) || state.ends_with_spaced(Cp)))
964            || (right.class == B2 && state.ends_with_spaced(B2))
965        {
966            return None;
967        }
968        return Some(Allowed);
969    }
970    if matches!(left.class, Al | Hl) && matches!(right.class, Al | Hl) {
971        return None;
972    }
973    if (left.class == Id && right.class == Id)
974        || (matches!(left.class, H2 | H3) && matches!(right.class, H2 | H3))
975    {
976        return Some(Allowed);
977    }
978    match PAIR_BREAKS[left.class as usize * 49 + right.class as usize] {
979        PAIR_NO_BREAK => return None,
980        PAIR_ALLOWED => return Some(Allowed),
981        PAIR_MANDATORY => return Some(Mandatory),
982        _ => {}
983    }
984    if left.class == Bk || (left.class == Cr && right.class != Lf) || matches!(left.class, Lf | Nl)
985    {
986        return Some(Mandatory);
987    }
988    if left.class == Cr && right.class == Lf {
989        return None;
990    }
991    if matches!(right.class, Bk | Cr | Lf | Nl) {
992        return None;
993    }
994    if matches!(right.class, Sp | Zw) {
995        return None;
996    }
997    if state.ends_with_spaced(Zw) {
998        return Some(Allowed);
999    }
1000    if matches!(left.class, Al | Hl) && matches!(right.class, Al | Hl) {
1001        return None;
1002    }
1003    if (matches!(left.class, Al | Hl) && right.class == Nu)
1004        || (left.class == Nu && matches!(right.class, Al | Hl | Nu))
1005    {
1006        return None;
1007    }
1008    if (left.class == Id && matches!(right.class, Al | Hl | Nu | Id))
1009        || (matches!(left.class, Al | Hl | Nu) && right.class == Id)
1010    {
1011        return Some(Allowed);
1012    }
1013    if left.class == Wj || right.class == Wj || left.class == Gl {
1014        return None;
1015    }
1016    if right.class == Gl && !matches!(left.class, Sp | Ba | Hy | Hh) {
1017        return None;
1018    }
1019    if matches!(right.class, Cl | Cp | Ex | Sy) {
1020        return None;
1021    }
1022    if state.ends_with_spaced(Op) || state.pi_run {
1023        return None;
1024    }
1025    if right.class == Qu
1026        && right.has(Property::PF)
1027        && next[0].map(|value| pf_suffix(value.class)).unwrap_or(true)
1028    {
1029        return None;
1030    }
1031    if right.class == Is {
1032        return None;
1033    }
1034    if right.class == Ns && (state.ends_with_spaced(Cl) || state.ends_with_spaced(Cp)) {
1035        return None;
1036    }
1037    if right.class == B2 && state.ends_with_spaced(B2) {
1038        return None;
1039    }
1040    if (right.class == Qu && !right.has(Property::PI))
1041        || (left.class == Qu && !left.has(Property::PF))
1042    {
1043        return None;
1044    }
1045    let right_followed_by_non_east_asian = next[0]
1046        .map(|value| !value.has(Property::EAST_ASIAN))
1047        .unwrap_or(true);
1048    if right.class == Qu && (!left.has(Property::EAST_ASIAN) || right_followed_by_non_east_asian) {
1049        return None;
1050    }
1051    if left.class == Qu
1052        && (!right.has(Property::EAST_ASIAN)
1053            || state
1054                .previous
1055                .map(|value| !value.has(Property::EAST_ASIAN))
1056                .unwrap_or(true))
1057    {
1058        return None;
1059    }
1060    if left.class == Cb || right.class == Cb {
1061        return Some(Allowed);
1062    }
1063    if matches!(left.class, Hy | Hh)
1064        && matches!(right.class, Al | Hl)
1065        && state
1066            .previous
1067            .map(|value| hyphen_prefix(value.class))
1068            .unwrap_or(true)
1069    {
1070        return None;
1071    }
1072    if matches!(right.class, Ba | Hh | Hy | Ns) || left.class == Bb {
1073        return None;
1074    }
1075    if matches!(left.class, Hy | Hh)
1076        && state.previous.map(|value| value.class) == Some(Hl)
1077        && right.class != Hl
1078    {
1079        return None;
1080    }
1081    if left.class == Sy && right.class == Hl {
1082        return None;
1083    }
1084    if right.class == In {
1085        return None;
1086    }
1087    if (matches!(left.class, Al | Hl) && right.class == Nu)
1088        || (left.class == Nu && matches!(right.class, Al | Hl))
1089    {
1090        return None;
1091    }
1092    if (left.class == Pr && matches!(right.class, Id | Eb | Em))
1093        || (matches!(left.class, Id | Eb | Em) && right.class == Po)
1094    {
1095        return None;
1096    }
1097    if (matches!(left.class, Pr | Po) && matches!(right.class, Al | Hl))
1098        || (matches!(left.class, Al | Hl) && matches!(right.class, Pr | Po))
1099    {
1100        return None;
1101    }
1102    if matches!(right.class, Po | Pr)
1103        && matches!(state.numeric, NumericState::Number | NumericState::Closed)
1104    {
1105        return None;
1106    }
1107    if matches!(left.class, Po | Pr)
1108        && (right.class == Nu
1109            || (right.class == Op
1110                && (next[0].map(|value| value.class) == Some(Nu)
1111                    || (next[0].map(|value| value.class) == Some(Is)
1112                        && next[1].map(|value| value.class) == Some(Nu)))))
1113    {
1114        return None;
1115    }
1116    if (matches!(left.class, Hy | Is) && right.class == Nu)
1117        || (state.numeric == NumericState::Number && right.class == Nu)
1118    {
1119        return None;
1120    }
1121    if (left.class == Jl && matches!(right.class, Jl | Jv | H2 | H3))
1122        || (matches!(left.class, Jv | H2) && matches!(right.class, Jv | Jt))
1123        || (matches!(left.class, Jt | H3) && right.class == Jt)
1124    {
1125        return None;
1126    }
1127    if (matches!(left.class, Jl | Jv | Jt | H2 | H3) && right.class == Po)
1128        || (left.class == Pr && matches!(right.class, Jl | Jv | Jt | H2 | H3))
1129    {
1130        return None;
1131    }
1132    if matches!(left.class, Al | Hl) && matches!(right.class, Al | Hl) {
1133        return None;
1134    }
1135    if (left.class == Ap && right.is_aksara_or_dotted())
1136        || (left.is_aksara_or_dotted() && matches!(right.class, Vf | Vi))
1137        || (state
1138            .previous
1139            .map(Property::is_aksara_or_dotted)
1140            .unwrap_or(false)
1141            && left.class == Vi
1142            && right.is_aksara_or_dotted())
1143        || (left.is_aksara_or_dotted()
1144            && right.is_aksara_or_dotted()
1145            && next[0].map(|value| value.class) == Some(Vf))
1146    {
1147        return None;
1148    }
1149    if left.class == Is && matches!(right.class, Al | Hl) {
1150        return None;
1151    }
1152    if (matches!(left.class, Al | Hl | Nu) && right.class == Op && !right.has(Property::EAST_ASIAN))
1153        || (left.class == Cp
1154            && !left.has(Property::EAST_ASIAN)
1155            && matches!(right.class, Al | Hl | Nu))
1156    {
1157        return None;
1158    }
1159    if left.class == Ri && right.class == Ri && state.regional_indicator_odd {
1160        return None;
1161    }
1162    if (left.class == Eb || left.has(Property::EXTENDED_PICTOGRAPHIC_CN)) && right.class == Em {
1163        return None;
1164    }
1165    Some(Allowed)
1166}
1167
1168fn pi_prefix(class: Class) -> bool {
1169    matches!(
1170        class,
1171        Class::Bk
1172            | Class::Cr
1173            | Class::Lf
1174            | Class::Nl
1175            | Class::Op
1176            | Class::Qu
1177            | Class::Gl
1178            | Class::Sp
1179            | Class::Zw
1180    )
1181}
1182
1183fn pf_suffix(class: Class) -> bool {
1184    matches!(
1185        class,
1186        Class::Sp
1187            | Class::Gl
1188            | Class::Wj
1189            | Class::Cl
1190            | Class::Qu
1191            | Class::Cp
1192            | Class::Ex
1193            | Class::Is
1194            | Class::Sy
1195            | Class::Bk
1196            | Class::Cr
1197            | Class::Lf
1198            | Class::Nl
1199            | Class::Zw
1200    )
1201}
1202
1203fn hyphen_prefix(class: Class) -> bool {
1204    matches!(
1205        class,
1206        Class::Bk
1207            | Class::Cr
1208            | Class::Lf
1209            | Class::Nl
1210            | Class::Sp
1211            | Class::Zw
1212            | Class::Cb
1213            | Class::Gl
1214    )
1215}
1216
1217fn is_safe_restart(value: Property) -> bool {
1218    unicode_tables::SAFE_RESTART_CLASSES & (1_u64 << value.class as u8) != 0
1219        && value.flags & unicode_tables::UNSAFE_RESTART_FLAGS == 0
1220}
1221
1222#[cfg(test)]
1223#[path = "unit_tests.rs"]
1224mod unit_tests;