Skip to main content

cranpose_text/
bidi_level.rs

1use icu_properties::{CodePointMapData, props::BidiClass};
2use unicode_bidi::{Level, ParagraphBidiInfo};
3
4/// The embedding level the Unicode bidirectional algorithm gives text: even
5/// levels run left to right, odd levels right to left.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
7pub struct BidiLevel(u8);
8
9impl BidiLevel {
10    /// The level of a left-to-right paragraph.
11    pub const LTR: Self = Self(0);
12    /// The level of a right-to-left paragraph.
13    pub const RTL: Self = Self(1);
14
15    /// The level as a number, at most 126.
16    pub fn value(self) -> u8 {
17        self.0
18    }
19
20    /// Whether text at this level runs right to left.
21    pub fn is_rtl(self) -> bool {
22        self.0 & 1 == 1
23    }
24}
25
26// Pushes the level runs of `paragraph`, whose base level is `base`, onto
27// `runs`, each as the end of its byte range offset by `offset` and its level.
28// A left-to-right paragraph without a right-to-left, Arabic-number or explicit
29// directional character is one run at level 0, as the algorithm would find.
30pub(crate) fn level_runs(
31    paragraph: &str,
32    base: BidiLevel,
33    offset: usize,
34    runs: &mut Vec<(usize, BidiLevel)>,
35) {
36    let classes = CodePointMapData::<BidiClass>::new();
37    if base == BidiLevel::LTR
38        && (paragraph.is_ascii() || !paragraph.chars().any(|ch| turns(classes.get(ch))))
39    {
40        runs.push((offset + paragraph.len(), base));
41        return;
42    }
43    let base = if base.is_rtl() {
44        Level::rtl()
45    } else {
46        Level::ltr()
47    };
48    let info = ParagraphBidiInfo::new_with_data_source(&classes, paragraph, Some(base));
49    let mut current: Option<Level> = None;
50    for (at, &level) in info.levels.iter().enumerate() {
51        if let Some(previous) = current
52            && previous != level
53        {
54            runs.push((offset + at, BidiLevel(previous.number())));
55        }
56        current = Some(level);
57    }
58    if let Some(level) = current {
59        runs.push((offset + paragraph.len(), BidiLevel(level.number())));
60    }
61}
62
63// Puts `runs` of one line, in text order, in the order they are drawn from
64// left to right, by rule L2 of UAX #9: from the highest level down to the
65// lowest odd one, every sequence of runs at that level or higher reverses.
66pub(crate) fn reorder_visually<T>(runs: &mut [T], level: impl Fn(&T) -> BidiLevel) {
67    let Some(highest) = runs.iter().map(|run| level(run).0).max() else {
68        return;
69    };
70    let lowest = runs.iter().map(|run| level(run).0).min().unwrap_or(0) | 1;
71    for current in (lowest..=highest).rev() {
72        let mut start = 0;
73        while start < runs.len() {
74            if level(&runs[start]).0 < current {
75                start += 1;
76                continue;
77            }
78            let mut end = start;
79            while end < runs.len() && level(&runs[end]).0 >= current {
80                end += 1;
81            }
82            runs[start..end].reverse();
83            start = end;
84        }
85    }
86}
87
88// Whether a character of `class` can give text in a left-to-right paragraph a
89// level other than 0.
90fn turns(class: BidiClass) -> bool {
91    matches!(
92        class,
93        BidiClass::RightToLeft
94            | BidiClass::ArabicLetter
95            | BidiClass::ArabicNumber
96            | BidiClass::RightToLeftEmbedding
97            | BidiClass::RightToLeftOverride
98            | BidiClass::RightToLeftIsolate
99            | BidiClass::FirstStrongIsolate
100            | BidiClass::LeftToRightEmbedding
101            | BidiClass::LeftToRightOverride
102            | BidiClass::LeftToRightIsolate
103            | BidiClass::PopDirectionalFormat
104            | BidiClass::PopDirectionalIsolate
105    )
106}