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");
}
}