#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Direction {
#[default]
Auto,
Ltr,
Rtl,
}
impl Direction {
fn level(self) -> Option<unicode_bidi::Level> {
match self {
Direction::Auto => None,
Direction::Ltr => Some(unicode_bidi::Level::ltr()),
Direction::Rtl => Some(unicode_bidi::Level::rtl()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Run {
pub start: usize,
pub length: usize,
pub is_rtl: bool,
}
#[derive(Debug)]
pub struct Resolver {
text: String,
offsets: Vec<usize>,
direction: Direction,
}
impl Resolver {
#[must_use]
pub fn new(chars: &[u32], direction: Direction) -> Resolver {
let mut text = String::new();
let mut offsets = Vec::with_capacity(chars.len() + 1);
for code in chars {
offsets.push(text.len());
text.push(char::from_u32(*code).unwrap_or(char::REPLACEMENT_CHARACTER));
}
offsets.push(text.len());
Resolver {
text,
offsets,
direction,
}
}
#[must_use]
pub fn visual_runs(&self, start: usize, length: usize) -> Vec<Run> {
if length == 0 || start >= self.offsets.len().saturating_sub(1) {
return Vec::new();
}
let end = (start + length).min(self.offsets.len() - 1);
let (Some(from), Some(to)) = (self.offsets.get(start), self.offsets.get(end)) else {
return Vec::new();
};
let info = unicode_bidi::BidiInfo::new(&self.text, self.direction.level());
let Some(paragraph) = info.paragraphs.first() else {
return Vec::new();
};
let (levels, ranges) = info.visual_runs(paragraph, *from..*to);
ranges
.into_iter()
.filter_map(|range| {
let run_start = self.char_index(range.start)?;
let run_end = self.char_index(range.end)?;
let is_rtl = levels
.get(range.start)
.is_some_and(unicode_bidi::Level::is_rtl);
(run_end > run_start).then_some(Run {
start: run_start,
length: run_end - run_start,
is_rtl,
})
})
.collect()
}
fn char_index(&self, byte: usize) -> Option<usize> {
self.offsets.iter().position(|offset| *offset == byte)
}
}
#[cfg(test)]
mod tests {
use super::{Direction, Resolver};
fn order(text: &str, direction: Direction) -> Vec<usize> {
let chars: Vec<u32> = text.chars().map(|c| c as u32).collect();
let resolver = Resolver::new(&chars, direction);
let mut out = Vec::new();
for run in resolver.visual_runs(0, chars.len()) {
let span = run.start..run.start + run.length;
if run.is_rtl {
out.extend(span.rev());
} else {
out.extend(span);
}
}
out
}
const LATIN_FIRST: &str = "A_B_C_\u{5D0}_\u{5D1}";
const HEBREW_FIRST: &str = "\u{5D0}_\u{5D1}_\u{5D2}_A_B";
#[test]
fn latin_first_reads_left_to_right_until_the_hebrew_run() {
assert_eq!(
order(LATIN_FIRST, Direction::Auto),
[0, 1, 2, 3, 4, 5, 8, 7, 6]
);
assert_eq!(
order(LATIN_FIRST, Direction::Ltr),
[0, 1, 2, 3, 4, 5, 8, 7, 6]
);
assert_eq!(
order(LATIN_FIRST, Direction::Rtl),
[8, 7, 6, 5, 0, 1, 2, 3, 4]
);
}
#[test]
fn hebrew_first_puts_the_whole_paragraph_the_other_way_round() {
assert_eq!(
order(HEBREW_FIRST, Direction::Auto),
[6, 7, 8, 5, 4, 3, 2, 1, 0]
);
assert_eq!(
order(HEBREW_FIRST, Direction::Ltr),
[4, 3, 2, 1, 0, 5, 6, 7, 8]
);
assert_eq!(
order(HEBREW_FIRST, Direction::Rtl),
[6, 7, 8, 5, 4, 3, 2, 1, 0]
);
}
#[test]
fn an_empty_line_reports_no_runs_at_all() {
let resolver = Resolver::new(&[], Direction::Auto);
assert!(resolver.visual_runs(0, 0).is_empty());
let resolver = Resolver::new(&[65], Direction::Auto);
assert!(resolver.visual_runs(0, 0).is_empty());
assert!(resolver.visual_runs(4, 2).is_empty());
}
}