Skip to main content

cranpose_text/
script.rs

1use icu_properties::{
2    CodePointMapData, PropertyNamesShortBorrowed,
3    props::{BidiMirroringGlyph, BidiPairedBracketType, Script as UnicodeScript},
4    script::ScriptWithExtensions,
5};
6use smallvec::SmallVec;
7
8/// A writing system, as its ISO 15924 code such as `Latn` or `Deva`.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub struct Script([u8; 4]);
11
12impl Script {
13    /// Text that belongs to no one script, such as digits and punctuation
14    /// alone: `Zyyy`.
15    pub const COMMON: Self = Self(*b"Zyyy");
16    /// The Latin script: `Latn`.
17    pub const LATIN: Self = Self(*b"Latn");
18
19    /// The ISO 15924 code.
20    pub fn tag(self) -> [u8; 4] {
21        self.0
22    }
23
24    // Whether letter spacing spreads this script's letters apart. Android
25    // leaves out the scripts whose letters join, cursively or along a
26    // headline.
27    pub(crate) fn takes_letter_spacing(self) -> bool {
28        // The tags as words, each compared at once: a byte at a time the
29        // comparisons of a Latin item, which matches none, run long.
30        const JOINING: [u32; 14] = [
31            u32::from_ne_bytes(*b"Arab"),
32            u32::from_ne_bytes(*b"Nkoo"),
33            u32::from_ne_bytes(*b"Phlp"),
34            u32::from_ne_bytes(*b"Mand"),
35            u32::from_ne_bytes(*b"Mong"),
36            u32::from_ne_bytes(*b"Phag"),
37            u32::from_ne_bytes(*b"Deva"),
38            u32::from_ne_bytes(*b"Beng"),
39            u32::from_ne_bytes(*b"Guru"),
40            u32::from_ne_bytes(*b"Modi"),
41            u32::from_ne_bytes(*b"Shrd"),
42            u32::from_ne_bytes(*b"Sylo"),
43            u32::from_ne_bytes(*b"Tirh"),
44            u32::from_ne_bytes(*b"Ogam"),
45        ];
46        !JOINING.contains(&u32::from_ne_bytes(self.0))
47    }
48
49    fn of(script: UnicodeScript) -> Self {
50        PropertyNamesShortBorrowed::<UnicodeScript>::new()
51            .get(script)
52            .and_then(|name| name.as_bytes().try_into().ok())
53            .map_or(Self::COMMON, Self)
54    }
55}
56
57// The script of ASCII `text`: Latin when it holds a letter, else Common.
58pub(crate) fn ascii_script(text: &str) -> Script {
59    if text.bytes().any(|byte| byte.is_ascii_alphabetic()) {
60        Script::LATIN
61    } else {
62        Script::COMMON
63    }
64}
65
66// Pushes the script runs of `text` onto `runs`, each as the end of its byte
67// range offset by `offset` and its script, by the rules of UAX #24: common and
68// inherited characters (spaces, digits, punctuation, combining marks) join
69// the run around them, a closing bracket takes the script of its opening
70// bracket, and a character whose script extensions hold the run's script
71// stays in the run.
72pub(crate) fn script_runs(text: &str, offset: usize, runs: &mut Vec<(usize, Script)>) {
73    if text.is_ascii() {
74        runs.push((offset + text.len(), ascii_script(text)));
75        return;
76    }
77    let scripts = ScriptWithExtensions::new();
78    let mirroring = CodePointMapData::<BidiMirroringGlyph>::new();
79    let mut run = UnicodeScript::Common;
80    // Each open bracket's closing bracket and the script at the opening.
81    let mut open: SmallVec<[(char, UnicodeScript); 8]> = SmallVec::new();
82    for (at, ch) in text.char_indices() {
83        let mut script = scripts.get_script_val(ch);
84        if matches!(script, UnicodeScript::Common | UnicodeScript::Inherited) {
85            let bracket = mirroring.get(ch);
86            match bracket.paired_bracket_type {
87                BidiPairedBracketType::Open => {
88                    open.push((bracket.mirroring_glyph.unwrap_or(ch), run));
89                }
90                BidiPairedBracketType::Close => {
91                    if let Some(at) = open.iter().rposition(|(closing, _)| *closing == ch) {
92                        script = open[at].1;
93                        open.truncate(at);
94                    }
95                }
96                _ => {}
97            }
98        }
99        if matches!(
100            script,
101            UnicodeScript::Common | UnicodeScript::Inherited | UnicodeScript::Unknown
102        ) || script == run
103            || (run != UnicodeScript::Common && scripts.has_script(ch, run))
104        {
105            continue;
106        }
107        if run == UnicodeScript::Common {
108            for (_, opening) in &mut open {
109                if *opening == UnicodeScript::Common {
110                    *opening = script;
111                }
112            }
113        } else {
114            runs.push((offset + at, Script::of(run)));
115        }
116        run = script;
117    }
118    runs.push((offset + text.len(), Script::of(run)));
119}