use crate::compat::Vec;
pub fn visual_order(text: &str, clusters: &[(usize, usize)]) -> Vec<usize> {
reorder(text, clusters)
}
#[cfg(feature = "text-bidi")]
fn reorder(text: &str, clusters: &[(usize, usize)]) -> Vec<usize> {
let mut order: Vec<usize> = (0..clusters.len()).collect();
if clusters.len() <= 1 {
return order;
}
let info = unicode_bidi::BidiInfo::new(text, None);
let levels: Vec<u8> = clusters
.iter()
.map(|&(start, _)| info.levels.get(start).map_or(0, |l| l.number()))
.collect();
let highest = levels.iter().copied().max().unwrap_or(0);
let Some(lowest_odd) = levels.iter().copied().filter(|level| level % 2 == 1).min() else {
return order;
};
for level in (lowest_odd..=highest).rev() {
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;
}
order[start..i].reverse();
}
}
order
}
#[cfg(not(feature = "text-bidi"))]
fn reorder(text: &str, clusters: &[(usize, usize)]) -> Vec<usize> {
let _ = text;
(0..clusters.len()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::String;
fn per_char(text: &str) -> Vec<(usize, usize)> {
text.char_indices().map(|(i, c)| (i, i + c.len_utf8())).collect()
}
fn visual_string(text: &str) -> String {
let ranges = per_char(text);
let order = visual_order(text, &ranges);
order.iter().map(|&i| &text[ranges[i].0..ranges[i].1]).collect()
}
#[test]
fn a_left_to_right_line_is_untouched() {
assert_eq!(visual_string("Hello, world"), "Hello, world");
assert_eq!(visual_string(""), "");
assert_eq!(visual_string("a"), "a");
}
#[test]
fn the_order_is_always_a_permutation() {
for text in ["Hello", "עברית", "abc עברית def", "مرحبا بالعالم", "123", "a1ב2c3"]
{
let ranges = per_char(text);
let mut order = visual_order(text, &ranges);
order.sort_unstable();
assert_eq!(order, (0..ranges.len()).collect::<Vec<_>>(), "{text}");
}
}
#[test]
fn a_hebrew_run_is_reversed() {
let text = "\u{05e9}\u{05dc}\u{05d5}\u{05dd}";
assert_eq!(
visual_string(text),
"\u{05dd}\u{05d5}\u{05dc}\u{05e9}",
"an all-RTL line must be painted right to left"
);
}
#[test]
fn a_latin_embedding_keeps_the_latin_run_readable() {
let text = "\u{05d0}\u{05d1}\u{05d2} abc";
let visual = visual_string(text);
assert!(visual.contains("abc"), "the embedded run must not itself reverse: {visual}");
assert!(
visual.starts_with("abc"),
"and it moves to the visual start of an RTL line: {visual}"
);
}
#[test]
fn combining_marks_travel_with_their_base() {
let text = "a\u{0301}b";
let ranges = vec![(0, 3), (3, 4)];
assert_eq!(visual_order(text, &ranges), vec![0, 1], "an LTR line stays in order");
}
#[test]
fn an_arabic_run_is_reversed() {
let text = "\u{0645}\u{0631}\u{062d}\u{0628}\u{0627}";
let reversed: String = text.chars().rev().collect();
assert_eq!(visual_string(text), reversed, "an all-RTL Arabic line must be reversed");
}
#[test]
fn a_mixed_arabic_latin_digit_line_keeps_each_run_readable() {
let text = "\u{0645}\u{0631}\u{062d}\u{0628}\u{0627} abc 123";
let visual = visual_string(text);
assert!(visual.contains("abc"), "the Latin run must stay readable: {visual}");
assert!(visual.contains("123"), "the digits must stay in order: {visual}");
assert!(!visual.contains("321"), "digits must not reverse: {visual}");
let mut a: Vec<char> = visual.chars().collect();
let mut b: Vec<char> = text.chars().collect();
a.sort_unstable();
b.sort_unstable();
assert_eq!(a, b, "reordering must not drop or duplicate a cluster");
}
#[test]
fn a_long_mixed_line_is_a_permutation() {
let text = "The quick \u{05e9}\u{05dc}\u{05d5}\u{05dd} brown fox \u{0645}\u{0631}\u{062d}\u{0628}\u{0627} jumps 42 over 7 lazy dogs \u{05d0}\u{05d1}\u{05d2}";
let ranges = per_char(text);
let mut order = visual_order(text, &ranges);
order.sort_unstable();
assert_eq!(
order,
(0..ranges.len()).collect::<Vec<_>>(),
"a long mixed line must reorder to a permutation"
);
}
#[test]
fn a_line_with_no_available_face_still_reorders_as_a_permutation() {
let text = "\u{12000}\u{12001}\u{12002}";
let ranges = per_char(text);
let mut order = visual_order(text, &ranges);
order.sort_unstable();
assert_eq!(
order,
(0..ranges.len()).collect::<Vec<_>>(),
"a face-less line must still be a valid permutation"
);
}
#[test]
fn each_wrapped_row_reorders_independently() {
let first = "\u{05e9}\u{05dc}\u{05d5}\u{05dd}"; let second = "\u{05e2}\u{05d5}\u{05dc}\u{05dd}"; for row in [first, second] {
let ranges = per_char(row);
let mut order = visual_order(row, &ranges);
order.sort_unstable();
assert_eq!(
order,
(0..ranges.len()).collect::<Vec<_>>(),
"each wrapped row must reorder to a permutation of itself"
);
let reversed: String = row.chars().rev().collect();
assert_eq!(
visual_string(row),
reversed,
"the row's own order must not depend on the split"
);
}
}
}