use std::ops::Range;
use unicode_bidi::{BidiInfo, Level};
use crate::types::Direction;
pub(crate) fn visual_order(levels: &[u8]) -> Vec<usize> {
let n = levels.len();
let mut order: Vec<usize> = (0..n).collect();
let max_level = match levels.iter().copied().max() {
Some(m) => m,
None => return order,
};
let Some(min_odd) = levels.iter().copied().filter(|l| l % 2 == 1).min() else {
return order;
};
for lvl in (min_odd..=max_level).rev() {
let mut i = 0;
while i < n {
if levels[i] >= lvl {
let start = i;
while i < n && levels[i] >= lvl {
i += 1;
}
order[start..i].reverse();
} else {
i += 1;
}
}
}
order
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct BidiRun {
pub byte_range: Range<usize>,
pub level: u8,
pub direction: Direction,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct LineBidi {
pub base_direction: Direction,
pub logical_runs: Vec<BidiRun>,
}
fn to_level(d: Direction) -> Level {
match d {
Direction::Ltr => Level::ltr(),
Direction::Rtl => Level::rtl(),
}
}
pub(crate) fn resolve_line(text: &str, base: Option<Direction>) -> LineBidi {
if text.is_empty() {
return LineBidi {
base_direction: base.unwrap_or_default(),
logical_runs: Vec::new(),
};
}
let info = BidiInfo::new(text, base.map(to_level));
let para = &info.paragraphs[0];
let base_direction = if para.level.is_rtl() {
Direction::Rtl
} else {
Direction::Ltr
};
let (levels, runs) = info.visual_runs(para, para.range.clone());
let mut logical_runs: Vec<BidiRun> = runs
.into_iter()
.map(|r| {
let level = levels[r.start].number();
BidiRun {
byte_range: r,
level,
direction: if level.is_multiple_of(2) {
Direction::Ltr
} else {
Direction::Rtl
},
}
})
.collect();
logical_runs.sort_by_key(|r| r.byte_range.start);
LineBidi {
base_direction,
logical_runs,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run_levels(b: &LineBidi) -> Vec<(Range<usize>, u8, Direction)> {
b.logical_runs
.iter()
.map(|r| (r.byte_range.clone(), r.level, r.direction))
.collect()
}
#[test]
fn pure_ltr_single_run_level_zero() {
let b = resolve_line("abc", None);
assert_eq!(b.base_direction, Direction::Ltr);
assert_eq!(run_levels(&b), vec![(0..3, 0, Direction::Ltr)]);
assert_eq!(visual_order(&[0]), vec![0]);
}
#[test]
fn pure_rtl_hebrew_single_run_level_one() {
let b = resolve_line("אבג", None);
assert_eq!(b.base_direction, Direction::Rtl);
assert_eq!(run_levels(&b), vec![(0..6, 1, Direction::Rtl)]);
}
#[test]
fn ltr_base_with_trailing_rtl_run() {
let b = resolve_line("abc אבג", None);
assert_eq!(b.base_direction, Direction::Ltr);
assert_eq!(
run_levels(&b),
vec![(0..4, 0, Direction::Ltr), (4..10, 1, Direction::Rtl)]
);
assert_eq!(visual_order(&[0, 1]), vec![0, 1]);
}
#[test]
fn rtl_base_with_embedded_latin_reorders_visually() {
let b = resolve_line("אבג abc", None);
assert_eq!(b.base_direction, Direction::Rtl);
assert_eq!(
run_levels(&b),
vec![(0..7, 1, Direction::Rtl), (7..10, 2, Direction::Ltr)]
);
assert_eq!(visual_order(&[1, 2]), vec![1, 0]);
}
#[test]
fn rtl_base_with_european_numbers_groups_ltr() {
let b = resolve_line("אבג 123", None);
assert_eq!(b.base_direction, Direction::Rtl);
let runs = run_levels(&b);
let digit_run = runs.last().unwrap();
assert_eq!(digit_run.0, 7..10);
assert_eq!(digit_run.2, Direction::Ltr, "EN group reads LTR");
assert_eq!(digit_run.1 % 2, 0, "EN run is even-level");
}
#[test]
fn arabic_indic_digits_resolve_at_paragraph_level_not_isolated_guess() {
let b = resolve_line("abc ١٢٣ خ", None);
assert_eq!(b.base_direction, Direction::Ltr);
let digit_run = b
.logical_runs
.iter()
.find(|r| r.byte_range.start == 4)
.expect("digit run starts after 'abc '");
assert_eq!(digit_run.level % 2, 0, "AN run resolves to even level");
assert_eq!(digit_run.direction, Direction::Ltr);
let letter_run = b.logical_runs.last().unwrap();
assert_eq!(letter_run.level % 2, 1, "trailing Arabic letter is RTL");
assert_eq!(letter_run.direction, Direction::Rtl);
}
#[test]
fn isolate_run_does_not_panic_and_keeps_base() {
let b = resolve_line("a\u{2067}ب\u{2069}c", None);
assert_eq!(b.base_direction, Direction::Ltr);
assert!(!b.logical_runs.is_empty());
assert_eq!(b.logical_runs.first().unwrap().byte_range.start, 0);
assert_eq!(
b.logical_runs.last().unwrap().byte_range.end,
"a\u{2067}ب\u{2069}c".len()
);
}
#[test]
fn forced_base_overrides_first_strong() {
let b = resolve_line("abc", Some(Direction::Rtl));
assert_eq!(b.base_direction, Direction::Rtl);
}
#[test]
fn empty_line_has_no_runs() {
let b = resolve_line("", None);
assert!(b.logical_runs.is_empty());
}
}