cleft 0.1.0

Unicode character properties, and grapheme, word and line segmentation with dictionaries, case mapping and CSS classes, for no_std.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
//! Character properties, from the crate's packed tables.
//!
//! Each code point has two `u32` words, generated by `support/unicode_gen`
//! from the pinned UCD files into `tables`. The sweep test checks them
//! against icu_properties for every character.
//!
//! Each word is one three-level lookup: three dependent loads in every
//! plane. icu_properties costs a trie or an inversion list per property, and
//! an out-of-line call for any trie above U+0FFF.
//!
//! The core word, read as [`CoreProps`], holds what a layout's analysis
//! reads for every character. The rare word, read as [`RareProps`], holds
//! what is read per cluster or behind a gate. Reading a field is a shift and
//! a mask. Nothing here allocates, and no `char` makes a lookup panic: every
//! array read has a fallback, which the sweep test shows is never taken.
//!
//! # The core word
//!
//! | Bits | Field | Accessor |
//! |---:|---|---|
//! | 0..5 | bidi class, ICU4C's `UCharDirection` numbering | [`CoreProps::bidi_class`] |
//! | 5..15 | script, or a Script_Extensions set id plus `SCRIPT_COUNT` | [`CoreProps::scripts`] |
//! | 15..22 | bracket pair id plus one, or zero for none | [`CoreProps::bracket_pair`] |
//! | 22 | the bracket opens its pair | [`CoreProps::opens_bracket`] |
//! | 23 | Regional_Indicator | [`CoreProps::is_regional_indicator`] |
//! | 24 | Extended_Pictographic | [`CoreProps::is_extended_pictographic`] |
//! | 25 | Emoji_Presentation | [`CoreProps::is_emoji_presentation`] |
//! | 26 | paragraph separator | [`CoreProps::is_paragraph_separator`] |
//! | 27 | White_Space | [`CoreProps::is_white_space`] |
//! | 28 | Line_Break = SA | [`CoreProps::is_complex_context`] |
//! | 29 | General_Category Mn, Mc or Me | [`CoreProps::is_mark`] |
//! | 30 | Emoji | [`CoreProps::is_emoji`] |
//! | 31 | zero | |
//!
//! # The rare word
//!
//! | Bits | Field | Accessor |
//! |---:|---|---|
//! | 0 | Joining_Type = Join_Causing | [`RareProps::is_join_causing`] |
//! | 1 | East_Asian_Width = Wide or Fullwidth | [`RareProps::is_wide`] |
//! | 2 | Vertical_Orientation = U, Tu or Tr | [`RareProps::is_mixed_upright`] |
//! | 3 | Line_Break = CJ | [`RareProps::is_conditional_japanese_starter`] |
//! | 4 | Line_Break = GL, WJ or ZWJ | [`RareProps::is_glue`] |
//! | 5 | Emoji_Modifier_Base | reserved: `RareProps::is_emoji_modifier_base` |
//! | 6 | Emoji_Component | reserved: `RareProps::is_emoji_component` |
//! | 7 | Default_Ignorable_Code_Point | [`RareProps::is_default_ignorable`] |
//! | 8..12 | Grapheme_Cluster_Break | [`RareProps::grapheme_cluster_break`] |
//! | 12..14 | Indic_Conjunct_Break | [`RareProps::grapheme_props`] |
//! | 14 | Extended_Pictographic | [`RareProps::grapheme_props`] |
//! | 15..18 | `css`: East Asian spacing (UTR #59, as Blink derives it) | `RareProps::east_asian_spacing` |
//! | 18..22 | `css`: the text-spacing class (CSS Text 4, as Blink numbers it) | `RareProps::text_spacing` |
//! | 22..25 | `css`: the emphasis class (`text-emphasis-skip`) | `RareProps::emphasis` |
//! | 25..27 | `css`: the first-letter class (`::first-letter`) | `RareProps::first_letter` |
//! | 27 | Cased | [`RareProps::is_cased`] |
//! | 28 | Case_Ignorable | [`RareProps::is_case_ignorable`] |
//! | 29 | `css`: General_Category L or N | `RareProps::is_letter_or_number` |
//! | 30..32 | zero | |
//!
//! Bits 8 to 15 are the grapheme segmenter's byte, [`GraphemeProps`], taken
//! with one shift and mask. Without the `css` feature, bits 15 to 27 and 29
//! are zero and a smaller table is compiled, still one lookup.
//!
//! Enumerated properties use ICU4C's numbering as their discriminants,
//! except Grapheme_Cluster_Break, whose ZWJ is 13 instead of 17 to fit four
//! bits. A property that every reader asks one question of is stored as that
//! question's bit. The generator documents each choice.
//!
//! Only the sweep test reads the reserved fields, so their accessors exist
//! only in tests.
//!
//! # The line byte
//!
//! The line segmenter reads a byte of its own for every character, from a
//! lookup of its own with a Latin-1 table in front (`line_props`): the line
//! class in bits 0 to 5, East_Asian_Width F, W or H in bit 6, and
//! Extended_Pictographic and unassigned in bit 7. The line class is
//! Line_Break, split where a rule or an option reads more; the generator
//! documents the splits. [`line_break`] maps the class back to Line_Break.
//!
//! # The word byte
//!
//! The word segmenter reads a byte of its own the same way (`word_props`):
//! the word class in bits 0 to 4, Word_Break with U+002E FULL STOP apart;
//! Extended_Pictographic in bit 5; and in bits 6 and 7 the run of a
//! dictionary the character can be in, 1 for Line_Break SA and 2 for Script
//! Han or Hiragana or Word_Break Katakana.

mod tables;

#[cfg(test)]
mod tests;

use crate::GraphemeProps;
use crate::line::LineProps;
use crate::word::WordProps;

// The grapheme segmenter's byte is the Grapheme_Cluster_Break,
// Indic_Conjunct_Break and Extended_Pictographic bits together, in that order.
const _: () = assert!(
    tables::GRAPHEME_CLUSTER_BREAK_MASK == 0xF
        && tables::INDIC_CONJUNCT_BREAK_SHIFT == tables::GRAPHEME_CLUSTER_BREAK_SHIFT + 4
        && tables::INDIC_CONJUNCT_BREAK_MASK == 0x3
        && tables::GRAPHEME_PICTOGRAPHIC == 1 << (tables::GRAPHEME_CLUSTER_BREAK_SHIFT + 6)
);

// The line byte is the line class in the low six bits, which `line` reads
// with a mask, and two flags above them.
const _: () = assert!(
    tables::LINE_CLASS_SHIFT == 0
        && tables::LINE_CLASS_MASK == 0x3F
        && tables::LINE_EAST_ASIAN == 0x40
        && tables::LINE_UNASSIGNED_PICTOGRAPHIC == 0x80
);

// The word byte is the word class in the low five bits, the pictographic
// bit and the run bits, which `word` reads with masks and shifts.
const _: () = assert!(
    tables::WORD_CLASS_SHIFT == 0
        && tables::WORD_CLASS_MASK == 0x1F
        && tables::WORD_PICTOGRAPHIC == 0x20
        && tables::WORD_RUN_SHIFT == 6
        && tables::WORD_RUN_MASK == 0x3
        && tables::WORD_RUN_SOUTHEAST_ASIAN == 1
        && tables::WORD_RUN_CHINESE_JAPANESE == 2
);

/// The version of Unicode the properties and case mappings follow, as
/// (major, minor, update).
pub const UNICODE_VERSION: (u8, u8, u8) = tables::UNICODE_VERSION;

/// Returns the core properties of `ch`: the ones a layout reads for every
/// character.
#[inline]
pub fn core_props(ch: char) -> CoreProps {
    CoreProps(tables::core_word(ch))
}

/// Returns the rare properties of `ch`: the ones a layout reads per cluster
/// or behind a gate.
#[inline]
pub fn rare_props(ch: char) -> RareProps {
    RareProps(tables::rare_word(ch))
}

/// Returns the Line_Break property of `ch`, as UAX #14 assigns it.
///
/// The value is that of [`UNICODE_VERSION`], before any rule resolves it:
/// AI, SA, CJ and XX stay as they are.
///
/// # Examples
///
/// ```
/// use cleft::{LineBreak, line_break};
///
/// assert_eq!(line_break('a'), LineBreak::ALPHABETIC);
/// assert_eq!(line_break('æ¼¢'), LineBreak::IDEOGRAPHIC);
/// assert_eq!(line_break('-'), LineBreak::HYPHEN);
/// ```
#[inline]
pub fn line_break(ch: char) -> LineBreak {
    line_props(ch).line_break()
}

/// Returns what the line segmenter reads of `ch`: its line class and
/// whether it is East Asian.
///
/// Latin-1 reads a small table; anything else reads the line byte's lookup.
#[inline]
pub(crate) fn line_props(ch: char) -> LineProps {
    let bits = match tables::LINE_LATIN_1.get(ch as usize) {
        Some(&bits) => bits,
        None => tables::line_word(ch),
    };
    LineProps::from_bits(bits)
}

/// The core properties of a character, packed in one word.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct CoreProps(u32);

impl CoreProps {
    /// Returns the Bidi_Class.
    #[inline]
    pub const fn bidi_class(self) -> BidiClass {
        // The word holds ICU4C's `UCharDirection` numbering, which
        // `BidiClass` uses too, so no conversion is needed.
        BidiClass(((self.0 >> tables::BIDI_CLASS_SHIFT) & tables::BIDI_CLASS_MASK) as u8)
    }

    /// Returns the Script and Script_Extensions as the word holds them.
    #[inline]
    pub const fn scripts(self) -> ScriptWithExt {
        ScriptWithExt(((self.0 >> tables::SCRIPT_SHIFT) & tables::SCRIPT_MASK) as u16)
    }

    /// Returns the Script.
    ///
    /// A character shared between scripts is Common or Inherited. Its
    /// [`script_extensions`](Self::script_extensions) list the scripts it
    /// may take.
    #[inline]
    pub fn script(self) -> ScriptId {
        let scripts = self.scripts();
        match scripts.single() {
            Some(script) => script,
            None => match scripts.set_run().first() {
                Some(script) => *script,
                None => ScriptId::UNKNOWN,
            },
        }
    }

    /// Returns the Script_Extensions, in ICU4C's value order.
    ///
    /// A character with none of its own has its Script alone, as UAX #24
    /// defines. A Common or Inherited character used by only some scripts
    /// lists those scripts and not its Script.
    #[inline]
    pub fn script_extensions(self) -> &'static [ScriptId] {
        let scripts = self.scripts();
        match scripts.single() {
            // `SCRIPT_RUNS` begins with every script alone, in value order.
            Some(script) => {
                let at = usize::from(script.0);
                tables::SCRIPT_RUNS.get(at..at + 1).unwrap_or(&[])
            }
            None => scripts.set_run().get(1..).unwrap_or(&[]),
        }
    }

    /// Returns the bracket pair of the character, or `None` if it is not a
    /// paired bracket (Bidi_Paired_Bracket_Type Open or Close).
    #[inline]
    pub const fn bracket_pair(self) -> Option<BracketPairId> {
        match (self.0 >> tables::BRACKET_PAIR_SHIFT) & tables::BRACKET_PAIR_MASK {
            0 => None,
            stored => Some(BracketPairId((stored - 1) as u8)),
        }
    }

    /// Returns whether the character opens its bracket pair.
    #[inline]
    pub const fn opens_bracket(self) -> bool {
        self.0 & tables::OPENS_BRACKET != 0
    }

    /// Returns the closing bracket of the character's pair, or `None` if it
    /// is not a paired bracket.
    ///
    /// Both brackets of a pair return the closing one, in canonical form:
    /// U+2329 and U+232A return U+3009, as U+3008 does.
    #[inline]
    pub fn closing_bracket(self) -> Option<char> {
        let pair = self.bracket_pair()?;
        tables::BRACKET_CLOSINGS.get(usize::from(pair.0)).copied()
    }

    /// Returns whether the character is a Regional_Indicator, half of a flag.
    #[inline]
    pub const fn is_regional_indicator(self) -> bool {
        self.0 & tables::REGIONAL_INDICATOR != 0
    }

    /// Returns whether the character is Extended_Pictographic: a pictograph,
    /// emoji or not, which a ZWJ sequence may join.
    #[inline]
    pub const fn is_extended_pictographic(self) -> bool {
        self.0 & tables::EXTENDED_PICTOGRAPHIC != 0
    }

    /// Returns whether the character has Emoji_Presentation: shown as emoji
    /// unless a variation selector says otherwise.
    #[inline]
    pub const fn is_emoji_presentation(self) -> bool {
        self.0 & tables::EMOJI_PRESENTATION != 0
    }

    /// Returns whether the character forces a line break and ends a
    /// paragraph: LF, VT, FF, CR, NEL, U+2028 or U+2029.
    ///
    /// These are UAX #14's mandatory breaks (Line_Break BK, CR, LF and NL).
    /// U+001C to U+001E are bidi class B but break no line, so they end only
    /// a bidi paragraph.
    #[inline]
    pub const fn is_paragraph_separator(self) -> bool {
        self.0 & tables::PARAGRAPH_SEPARATOR != 0
    }

    /// Returns whether the character is White_Space.
    #[inline]
    pub const fn is_white_space(self) -> bool {
        self.0 & tables::WHITE_SPACE != 0
    }

    /// Returns whether the character is Line_Break SA: Thai, Lao, Khmer,
    /// Myanmar and the like, whose breaks need a dictionary.
    #[inline]
    pub const fn is_complex_context(self) -> bool {
        self.0 & tables::COMPLEX_CONTEXT != 0
    }

    /// Returns whether the character is a mark: General_Category Mn, Mc or
    /// Me.
    #[inline]
    pub const fn is_mark(self) -> bool {
        self.0 & tables::MARK != 0
    }

    /// Returns whether the character is Emoji, in either presentation.
    ///
    /// True for every Emoji_Presentation character, the Extended_Pictographic
    /// characters that are emoji, and the keycap bases `#`, `*` and the
    /// digits.
    #[inline]
    pub const fn is_emoji(self) -> bool {
        self.0 & tables::EMOJI != 0
    }
}

/// Returns what the word segmenter reads of `ch`: its word class, whether
/// it is a pictograph, and its dictionary run.
///
/// Latin-1 reads a small table; anything else reads the word byte's lookup.
#[inline]
pub(crate) fn word_props(ch: char) -> WordProps {
    let bits = match tables::WORD_LATIN_1.get(ch as usize) {
        Some(&bits) => bits,
        None => tables::word_break_word(ch),
    };
    WordProps::from_bits(bits)
}

/// The rare properties of a character, packed in one word.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct RareProps(u32);

impl RareProps {
    /// Returns whether the character is Joining_Type Join_Causing: the
    /// tatweel and U+200D ZERO WIDTH JOINER, which join on both sides without
    /// forms of their own.
    #[inline]
    pub const fn is_join_causing(self) -> bool {
        self.0 & tables::JOIN_CAUSING != 0
    }

    /// Returns whether the character is East_Asian_Width Wide or Fullwidth:
    /// ideographs, kana, Hangul, most emoji and the fullwidth forms.
    #[inline]
    pub const fn is_wide(self) -> bool {
        self.0 & tables::WIDE != 0
    }

    /// Returns whether the character stands upright in a vertical line under
    /// `text-orientation: mixed`: Vertical_Orientation U, Tu or Tr.
    ///
    /// A Tr character's vertical form is left to the font's `vert` feature.
    #[inline]
    pub const fn is_mixed_upright(self) -> bool {
        // As Blink's `Character::IsUprightInMixedVertical` reads them. CSS
        // Writing Modes 3, section 5.1.1 turns a Tr character sideways in a
        // font without a vertical form, but fonts are chosen after analysis
        // fixes the orientation.
        self.0 & tables::UPRIGHT_IN_MIXED != 0
    }

    /// Returns whether the character is Line_Break CJ: small kana and the
    /// prolonged sound mark, which `line-break: strict` keeps from starting
    /// a line.
    #[inline]
    pub const fn is_conditional_japanese_starter(self) -> bool {
        self.0 & tables::CONDITIONAL_JAPANESE_STARTER != 0
    }

    /// Returns whether the character is Line_Break GL, WJ or ZWJ, which glue
    /// their neighbours together.
    ///
    /// Among the space separators, only the no-break spaces (GL) glue.
    #[inline]
    pub const fn is_glue(self) -> bool {
        self.0 & tables::GLUE != 0
    }

    /// Returns whether the character is an Emoji_Modifier_Base, which takes
    /// a skin tone modifier.
    #[inline]
    #[cfg(test)]
    const fn is_emoji_modifier_base(self) -> bool {
        self.0 & tables::EMOJI_MODIFIER_BASE != 0
    }

    /// Returns whether the character is an Emoji_Component: part of a
    /// keycap, flag, tag or modifier sequence.
    #[inline]
    #[cfg(test)]
    const fn is_emoji_component(self) -> bool {
        self.0 & tables::EMOJI_COMPONENT != 0
    }

    /// Returns whether the character is a Default_Ignorable_Code_Point,
    /// drawn as nothing when a font lacks it.
    #[inline]
    pub const fn is_default_ignorable(self) -> bool {
        self.0 & tables::DEFAULT_IGNORABLE != 0
    }

    /// Returns the Grapheme_Cluster_Break.
    #[inline]
    pub const fn grapheme_cluster_break(self) -> GraphemeClusterBreak {
        GraphemeClusterBreak::from_bits(
            (self.0 >> tables::GRAPHEME_CLUSTER_BREAK_SHIFT) & tables::GRAPHEME_CLUSTER_BREAK_MASK,
        )
    }

    /// Returns the properties the grapheme segmenter reads:
    /// Grapheme_Cluster_Break, Indic_Conjunct_Break and
    /// Extended_Pictographic.
    #[inline]
    pub const fn grapheme_props(self) -> GraphemeProps {
        GraphemeProps::from_bits(((self.0 >> tables::GRAPHEME_CLUSTER_BREAK_SHIFT) & 0x7F) as u8)
    }

    /// Returns the Indic_Conjunct_Break.
    #[inline]
    #[cfg(test)]
    const fn indic_conjunct_break(self) -> IndicConjunctBreak {
        IndicConjunctBreak::from_bits(
            (self.0 >> tables::INDIC_CONJUNCT_BREAK_SHIFT) & tables::INDIC_CONJUNCT_BREAK_MASK,
        )
    }

    /// Returns the class `text-autospace` reads.
    #[cfg(feature = "css")]
    #[inline]
    pub const fn east_asian_spacing(self) -> EastAsianSpacing {
        EastAsianSpacing::from_bits(
            (self.0 >> tables::EAST_ASIAN_SPACING_SHIFT) & tables::EAST_ASIAN_SPACING_MASK,
        )
    }

    /// Returns the class `text-spacing-trim` reads.
    #[cfg(feature = "css")]
    #[inline]
    pub const fn text_spacing(self) -> TextSpacingClass {
        TextSpacingClass::from_bits(
            (self.0 >> tables::TEXT_SPACING_SHIFT) & tables::TEXT_SPACING_MASK,
        )
    }

    /// Returns the class `text-emphasis-skip` reads.
    #[cfg(feature = "css")]
    #[inline]
    pub const fn emphasis(self) -> EmphasisClass {
        EmphasisClass::from_bits((self.0 >> tables::EMPHASIS_SHIFT) & tables::EMPHASIS_MASK)
    }

    /// Returns the class `::first-letter` reads to find the first letter's
    /// end.
    #[cfg(feature = "css")]
    #[inline]
    pub const fn first_letter(self) -> FirstLetterClass {
        FirstLetterClass::from_bits(
            (self.0 >> tables::FIRST_LETTER_SHIFT) & tables::FIRST_LETTER_MASK,
        )
    }

    /// Returns whether the character is Cased: a lowercase, uppercase or
    /// titlecase character.
    #[inline]
    pub const fn is_cased(self) -> bool {
        self.0 & tables::CASED != 0
    }

    /// Returns whether the character is Case_Ignorable: a mark, a format
    /// character, a modifier or an apostrophe, which the case mappings that
    /// read their neighbours step over.
    #[inline]
    pub const fn is_case_ignorable(self) -> bool {
        self.0 & tables::CASE_IGNORABLE != 0
    }

    /// Returns whether the character is a letter or a number:
    /// General_Category L or N.
    ///
    /// `word-break: keep-all` holds these together.
    #[cfg(feature = "css")]
    #[inline]
    pub const fn is_letter_or_number(self) -> bool {
        self.0 & tables::LETTER_OR_NUMBER != 0
    }
}

/// A Bidi_Class, in ICU4C's `UCharDirection` numbering.
#[derive(Copy, Clone, Default, Eq, PartialEq, Debug)]
#[repr(transparent)]
pub struct BidiClass(u8);

impl BidiClass {
    /// Left-to-right letter (L).
    pub const LEFT_TO_RIGHT: Self = Self(0);
    /// Right-to-left letter (R).
    pub const RIGHT_TO_LEFT: Self = Self(1);
    /// European number (EN).
    pub const EUROPEAN_NUMBER: Self = Self(2);
    /// European separator (ES).
    pub const EUROPEAN_SEPARATOR: Self = Self(3);
    /// European terminator (ET).
    pub const EUROPEAN_TERMINATOR: Self = Self(4);
    /// Arabic number (AN).
    pub const ARABIC_NUMBER: Self = Self(5);
    /// Common separator (CS).
    pub const COMMON_SEPARATOR: Self = Self(6);
    /// Paragraph separator (B).
    pub const PARAGRAPH_SEPARATOR: Self = Self(7);
    /// Segment separator (S).
    pub const SEGMENT_SEPARATOR: Self = Self(8);
    /// Whitespace (WS).
    pub const WHITE_SPACE: Self = Self(9);
    /// Other neutral (ON).
    pub const OTHER_NEUTRAL: Self = Self(10);
    /// Left-to-right embedding (LRE).
    pub const LEFT_TO_RIGHT_EMBEDDING: Self = Self(11);
    /// Left-to-right override (LRO).
    pub const LEFT_TO_RIGHT_OVERRIDE: Self = Self(12);
    /// Arabic letter (AL).
    pub const ARABIC_LETTER: Self = Self(13);
    /// Right-to-left embedding (RLE).
    pub const RIGHT_TO_LEFT_EMBEDDING: Self = Self(14);
    /// Right-to-left override (RLO).
    pub const RIGHT_TO_LEFT_OVERRIDE: Self = Self(15);
    /// Pop directional format (PDF).
    pub const POP_DIRECTIONAL_FORMAT: Self = Self(16);
    /// Nonspacing mark (NSM).
    pub const NONSPACING_MARK: Self = Self(17);
    /// Boundary neutral (BN).
    pub const BOUNDARY_NEUTRAL: Self = Self(18);
    /// First strong isolate (FSI).
    pub const FIRST_STRONG_ISOLATE: Self = Self(19);
    /// Left-to-right isolate (LRI).
    pub const LEFT_TO_RIGHT_ISOLATE: Self = Self(20);
    /// Right-to-left isolate (RLI).
    pub const RIGHT_TO_LEFT_ISOLATE: Self = Self(21);
    /// Pop directional isolate (PDI).
    pub const POP_DIRECTIONAL_ISOLATE: Self = Self(22);

    /// Creates a class from its ICU4C `UCharDirection` value.
    ///
    /// A value past 22 names no class: it equals no constant and is in no
    /// [`mask`](Self::mask) set of classes.
    #[inline]
    pub const fn new(value: u8) -> Self {
        Self(value)
    }

    /// Returns the class's ICU4C `UCharDirection` value.
    #[inline]
    pub const fn get(self) -> u8 {
        self.0
    }

    /// Returns whether rule X9 of UAX #9 removes the class: the embeddings,
    /// the overrides, PDF and BN.
    #[inline]
    pub const fn is_removed_by_x9(self) -> bool {
        matches!(self.0, 11 | 12 | 14 | 15 | 16 | 18)
    }

    /// Returns the class as a single bit, for testing membership of a set of
    /// classes.
    ///
    /// Every class is under 31, so a value from 31 up maps to bit 31, which
    /// no class has.
    #[inline]
    pub const fn mask(self) -> u32 {
        // A wrapping shift would alias 200 onto class 8.
        1u32 << if self.0 < 31 { self.0 as u32 } else { 31 }
    }
}

/// A Line_Break value (UAX #14), in ICU4C's `ULineBreak` numbering.
#[derive(Copy, Clone, Default, Eq, PartialEq, Debug)]
#[repr(transparent)]
pub struct LineBreak(u8);

impl LineBreak {
    /// Unknown (XX): unassigned and private use.
    pub const UNKNOWN: Self = Self(0);
    /// Ambiguous (AI).
    pub const AMBIGUOUS: Self = Self(1);
    /// Alphabetic (AL).
    pub const ALPHABETIC: Self = Self(2);
    /// Break opportunity before and after (B2).
    pub const BREAK_BOTH: Self = Self(3);
    /// Break after (BA).
    pub const BREAK_AFTER: Self = Self(4);
    /// Break before (BB).
    pub const BREAK_BEFORE: Self = Self(5);
    /// Mandatory break (BK).
    pub const MANDATORY_BREAK: Self = Self(6);
    /// Contingent break opportunity (CB).
    pub const CONTINGENT_BREAK: Self = Self(7);
    /// Close punctuation (CL).
    pub const CLOSE_PUNCTUATION: Self = Self(8);
    /// Combining mark (CM).
    pub const COMBINING_MARK: Self = Self(9);
    /// Carriage return (CR).
    pub const CARRIAGE_RETURN: Self = Self(10);
    /// Exclamation or interrogation (EX).
    pub const EXCLAMATION: Self = Self(11);
    /// Non-breaking glue (GL).
    pub const GLUE: Self = Self(12);
    /// Hyphen (HY).
    pub const HYPHEN: Self = Self(13);
    /// Ideographic (ID).
    pub const IDEOGRAPHIC: Self = Self(14);
    /// Inseparable characters (IN).
    pub const INSEPARABLE: Self = Self(15);
    /// Infix numeric separator (IS).
    pub const INFIX_NUMERIC: Self = Self(16);
    /// Line feed (LF).
    pub const LINE_FEED: Self = Self(17);
    /// Nonstarter (NS).
    pub const NONSTARTER: Self = Self(18);
    /// Numeric (NU).
    pub const NUMERIC: Self = Self(19);
    /// Open punctuation (OP).
    pub const OPEN_PUNCTUATION: Self = Self(20);
    /// Postfix numeric (PO).
    pub const POSTFIX_NUMERIC: Self = Self(21);
    /// Prefix numeric (PR).
    pub const PREFIX_NUMERIC: Self = Self(22);
    /// Quotation (QU).
    pub const QUOTATION: Self = Self(23);
    /// Complex context dependent (SA): Thai, Lao, Khmer, Myanmar and the like.
    pub const COMPLEX_CONTEXT: Self = Self(24);
    /// Space (SP).
    pub const SPACE: Self = Self(26);
    /// Symbols allowing a break after (SY).
    pub const BREAK_SYMBOLS: Self = Self(27);
    /// Zero width space (ZW).
    pub const ZERO_WIDTH_SPACE: Self = Self(28);
    /// Next line (NL).
    pub const NEXT_LINE: Self = Self(29);
    /// Word joiner (WJ).
    pub const WORD_JOINER: Self = Self(30);
    /// Hangul LV syllable (H2).
    pub const H2: Self = Self(31);
    /// Hangul LVT syllable (H3).
    pub const H3: Self = Self(32);
    /// Hangul leading jamo (JL).
    pub const JL: Self = Self(33);
    /// Hangul trailing jamo (JT).
    pub const JT: Self = Self(34);
    /// Hangul vowel jamo (JV).
    pub const JV: Self = Self(35);
    /// Close parenthesis (CP).
    pub const CLOSE_PARENTHESIS: Self = Self(36);
    /// Conditional Japanese starter (CJ): small kana and the prolonged sound
    /// mark.
    pub const CONDITIONAL_JAPANESE_STARTER: Self = Self(37);
    /// Hebrew letter (HL).
    pub const HEBREW_LETTER: Self = Self(38);
    /// Regional indicator (RI).
    pub const REGIONAL_INDICATOR: Self = Self(39);
    /// Emoji base (EB).
    pub const E_BASE: Self = Self(40);
    /// Emoji modifier (EM).
    pub const E_MODIFIER: Self = Self(41);
    /// Zero width joiner (ZWJ).
    pub const ZWJ: Self = Self(42);
    /// Aksara (AK).
    pub const AKSARA: Self = Self(43);
    /// Aksara prebase (AP).
    pub const AKSARA_PREBASE: Self = Self(44);
    /// Aksara start (AS).
    pub const AKSARA_START: Self = Self(45);
    /// Virama final (VF).
    pub const VIRAMA_FINAL: Self = Self(46);
    /// Virama (VI).
    pub const VIRAMA: Self = Self(47);
    /// Unambiguous hyphen (HH).
    pub const UNAMBIGUOUS_HYPHEN: Self = Self(48);

    /// Creates a value from its ICU4C `ULineBreak` number.
    ///
    /// A number past 48, or 25 (SG, which no scalar value has), equals no
    /// constant.
    #[inline]
    pub const fn new(value: u8) -> Self {
        Self(value)
    }

    /// Returns the value's ICU4C `ULineBreak` number.
    #[inline]
    pub const fn get(self) -> u8 {
        self.0
    }
}

/// A script in ICU4C's `UScriptCode` numbering, which indexes the table of
/// script tags.
///
/// A script Unicode has added and ICU4C has not numbered yet takes the next
/// value.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
#[repr(transparent)]
pub struct ScriptId(u16);

impl ScriptId {
    /// Common (`Zyyy`): punctuation, digits and symbols every script uses.
    pub const COMMON: Self = Self(tables::SCRIPT_COMMON);
    /// Inherited (`Zinh`): marks that take the script of what they sit on.
    pub const INHERITED: Self = Self(tables::SCRIPT_INHERITED);
    /// Unknown (`Zzzz`): unassigned code points, private use and the like.
    pub const UNKNOWN: Self = Self(tables::SCRIPT_UNKNOWN);
    /// Han (`Hani`): the ideographs, and the radicals and marks that go with
    /// them.
    pub const HAN: Self = Self(tables::SCRIPT_HAN);
    /// Hiragana (`Hira`).
    pub const HIRAGANA: Self = Self(tables::SCRIPT_HIRAGANA);
    /// Katakana (`Kana`).
    pub const KATAKANA: Self = Self(tables::SCRIPT_KATAKANA);
    /// Khmer (`Khmr`).
    pub const KHMER: Self = Self(tables::SCRIPT_KHMER);
    /// Lao (`Laoo`).
    pub const LAO: Self = Self(tables::SCRIPT_LAO);
    /// Myanmar (`Mymr`).
    pub const MYANMAR: Self = Self(tables::SCRIPT_MYANMAR);
    /// Thai (`Thai`).
    pub const THAI: Self = Self(tables::SCRIPT_THAI);

    /// Returns the ISO 15924 tag, or `Zzzz` for an unknown id.
    #[inline]
    pub fn tag(self) -> [u8; 4] {
        match tables::SCRIPT_TAGS.get(usize::from(self.0)) {
            Some(tag) => *tag,
            None => *b"Zzzz",
        }
    }

    /// Returns the ICU4C `UScriptCode` value.
    #[inline]
    pub const fn get(self) -> u16 {
        self.0
    }
}

/// A character's Script and Script_Extensions in ten bits, after ICU's
/// `ScriptWithExtensions` encoding.
///
/// Most characters have a script that is the whole of their
/// Script_Extensions, which [`single`](Self::single) returns.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct ScriptWithExt(u16);

impl ScriptWithExt {
    /// Returns the script if it is the whole Script_Extensions, or `None` if
    /// the character has a set of scripts.
    #[inline]
    pub const fn single(self) -> Option<ScriptId> {
        // Below `SCRIPT_COUNT` the value is a script whose Script_Extensions
        // is the script alone. At or above, it names a set, one per distinct
        // pair of Script and Script_Extensions, whose run in `SCRIPT_RUNS` is
        // the Script and then the extensions. ICU spends two more bits to
        // keep Common and Inherited out of the run; the word has none to
        // spare.
        if self.0 < tables::SCRIPT_COUNT {
            Some(ScriptId(self.0))
        } else {
            None
        }
    }

    /// Returns a set's run, its Script and then its Script_Extensions; empty
    /// for a single script.
    #[inline]
    fn set_run(self) -> &'static [ScriptId] {
        let Some(set) = self.0.checked_sub(tables::SCRIPT_COUNT) else {
            return &[];
        };
        let set = usize::from(set);
        let starts = &tables::SCRIPT_SET_STARTS;
        let (Some(start), Some(end)) = (starts.get(set), starts.get(set + 1)) else {
            return &[];
        };
        tables::SCRIPT_RUNS
            .get(usize::from(*start)..usize::from(*end))
            .unwrap_or(&[])
    }
}

/// A bracket pair, which indexes the table of closing brackets.
///
/// Both brackets of a pair carry the same id, as do canonically equivalent
/// pairs: U+2329 and U+232A carry the id of U+3008 and U+3009, so that
/// comparing ids matches brackets as UAX #9 BD16 does.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
#[repr(transparent)]
pub struct BracketPairId(u8);

/// A character's class for `text-autospace`: UTR #59's East_Asian_Spacing.
///
/// Two values are this crate's own. Decimal digits are split from the other
/// narrow characters to tell `ideograph-numeric` from `ideograph-alpha`.
/// Marks have their own value, since `text-autospace` skips every mark
/// before classifying.
#[cfg(feature = "css")]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
#[repr(u8)]
pub enum EastAsianSpacing {
    /// Neither side of a seam: punctuation, symbols, spaces, the fullwidth
    /// and halfwidth forms.
    #[default]
    Other = 0,
    /// A letter of a script that sets text with spaces between words: Latin,
    /// Greek, Cyrillic, Arabic and the like.
    Narrow = 1,
    /// A decimal digit that is not full width.
    NarrowDigit = 2,
    /// Other_Punctuation that is narrow in Chinese and other elsewhere.
    Conditional = 3,
    /// An ideograph, kana, Hangul, Bopomofo, Yi and the like.
    Wide = 4,
    /// A combining mark, which takes no part in a seam.
    Mark = 5,
}

#[cfg(feature = "css")]
impl EastAsianSpacing {
    // The values are derived as Blink derives the property, since the UCD
    // lacks it; the derivation is in `support/unicode_gen`.
    #[inline]
    const fn from_bits(bits: u32) -> Self {
        match bits {
            1 => Self::Narrow,
            2 => Self::NarrowDigit,
            3 => Self::Conditional,
            4 => Self::Wide,
            5 => Self::Mark,
            _ => Self::Other,
        }
    }
}

/// A character's text-spacing class (CSS Text 4, section 8.5).
///
/// It says which full-width punctuation the character is, and so how
/// `text-spacing-trim` treats it beside other punctuation. Five classes
/// depend on the font: whether its dots and colons sit in the left half or
/// the middle of their em, and whether its curly quotes are full width.
#[cfg(feature = "css")]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
#[repr(u8)]
pub enum TextSpacingClass {
    /// Not punctuation the rule reads.
    #[default]
    Other = 0,
    /// A full-width opening bracket, drawn in the right half of its em.
    Open = 1,
    /// A full-width closing bracket, drawn in the left half.
    Close = 2,
    /// A middle dot or the ideographic space, drawn in the middle.
    Middle = 3,
    /// An opening bracket that is not full width.
    OpenNarrow = 4,
    /// A closing bracket that is not full width.
    CloseNarrow = 5,
    /// The ideographic and fullwidth comma and full stop.
    Dot = 6,
    /// The fullwidth colon.
    Colon = 7,
    /// The fullwidth semicolon.
    Semicolon = 8,
    /// U+2018 and U+201C.
    OpenQuote = 9,
    /// U+2019 and U+201D.
    CloseQuote = 10,
}

#[cfg(feature = "css")]
impl TextSpacingClass {
    // The values are Blink's `HanKerningCharType` numbering.
    #[inline]
    const fn from_bits(bits: u32) -> Self {
        match bits {
            1 => Self::Open,
            2 => Self::Close,
            3 => Self::Middle,
            4 => Self::OpenNarrow,
            5 => Self::CloseNarrow,
            6 => Self::Dot,
            7 => Self::Colon,
            8 => Self::Semicolon,
            9 => Self::OpenQuote,
            10 => Self::CloseQuote,
            _ => Self::Other,
        }
    }
}

/// A character's class for `text-emphasis-skip` (CSS Text Decoration 4,
/// section 3.5), from its General_Category.
///
/// The initial `spaces punctuation` marks the [`Marked`](Self::Marked) and
/// [`Symbol`](Self::Symbol) characters.
#[cfg(feature = "css")]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
#[repr(u8)]
pub enum EmphasisClass {
    // The classes mark what Blink's `Character::CanReceiveTextEmphasis`
    // marks; the derivation is in `support/unicode_gen`.
    /// A letter, a digit, a mark, and the punctuation CSS marks anyway (`#`,
    /// `%`, `&`, `@` and their kin).
    #[default]
    Marked = 0,
    /// A space separator, or a word separator such as U+1361 ETHIOPIC
    /// WORDSPACE.
    Space = 1,
    /// Punctuation.
    Punctuation = 2,
    /// A symbol.
    Symbol = 3,
    /// A control, a format character, an unassigned code point, or a line or
    /// paragraph separator: never marked.
    Never = 4,
}

#[cfg(feature = "css")]
impl EmphasisClass {
    #[inline]
    const fn from_bits(bits: u32) -> Self {
        match bits {
            1 => Self::Space,
            2 => Self::Punctuation,
            3 => Self::Symbol,
            4 => Self::Never,
            _ => Self::Marked,
        }
    }
}

/// A character's class for `::first-letter`, from its General_Category.
///
/// The classes are the groups CSS Pseudo-Elements 4, section 2.2.1 includes
/// with the first letter.
#[cfg(feature = "css")]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
#[repr(u8)]
pub enum FirstLetterClass {
    // The groups as Blink's `FirstLetterPseudoElement` tests them.
    /// Neither punctuation nor a space separator: a candidate first letter.
    #[default]
    Other = 0,
    /// A space separator (Zs) other than U+3000 IDEOGRAPHIC SPACE: the
    /// typographic space that may come between the punctuation and the
    /// letter.
    Space = 1,
    /// Opening punctuation (Ps) or a dash (Pd), included only before the
    /// letter.
    Opening = 2,
    /// The rest of the punctuation (Pc, Pe, Pi, Pf, Po), included on either
    /// side of the letter.
    Punctuation = 3,
}

#[cfg(feature = "css")]
impl FirstLetterClass {
    #[inline]
    const fn from_bits(bits: u32) -> Self {
        match bits {
            1 => Self::Space,
            2 => Self::Opening,
            3 => Self::Punctuation,
            _ => Self::Other,
        }
    }
}

/// Grapheme_Cluster_Break (UAX #29), in ICU4C's numbering except for
/// [`Zwj`](Self::Zwj).
///
/// ICU4C numbers ZWJ 17, after four values no character has had since
/// Unicode 11.0. Here ZWJ is 13, and the four are absent.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
#[repr(u8)]
pub enum GraphemeClusterBreak {
    /// Anything else.
    #[default]
    Other = 0,
    /// Controls and separators, which stand alone.
    Control = 1,
    /// U+000D.
    Cr = 2,
    /// Marks and joiners that extend what precedes them.
    Extend = 3,
    /// A Hangul leading consonant.
    L = 4,
    /// U+000A.
    Lf = 5,
    /// A Hangul LV syllable.
    Lv = 6,
    /// A Hangul LVT syllable.
    Lvt = 7,
    /// A Hangul trailing consonant.
    T = 8,
    /// A Hangul vowel.
    V = 9,
    /// A spacing mark.
    SpacingMark = 10,
    /// A prepended concatenation mark.
    Prepend = 11,
    /// Half of a flag.
    RegionalIndicator = 12,
    /// U+200D ZERO WIDTH JOINER.
    Zwj = 13,
}

impl GraphemeClusterBreak {
    /// Returns whether two characters of such classes always have a
    /// grapheme boundary between them, except inside a CRLF.
    ///
    /// True for Other, Control, CR and LF. Every UAX #29 rule that joins two
    /// characters names another class on one side.
    #[inline]
    pub const fn stands_alone(self) -> bool {
        matches!(self, Self::Other | Self::Control | Self::Cr | Self::Lf)
    }

    #[inline]
    const fn from_bits(bits: u32) -> Self {
        match bits {
            1 => Self::Control,
            2 => Self::Cr,
            3 => Self::Extend,
            4 => Self::L,
            5 => Self::Lf,
            6 => Self::Lv,
            7 => Self::Lvt,
            8 => Self::T,
            9 => Self::V,
            10 => Self::SpacingMark,
            11 => Self::Prepend,
            12 => Self::RegionalIndicator,
            13 => Self::Zwj,
            _ => Self::Other,
        }
    }
}

/// Indic_Conjunct_Break (UAX #29 GB9c), in ICU4C's numbering.
#[cfg(test)]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
#[repr(u8)]
enum IndicConjunctBreak {
    /// Takes no part in a conjunct.
    #[default]
    None = 0,
    /// A consonant a conjunct may join.
    Consonant = 1,
    /// A mark that may sit inside a conjunct.
    Extend = 2,
    /// A virama that joins two consonants.
    Linker = 3,
}

#[cfg(test)]
impl IndicConjunctBreak {
    const fn from_bits(bits: u32) -> Self {
        match bits {
            1 => Self::Consonant,
            2 => Self::Extend,
            3 => Self::Linker,
            _ => Self::None,
        }
    }
}