#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Ltr,
Rtl,
}
pub fn strong_direction(ch: char) -> Option<Direction> {
match unicode_bidi::bidi_class(ch) {
unicode_bidi::BidiClass::L => Some(Direction::Ltr),
unicode_bidi::BidiClass::R | unicode_bidi::BidiClass::AL => Some(Direction::Rtl),
_ => None,
}
}
pub fn is_rtl_char(ch: char) -> bool {
matches!(
unicode_bidi::bidi_class(ch),
unicode_bidi::BidiClass::R | unicode_bidi::BidiClass::AL
)
}
pub fn base_direction(text: &str) -> Direction {
text.chars()
.find_map(strong_direction)
.unwrap_or(Direction::Ltr)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectionalRun {
pub text: String,
pub direction: Direction,
}
pub fn segment_runs(text: &str, base: Direction) -> Vec<DirectionalRun> {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() {
return Vec::new();
}
let strong: Vec<Option<Direction>> = chars.iter().map(|&ch| strong_direction(ch)).collect();
let mut next_strong = vec![None; strong.len()];
let mut next = None;
for index in (0..strong.len()).rev() {
next_strong[index] = next;
if let Some(direction) = strong[index] {
next = Some(direction);
}
}
let mut runs: Vec<DirectionalRun> = Vec::new();
let mut previous = None;
for (index, &ch) in chars.iter().enumerate() {
let direction = match strong[index] {
Some(direction) => {
previous = Some(direction);
direction
}
None => match (previous, next_strong[index]) {
(Some(left), Some(right)) if left == right => left,
_ => base,
},
};
match runs.last_mut() {
Some(run) if run.direction == direction => run.text.push(ch),
_ => runs.push(DirectionalRun {
text: ch.to_string(),
direction,
}),
}
}
runs
}
pub fn reorder_visual(runs: &[DirectionalRun], base: Direction) -> Vec<DirectionalRun> {
if runs.is_empty() {
return Vec::new();
}
let base_level: u8 = if base == Direction::Ltr { 0 } else { 1 };
let levels: Vec<u8> = runs
.iter()
.map(|run| {
let run_parity = u8::from(run.direction == Direction::Rtl);
if run_parity == base_level % 2 {
base_level
} else {
base_level + 1
}
})
.collect();
let mut order: Vec<usize> = (0..runs.len()).collect();
let max_level = levels.iter().copied().max().unwrap_or(0);
let min_odd = levels
.iter()
.copied()
.filter(|level| level % 2 == 1)
.min()
.unwrap_or(max_level + 1);
let mut level = max_level;
while level >= min_odd {
let mut index = 0;
while index < order.len() {
if levels[order[index]] >= level {
let start = index;
while index < order.len() && levels[order[index]] >= level {
index += 1;
}
order[start..index].reverse();
} else {
index += 1;
}
}
level -= 1;
}
order
.into_iter()
.map(|index| {
let run = &runs[index];
let text = if levels[index] % 2 == 1 {
run.text.chars().rev().collect()
} else {
run.text.clone()
};
DirectionalRun {
text,
direction: run.direction,
}
})
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BidiClass {
L,
R,
Al,
En,
Es,
Et,
An,
Cs,
Nsm,
On,
}
pub fn bidi_class(ch: char) -> BidiClass {
match unicode_bidi::bidi_class(ch) {
unicode_bidi::BidiClass::L => BidiClass::L,
unicode_bidi::BidiClass::R => BidiClass::R,
unicode_bidi::BidiClass::AL => BidiClass::Al,
unicode_bidi::BidiClass::EN => BidiClass::En,
unicode_bidi::BidiClass::ES => BidiClass::Es,
unicode_bidi::BidiClass::ET => BidiClass::Et,
unicode_bidi::BidiClass::AN => BidiClass::An,
unicode_bidi::BidiClass::CS => BidiClass::Cs,
unicode_bidi::BidiClass::NSM => BidiClass::Nsm,
_ => BidiClass::On,
}
}
pub fn resolve_weak_types(classes: &[BidiClass], base: Direction) -> Vec<BidiClass> {
let sor = if base == Direction::Rtl {
BidiClass::R
} else {
BidiClass::L
};
let mut types = classes.to_vec();
for index in 0..types.len() {
if types[index] == BidiClass::Nsm {
types[index] = if index == 0 { sor } else { types[index - 1] };
}
}
let mut last_strong = sor;
for class in &mut types {
match *class {
BidiClass::R | BidiClass::L | BidiClass::Al => last_strong = *class,
BidiClass::En if last_strong == BidiClass::Al => *class = BidiClass::An,
_ => {}
}
}
for class in &mut types {
if *class == BidiClass::Al {
*class = BidiClass::R;
}
}
for index in 1..types.len().saturating_sub(1) {
let (prev, next) = (types[index - 1], types[index + 1]);
types[index] = match types[index] {
BidiClass::Es if prev == BidiClass::En && next == BidiClass::En => BidiClass::En,
BidiClass::Cs if prev == BidiClass::En && next == BidiClass::En => BidiClass::En,
BidiClass::Cs if prev == BidiClass::An && next == BidiClass::An => BidiClass::An,
other => other,
};
}
let len = types.len();
let mut index = 0;
while index < len {
if types[index] == BidiClass::Et {
let start = index;
while index < len && types[index] == BidiClass::Et {
index += 1;
}
let touches_en = (start > 0 && types[start - 1] == BidiClass::En)
|| (index < len && types[index] == BidiClass::En);
if touches_en {
for class in &mut types[start..index] {
*class = BidiClass::En;
}
}
} else {
index += 1;
}
}
for class in &mut types {
if matches!(*class, BidiClass::Es | BidiClass::Et | BidiClass::Cs) {
*class = BidiClass::On;
}
}
let mut last_strong = sor;
for class in &mut types {
match *class {
BidiClass::R | BidiClass::L => last_strong = *class,
BidiClass::En if last_strong == BidiClass::L => *class = BidiClass::L,
_ => {}
}
}
types
}
pub fn mirror_char(ch: char) -> char {
match ch {
'(' => ')',
')' => '(',
'[' => ']',
']' => '[',
'{' => '}',
'}' => '{',
'<' => '>',
'>' => '<',
'\u{00AB}' => '\u{00BB}', '\u{00BB}' => '\u{00AB}',
'\u{2039}' => '\u{203A}', '\u{203A}' => '\u{2039}',
'\u{2264}' => '\u{2265}', '\u{2265}' => '\u{2264}',
'\u{2308}' => '\u{2309}', '\u{2309}' => '\u{2308}',
'\u{230A}' => '\u{230B}', '\u{230B}' => '\u{230A}',
'\u{27E8}' => '\u{27E9}', '\u{27E9}' => '\u{27E8}',
other => other,
}
}
pub fn is_mirrored(ch: char) -> bool {
mirror_char(ch) != ch
}
pub fn display_order(text: &str) -> String {
let info = unicode_bidi::BidiInfo::new(text, None);
let mut output = String::with_capacity(text.len());
for paragraph in &info.paragraphs {
output.push_str(&info.reorder_line(paragraph, paragraph.range.clone()));
}
output
}
#[cfg(test)]
mod tests {
use super::*;
const HEBREW: &str = "שלום";
const ARABIC: &str = "سلام";
#[test]
fn classifies_strong_directions() {
assert_eq!(strong_direction('a'), Some(Direction::Ltr));
assert_eq!(strong_direction('Ж'), Some(Direction::Ltr)); assert_eq!(strong_direction('語'), Some(Direction::Ltr)); assert_eq!(strong_direction('א'), Some(Direction::Rtl)); assert_eq!(strong_direction('ا'), Some(Direction::Rtl)); assert_eq!(strong_direction('5'), None);
assert_eq!(strong_direction(' '), None);
assert_eq!(strong_direction('!'), None);
assert_eq!(strong_direction('\u{1E900}'), Some(Direction::Rtl)); }
#[test]
fn base_direction_uses_first_strong_character() {
assert_eq!(base_direction("hello"), Direction::Ltr);
assert_eq!(base_direction(HEBREW), Direction::Rtl);
assert_eq!(base_direction("123 hello"), Direction::Ltr);
assert_eq!(base_direction(" \"שלום\""), Direction::Rtl);
assert_eq!(base_direction("123 !!!"), Direction::Ltr);
assert_eq!(base_direction(""), Direction::Ltr);
}
#[test]
fn segments_pure_runs() {
let runs = segment_runs("abc", Direction::Ltr);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].direction, Direction::Ltr);
assert_eq!(runs[0].text, "abc");
let runs = segment_runs(HEBREW, Direction::Rtl);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].direction, Direction::Rtl);
}
#[test]
fn n1_keeps_neutrals_between_matching_strongs() {
let runs = segment_runs("a 1 b", Direction::Rtl);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].direction, Direction::Ltr);
assert_eq!(runs[0].text, "a 1 b");
}
#[test]
fn mixed_script_splits_into_runs() {
let input = format!("abc {HEBREW}");
let runs = segment_runs(&input, Direction::Ltr);
assert_eq!(runs.len(), 2);
assert_eq!(runs[0].direction, Direction::Ltr);
assert_eq!(runs[0].text, "abc ");
assert_eq!(runs[1].direction, Direction::Rtl);
assert_eq!(runs[1].text, HEBREW);
}
#[test]
fn arabic_is_right_to_left() {
assert_eq!(base_direction(ARABIC), Direction::Rtl);
let runs = segment_runs(ARABIC, Direction::Rtl);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].direction, Direction::Rtl);
}
#[test]
fn empty_text_has_no_runs() {
assert!(segment_runs("", Direction::Ltr).is_empty());
}
fn visual(text: &str, base: Direction) -> String {
reorder_visual(&segment_runs(text, base), base)
.iter()
.map(|run| run.text.as_str())
.collect()
}
#[test]
fn reorder_pure_ltr_is_unchanged() {
assert_eq!(visual("abc", Direction::Ltr), "abc");
}
#[test]
fn reorder_reverses_single_rtl_run() {
let expected: String = HEBREW.chars().rev().collect();
assert_eq!(visual(HEBREW, Direction::Rtl), expected);
}
#[test]
fn reorder_ltr_base_with_embedded_rtl() {
let hebrew_rev: String = HEBREW.chars().rev().collect();
assert_eq!(
visual(&format!("abc{HEBREW}"), Direction::Ltr),
format!("abc{hebrew_rev}")
);
}
#[test]
fn reorder_rtl_base_with_embedded_ltr() {
assert_eq!(visual("אbc", Direction::Rtl), "bcא");
}
#[test]
fn reorder_empty_is_empty() {
assert!(reorder_visual(&[], Direction::Ltr).is_empty());
}
use BidiClass::*;
#[test]
fn bidi_class_classifies_numbers_and_letters() {
assert_eq!(bidi_class('5'), En);
assert_eq!(bidi_class('\u{0665}'), An); assert_eq!(bidi_class('ا'), Al); assert_eq!(bidi_class('א'), R); assert_eq!(bidi_class('a'), L);
assert_eq!(bidi_class('+'), Es);
assert_eq!(bidi_class(','), Cs);
assert_eq!(bidi_class('$'), Et);
}
#[test]
fn w1_nsm_takes_previous_type() {
assert_eq!(resolve_weak_types(&[L, Nsm], Direction::Ltr), vec![L, L]);
assert_eq!(resolve_weak_types(&[Nsm], Direction::Rtl), vec![R]);
}
#[test]
fn w2_w3_arabic_number_and_letter_resolution() {
assert_eq!(resolve_weak_types(&[Al, En], Direction::Rtl), vec![R, An]);
assert_eq!(resolve_weak_types(&[Al], Direction::Rtl), vec![R]);
}
#[test]
fn w4_single_separator_joins_numbers() {
assert_eq!(
resolve_weak_types(&[En, Es, En], Direction::Rtl),
vec![En, En, En]
);
assert_eq!(
resolve_weak_types(&[En, Cs, En], Direction::Rtl),
vec![En, En, En]
);
assert_eq!(
resolve_weak_types(&[An, Cs, An], Direction::Rtl),
vec![An, An, An]
);
assert_eq!(
resolve_weak_types(&[En, Es, Es, En], Direction::Rtl),
vec![En, On, On, En]
);
}
#[test]
fn w5_terminators_adjacent_to_numbers() {
assert_eq!(resolve_weak_types(&[Et, En], Direction::Rtl), vec![En, En]);
assert_eq!(
resolve_weak_types(&[En, Et, Et], Direction::Rtl),
vec![En, En, En]
);
assert_eq!(resolve_weak_types(&[Et], Direction::Rtl), vec![On]);
}
#[test]
fn w7_european_number_after_left_becomes_left() {
assert_eq!(resolve_weak_types(&[L, En], Direction::Ltr), vec![L, L]);
assert_eq!(resolve_weak_types(&[R, En], Direction::Rtl), vec![R, En]);
}
#[test]
fn mirror_char_swaps_paired_punctuation() {
assert_eq!(mirror_char('('), ')');
assert_eq!(mirror_char(')'), '(');
assert_eq!(mirror_char('['), ']');
assert_eq!(mirror_char('<'), '>');
assert_eq!(mirror_char('\u{00AB}'), '\u{00BB}'); assert_eq!(mirror_char('\u{2265}'), '\u{2264}'); for ch in ['(', '[', '{', '<', '\u{00AB}', '\u{2039}', '\u{27E8}'] {
assert_eq!(mirror_char(mirror_char(ch)), ch);
}
}
#[test]
fn non_mirrored_chars_are_unchanged() {
assert_eq!(mirror_char('a'), 'a');
assert_eq!(mirror_char('5'), '5');
assert_eq!(mirror_char('\u{0627}'), '\u{0627}'); assert!(!is_mirrored('a'));
assert!(is_mirrored('('));
}
#[test]
fn display_order_leaves_ltr_unchanged() {
assert_eq!(display_order("abc(def)"), "abc(def)");
assert_eq!(display_order(""), "");
}
#[test]
fn display_order_reverses_a_pure_rtl_run() {
let expected: String = HEBREW.chars().rev().collect();
assert_eq!(display_order(HEBREW), expected);
}
#[test]
fn display_order_leaves_mirroring_to_the_renderer() {
let input = format!("({HEBREW})");
let expected = format!("){}(", HEBREW.chars().rev().collect::<String>());
assert_eq!(display_order(&input), expected);
}
}