use unicode_bidi::BidiClass::{
self, AL, AN, B, BN, FSI, LRE, LRI, LRO, PDF, PDI, R, RLE, RLI, RLO, S, WS,
};
use unicode_bidi::{BidiInfo, Level, bidi_class};
fn needs_bidi(chars: &[char]) -> bool {
chars.iter().any(|c| {
matches!(
bidi_class(*c),
R | AL | AN | LRE | RLE | LRO | RLO | PDF | LRI | RLI | FSI | PDI
)
})
}
pub(crate) fn levels(text: &str, chars: &[char], rtl: bool) -> Vec<u8> {
let base = u8::from(rtl);
if !rtl && !needs_bidi(chars) {
return vec![0; chars.len()];
}
let info = BidiInfo::new(text, Some(if rtl { Level::rtl() } else { Level::ltr() }));
text.char_indices()
.map(|(b, _)| info.levels.get(b).map(|l| l.number()).unwrap_or(base))
.collect()
}
fn l1_whitespace(c: BidiClass) -> bool {
matches!(
c,
WS | FSI | LRI | RLI | PDI | BN | LRE | RLE | LRO | RLO | PDF
)
}
pub(crate) fn line_levels(chars: &[char], levels: &[u8], para_level: u8) -> Vec<u8> {
let mut out: Vec<u8> = levels.to_vec();
out.resize(chars.len(), para_level);
let mut trailing = true;
for (i, c) in chars.iter().enumerate().rev() {
let class = bidi_class(*c);
if matches!(class, S | B) {
if let Some(l) = out.get_mut(i) {
*l = para_level;
}
trailing = true;
} else if l1_whitespace(class) {
if trailing && let Some(l) = out.get_mut(i) {
*l = para_level;
}
} else {
trailing = false;
}
}
out
}
pub(crate) fn visual_order(levels: &[u8]) -> Vec<usize> {
let mut order: Vec<usize> = (0..levels.len()).collect();
let Some(&max) = levels.iter().max() else {
return order;
};
let min_odd = levels
.iter()
.copied()
.filter(|l| l % 2 == 1)
.min()
.unwrap_or(max.saturating_add(1));
let mut lvl = max;
while lvl >= min_odd && lvl > 0 {
let mut i = 0;
while i < order.len() {
let at = |k: usize| {
order
.get(k)
.and_then(|&j| levels.get(j))
.copied()
.unwrap_or(0)
};
if at(i) >= lvl {
let start = i;
while i < order.len() && at(i) >= lvl {
i += 1;
}
if let Some(run) = order.get_mut(start..i) {
run.reverse();
}
} else {
i += 1;
}
}
lvl -= 1;
}
order
}
#[cfg(test)]
mod tests {
use super::*;
fn lv(s: &str, rtl: bool) -> Vec<u8> {
let chars: Vec<char> = s.chars().collect();
levels(s, &chars, rtl)
}
#[test]
fn mixed_arabic_latin_numbers_and_brackets() {
let s = "سنة 2024 (Retrait) فقط";
let l = lv(s, true);
let chars: Vec<char> = s.chars().collect();
assert_eq!(l.len(), chars.len());
assert_eq!(l[0], 1);
assert_eq!(&l[4..8], &[2, 2, 2, 2]);
assert_eq!(&l[10..17], &[2; 7]);
assert_eq!(l[19], 1);
assert_eq!(l[9] % 2, 1, "( is right-to-left");
assert_eq!(l[17] % 2, 1, ") is right-to-left");
let order = visual_order(&line_levels(&chars, &l, 1));
let vis: String = order.iter().map(|&i| chars[i]).collect();
assert_eq!(vis, "طقف )Retrait( 2024 ةنس");
}
#[test]
fn ltr_paragraph_fast_path_and_embedded_rtl() {
assert_eq!(lv("Hello 123", false), vec![0; 9]);
let s = "ab سن cd";
let chars: Vec<char> = s.chars().collect();
let l = lv(s, false);
assert_eq!(l, vec![0, 0, 0, 1, 1, 0, 0, 0]);
let vis: String = visual_order(&l).iter().map(|&i| chars[i]).collect();
assert_eq!(vis, "ab نس cd");
assert_eq!(lv("ab", true), vec![2, 2]);
}
#[test]
fn l1_resets_trailing_whitespace_and_tabs() {
let chars: Vec<char> = "ab\tcd ".chars().collect();
let l = line_levels(&chars, &[2, 2, 2, 2, 2, 2, 2], 1);
assert_eq!(l, vec![2, 2, 1, 2, 2, 1, 1]);
assert_eq!(line_levels(&['a', 'b'], &[], 1), vec![1, 1]);
}
#[test]
fn visual_order_edge_cases() {
assert!(visual_order(&[]).is_empty());
assert_eq!(visual_order(&[0, 0]), vec![0, 1]);
assert_eq!(visual_order(&[1, 1, 1]), vec![2, 1, 0]);
assert_eq!(visual_order(&[1, 2, 2, 1]), vec![3, 1, 2, 0]);
assert_eq!(visual_order(&[125, 125]), vec![1, 0]);
}
}