use std::borrow::Cow;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Class {
Left,
Right,
EuropeanNumber,
ArabicNumber,
Mark,
Neutral,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Direction {
Left,
Right,
}
#[derive(Clone, Copy, Debug)]
struct Cluster {
start: usize,
end: usize,
class: Class,
}
const RIGHT_TO_LEFT: &[(u32, u32)] = &[
(0x0590, 0x05FF), (0x0600, 0x06FF), (0x0700, 0x074F), (0x0750, 0x077F), (0x0780, 0x07BF), (0x07C0, 0x07FF), (0x0800, 0x083F), (0x0840, 0x085F), (0x0860, 0x08FF), (0x200F, 0x200F), (0x061C, 0x061C), (0xFB1D, 0xFB4F), (0xFB50, 0xFDFF), (0xFE70, 0xFEFF), (0x10800, 0x10FFF),
(0x1E800, 0x1EFFF),
];
const ARABIC_NUMBER: &[(u32, u32)] = &[
(0x0600, 0x0605), (0x0660, 0x0669), (0x066B, 0x066C), (0x06DD, 0x06DD), (0x08E2, 0x08E2), (0x10D30, 0x10D39),
];
const EUROPEAN_NUMBER: &[(u32, u32)] = &[(0x0030, 0x0039), (0x06F0, 0x06F9)];
const MARK: &[(u32, u32)] = &[
(0x0300, 0x036F), (0x0483, 0x0489),
(0x0591, 0x05BD), (0x05BF, 0x05BF),
(0x05C1, 0x05C2),
(0x05C4, 0x05C5),
(0x05C7, 0x05C7),
(0x0610, 0x061A), (0x064B, 0x065F), (0x0670, 0x0670),
(0x06D6, 0x06DC),
(0x06DF, 0x06E4),
(0x06E7, 0x06E8),
(0x06EA, 0x06ED),
(0x0711, 0x0711),
(0x0730, 0x074A), (0x07A6, 0x07B0), (0x07EB, 0x07F3), (0x07FD, 0x07FD),
(0x0816, 0x0819), (0x081B, 0x0823),
(0x0825, 0x0827),
(0x0829, 0x082D),
(0x0859, 0x085B), (0x08D3, 0x08E1), (0x08E3, 0x08FF),
(0x20D0, 0x20F0), (0xFB1E, 0xFB1E), (0xFE00, 0xFE0F), (0xFE20, 0xFE2F), ];
fn in_ranges(code: u32, ranges: &[(u32, u32)]) -> bool {
ranges.iter().any(|&(lo, hi)| code >= lo && code <= hi)
}
fn class_of(ch: char) -> Class {
let code = ch as u32;
if in_ranges(code, MARK) {
return Class::Mark;
}
if in_ranges(code, ARABIC_NUMBER) {
return Class::ArabicNumber;
}
if in_ranges(code, EUROPEAN_NUMBER) {
return Class::EuropeanNumber;
}
if in_ranges(code, RIGHT_TO_LEFT) {
return Class::Right;
}
if ch.is_alphabetic() {
Class::Left
} else {
Class::Neutral
}
}
fn into_clusters(chars: &[char]) -> Vec<Cluster> {
let mut out: Vec<Cluster> = Vec::with_capacity(chars.len());
for (index, &ch) in chars.iter().enumerate() {
let class = class_of(ch);
if class == Class::Mark {
if let Some(last) = out.last_mut() {
last.end = index + 1;
continue;
}
out.push(Cluster {
start: index,
end: index + 1,
class: Class::Neutral,
});
continue;
}
out.push(Cluster {
start: index,
end: index + 1,
class,
});
}
out
}
fn base_is_right_to_left(clusters: &[Cluster]) -> bool {
let mut right = 0usize;
let mut left = 0usize;
for cluster in clusters {
match cluster.class {
Class::Right => right += 1,
Class::Left => left += 1,
_ => {}
}
}
right > left
}
fn resolve_levels(clusters: &[Cluster], base_is_rtl: bool) -> Vec<u8> {
let mut last_strong = if base_is_rtl {
Direction::Right
} else {
Direction::Left
};
let mut resolved: Vec<Option<Direction>> = Vec::with_capacity(clusters.len());
let mut is_number: Vec<bool> = Vec::with_capacity(clusters.len());
for cluster in clusters {
match cluster.class {
Class::Left => {
last_strong = Direction::Left;
resolved.push(Some(Direction::Left));
is_number.push(false);
}
Class::Right => {
last_strong = Direction::Right;
resolved.push(Some(Direction::Right));
is_number.push(false);
}
Class::EuropeanNumber => {
let left_context = last_strong == Direction::Left;
resolved.push(Some(Direction::Right));
is_number.push(!left_context);
}
Class::ArabicNumber => {
resolved.push(Some(Direction::Right));
is_number.push(true);
}
Class::Mark | Class::Neutral => {
resolved.push(None);
is_number.push(false);
}
}
}
for (index, cluster) in clusters.iter().enumerate() {
if cluster.class == Class::EuropeanNumber && !is_number[index] {
resolved[index] = Some(Direction::Left);
}
}
let boundary = if base_is_rtl {
Direction::Right
} else {
Direction::Left
};
let mut index = 0usize;
while index < resolved.len() {
if resolved[index].is_some() {
index += 1;
continue;
}
let start = index;
while index < resolved.len() && resolved[index].is_none() {
index += 1;
}
let before = resolved[..start]
.iter()
.rev()
.find_map(|d| *d)
.unwrap_or(boundary);
let after = resolved[index..]
.iter()
.find_map(|d| *d)
.unwrap_or(boundary);
let side = if before == after { before } else { boundary };
for slot in &mut resolved[start..index] {
*slot = Some(side);
}
}
let base: u8 = u8::from(base_is_rtl);
resolved
.iter()
.enumerate()
.map(|(slot, direction)| {
if is_number[slot] {
return 2;
}
match direction.unwrap_or(boundary) {
Direction::Right => 1,
Direction::Left => {
if base == 0 {
0
} else {
2
}
}
}
})
.collect()
}
fn reorder(chars: &[char], clusters: &[Cluster], levels: &[u8]) -> String {
let mut order: Vec<usize> = (0..clusters.len()).collect();
let highest = levels.iter().copied().max().unwrap_or(0);
let lowest_odd = levels.iter().copied().filter(|l| l % 2 == 1).min();
if let Some(lowest_odd) = lowest_odd {
for level in (lowest_odd..=highest).rev() {
let mut index = 0usize;
while index < order.len() {
if levels[order[index]] < level {
index += 1;
continue;
}
let start = index;
while index < order.len() && levels[order[index]] >= level {
index += 1;
}
order[start..index].reverse();
}
}
}
let mut out = String::with_capacity(chars.len());
for &slot in &order {
let cluster = clusters[slot];
let base = chars[cluster.start];
if cluster.class == Class::Neutral && levels[slot] % 2 == 1 {
out.push(mirrored(base).unwrap_or(base));
} else {
out.push(base);
}
out.extend(&chars[cluster.start + 1..cluster.end]);
}
out
}
const MIRRORED: &[(char, char)] = &[
('(', ')'),
('[', ']'),
('{', '}'),
('<', '>'),
('\u{ab}', '\u{bb}'), ('\u{2039}', '\u{203a}'), ('\u{2045}', '\u{2046}'), ('\u{207d}', '\u{207e}'), ('\u{208d}', '\u{208e}'), ('\u{2308}', '\u{2309}'), ('\u{230a}', '\u{230b}'), ('\u{2329}', '\u{232a}'), ('\u{27e6}', '\u{27e7}'), ('\u{27e8}', '\u{27e9}'), ('\u{3008}', '\u{3009}'),
('\u{300a}', '\u{300b}'),
('\u{300c}', '\u{300d}'),
('\u{300e}', '\u{300f}'),
('\u{3010}', '\u{3011}'),
('\u{3014}', '\u{3015}'),
('\u{3016}', '\u{3017}'),
('\u{3018}', '\u{3019}'),
('\u{301a}', '\u{301b}'),
('\u{ff08}', '\u{ff09}'),
('\u{ff1c}', '\u{ff1e}'),
('\u{ff3b}', '\u{ff3d}'),
('\u{ff5b}', '\u{ff5d}'),
('\u{ff5f}', '\u{ff60}'),
('\u{ff62}', '\u{ff63}'),
];
fn mirrored(ch: char) -> Option<char> {
MIRRORED.iter().find_map(|&(open, close)| {
if ch == open {
Some(close)
} else if ch == close {
Some(open)
} else {
None
}
})
}
pub(crate) fn is_only_marks(text: &str) -> bool {
!text.is_empty() && text.chars().all(|ch| class_of(ch) == Class::Mark)
}
pub(crate) fn logical_order(visual: &str) -> Cow<'_, str> {
if !visual
.chars()
.any(|ch| matches!(class_of(ch), Class::Right | Class::ArabicNumber))
{
return Cow::Borrowed(visual);
}
let chars: Vec<char> = visual.chars().collect();
let clusters = into_clusters(&chars);
let base_is_rtl = base_is_right_to_left(&clusters);
let levels = resolve_levels(&clusters, base_is_rtl);
Cow::Owned(reorder(&chars, &clusters, &levels))
}
#[cfg(test)]
mod tests {
use super::*;
const ARABIC_LOGICAL: &str = "\u{627}\u{644}\u{639}\u{631}\u{628}\u{64a}\u{629}";
const ARABIC_VISUAL: &str = "\u{629}\u{64a}\u{628}\u{631}\u{639}\u{644}\u{627}";
#[test]
fn a_latin_line_is_returned_untouched_and_unallocated() {
let line = "The quick brown fox, 1970-01-01.";
assert!(matches!(logical_order(line), Cow::Borrowed(_)));
assert_eq!(logical_order(line), line);
}
#[test]
fn a_line_of_arabic_reads_right_to_left() {
assert_eq!(logical_order(ARABIC_VISUAL), ARABIC_LOGICAL);
}
#[test]
fn an_arabic_run_inside_a_latin_line_reverses_alone() {
let visual = format!("Hello {ARABIC_VISUAL} World.");
let logical = format!("Hello {ARABIC_LOGICAL} World.");
assert_eq!(logical_order(&visual), logical);
}
#[test]
fn a_latin_run_inside_an_arabic_line_keeps_its_own_order() {
let visual = format!("{ARABIC_VISUAL} PDF {ARABIC_VISUAL}");
let logical = format!("{ARABIC_LOGICAL} PDF {ARABIC_LOGICAL}");
assert_eq!(logical_order(&visual), logical);
}
#[test]
fn arabic_indic_digits_stay_in_their_own_order() {
let month = "\u{625}\u{628}\u{631}\u{64a}\u{644}"; let month_visual: String = month.chars().rev().collect();
let visual = format!("\u{661}\u{664}\u{663}\u{664} {month_visual} \u{661}\u{664}");
let logical = format!("\u{661}\u{664} {month} \u{661}\u{664}\u{663}\u{664}");
assert_eq!(logical_order(&visual), logical);
}
#[test]
fn european_digits_inside_a_right_to_left_line_stay_in_their_own_order() {
let visual = format!("2026 {ARABIC_VISUAL}");
let logical = format!("{ARABIC_LOGICAL} 2026");
assert_eq!(logical_order(&visual), logical);
}
#[test]
fn european_digits_in_a_latin_line_are_untouched() {
let visual = format!("Chapter 12 {ARABIC_VISUAL} end");
let logical = format!("Chapter 12 {ARABIC_LOGICAL} end");
assert_eq!(logical_order(&visual), logical);
}
#[test]
fn hebrew_points_stay_behind_their_letter() {
let logical = "\u{5e9}\u{5b8}\u{5c1}\u{5dc}\u{5d5}\u{5b9}\u{5dd}";
let visual = "\u{5dd}\u{5d5}\u{5b9}\u{5dc}\u{5e9}\u{5b8}\u{5c1}";
assert_eq!(logical_order(visual), logical);
}
#[test]
fn arabic_diacritics_stay_behind_their_letter() {
let logical = "\u{645}\u{64f}\u{62d}\u{64e}\u{645}\u{64e}\u{651}\u{62f}";
let visual = "\u{62f}\u{645}\u{64e}\u{651}\u{62d}\u{64e}\u{645}\u{64f}";
assert_eq!(logical_order(visual), logical);
}
#[test]
fn a_leading_mark_has_no_base_and_is_left_where_it_is() {
let visual = format!("\u{5b8}{ARABIC_VISUAL}");
assert_eq!(
logical_order(&visual).chars().count(),
visual.chars().count()
);
}
#[test]
fn an_empty_line_is_empty() {
assert_eq!(logical_order(""), "");
}
#[test]
fn reordering_moves_characters_and_never_loses_them() {
for line in [
ARABIC_VISUAL,
"Hello \u{62f}\u{645}\u{62d}\u{645} World.",
"\u{661}\u{664}\u{663}\u{664}-\u{644}\u{64a}\u{631}\u{628}\u{625}-\u{661}\u{664}",
"\u{5dd}\u{5d5}\u{5b9}\u{5dc}\u{5e9}",
] {
let mut before: Vec<char> = line.chars().collect();
let mut after: Vec<char> = logical_order(line).chars().collect();
before.sort_unstable();
after.sort_unstable();
assert_eq!(
before, after,
"reordering changed the characters of {line:?}"
);
}
}
#[test]
fn a_bracket_the_writer_mirrored_is_mirrored_back() {
let visual = format!("\u{ab}{ARABIC_VISUAL}\u{bb}");
assert_eq!(
logical_order(&visual),
format!("\u{ab}{ARABIC_LOGICAL}\u{bb}")
);
let visual = format!("({ARABIC_VISUAL})");
assert_eq!(logical_order(&visual), format!("({ARABIC_LOGICAL})"));
}
#[test]
fn a_bracket_in_a_left_to_right_line_is_left_alone() {
let visual = format!("Note (see {ARABIC_VISUAL}) below");
let logical = format!("Note (see {ARABIC_LOGICAL}) below");
assert_eq!(logical_order(&visual), logical);
}
}