use unicode_bidi::BidiInfo;
pub(crate) fn levels(text: &str) -> Vec<u8> {
let info = BidiInfo::new(text, None);
text.char_indices()
.map(|(byte, _)| info.levels[byte].number())
.collect()
}
pub(crate) fn reorder_visual<T>(items: &mut [T], level_of: impl Fn(&T) -> u8) {
let levels: Vec<u8> = items.iter().map(level_of).collect();
let (Some(&max), Some(lowest_odd)) = (
levels.iter().max(),
levels.iter().copied().filter(|l| l % 2 == 1).min(),
) else {
return;
};
let mut level = max;
while level >= lowest_odd {
let mut i = 0;
while i < levels.len() {
if levels[i] < level {
i += 1;
continue;
}
let start = i;
while i < levels.len() && levels[i] >= level {
i += 1;
}
items[start..i].reverse();
}
level -= 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn levels_are_even_for_latin_and_odd_for_hebrew() {
assert!(levels("abc").iter().all(|l| l.is_multiple_of(2)));
assert!(levels("תל אביב").iter().all(|l| !l.is_multiple_of(2)));
}
#[test]
fn a_paragraph_resolves_its_own_direction() {
let text = "תל\nabc";
let lv = levels(text);
let chars: Vec<char> = text.chars().collect();
let latin = chars.iter().position(|&c| c == 'a').unwrap();
assert!(!lv[0].is_multiple_of(2), "the Hebrew paragraph is RTL");
assert!(lv[latin].is_multiple_of(2), "the Latin paragraph is LTR");
}
#[test]
fn reorder_reverses_a_right_to_left_run() {
let mut items = vec![0, 1, 2, 3];
reorder_visual(&mut items, |_| 1);
assert_eq!(items, vec![3, 2, 1, 0]);
}
#[test]
fn reorder_leaves_a_left_to_right_line_alone() {
let mut items = vec![0, 1, 2, 3];
reorder_visual(&mut items, |_| 0);
assert_eq!(items, vec![0, 1, 2, 3]);
}
#[test]
fn a_number_inside_a_right_to_left_run_keeps_its_digit_order() {
let levels = [1u8, 1, 2, 2, 1];
let mut items = vec![0usize, 1, 2, 3, 4];
reorder_visual(&mut items, |&i| levels[i]);
assert_eq!(items, vec![4, 2, 3, 1, 0]);
}
}