#![allow(clippy::module_name_repetitions)]
mod data;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BidiClass {
L,
R,
AL,
EN,
ES,
ET,
AN,
CS,
NSM,
BN,
B,
S,
WS,
ON,
LRE,
LRO,
RLE,
RLO,
PDF,
LRI,
RLI,
FSI,
PDI,
}
impl BidiClass {
#[must_use]
pub const fn is_strong(self) -> bool {
matches!(self, Self::L | Self::R | Self::AL)
}
#[must_use]
pub const fn is_isolate_initiator(self) -> bool {
matches!(self, Self::LRI | Self::RLI | Self::FSI)
}
#[must_use]
pub const fn is_neutral_or_isolate(self) -> bool {
matches!(
self,
Self::B | Self::S | Self::WS | Self::ON | Self::FSI | Self::LRI | Self::RLI | Self::PDI
)
}
}
#[must_use]
pub fn bidi_class(c: char) -> BidiClass {
data::class_lookup(c as u32)
}
#[must_use]
pub fn paragraph_level(text: &str) -> u8 {
let mut isolate_depth: u32 = 0;
for c in text.chars() {
let class = bidi_class(c);
if isolate_depth > 0 {
match class {
BidiClass::LRI | BidiClass::RLI | BidiClass::FSI => {
isolate_depth = isolate_depth.saturating_add(1);
}
BidiClass::PDI => {
isolate_depth -= 1;
}
_ => {}
}
continue;
}
match class {
BidiClass::L => return 0,
BidiClass::R | BidiClass::AL => return 1,
BidiClass::LRI | BidiClass::RLI | BidiClass::FSI => {
isolate_depth = 1;
}
_ => {}
}
}
0
}
pub const MAX_DEPTH: u8 = 125;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExplicitLevels {
pub levels: Vec<u8>,
pub effective_classes: Vec<BidiClass>,
pub removed: Vec<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OverrideStatus {
Neutral,
Ltr,
Rtl,
}
#[derive(Debug, Clone, Copy)]
struct StackFrame {
level: u8,
overrride: OverrideStatus,
isolate: bool,
}
#[must_use]
pub fn resolve_explicit_levels(classes: &[BidiClass], paragraph_level: u8) -> ExplicitLevels {
let n = classes.len();
let mut levels = vec![paragraph_level; n];
let mut effective: Vec<BidiClass> = classes.to_vec();
let mut removed = vec![false; n];
let mut stack: Vec<StackFrame> = Vec::with_capacity(8);
stack.push(StackFrame {
level: paragraph_level,
overrride: OverrideStatus::Neutral,
isolate: false,
});
let mut overflow_isolate: u32 = 0;
let mut overflow_embedding: u32 = 0;
let mut valid_isolate: u32 = 0;
for i in 0..n {
let cls = classes[i];
match cls {
BidiClass::RLE => {
apply_embedding(
&mut stack,
&mut overflow_isolate,
&mut overflow_embedding,
&mut levels,
i,
true,
OverrideStatus::Neutral,
);
}
BidiClass::LRE => {
apply_embedding(
&mut stack,
&mut overflow_isolate,
&mut overflow_embedding,
&mut levels,
i,
false,
OverrideStatus::Neutral,
);
}
BidiClass::RLO => {
apply_embedding(
&mut stack,
&mut overflow_isolate,
&mut overflow_embedding,
&mut levels,
i,
true,
OverrideStatus::Rtl,
);
}
BidiClass::LRO => {
apply_embedding(
&mut stack,
&mut overflow_isolate,
&mut overflow_embedding,
&mut levels,
i,
false,
OverrideStatus::Ltr,
);
}
BidiClass::RLI => {
apply_isolate(
&mut stack,
&mut overflow_isolate,
&mut overflow_embedding,
&mut valid_isolate,
&mut levels,
&mut effective,
i,
true,
);
}
BidiClass::LRI => {
apply_isolate(
&mut stack,
&mut overflow_isolate,
&mut overflow_embedding,
&mut valid_isolate,
&mut levels,
&mut effective,
i,
false,
);
}
BidiClass::FSI => {
let span_level = fsi_inner_level(classes, i + 1);
apply_isolate(
&mut stack,
&mut overflow_isolate,
&mut overflow_embedding,
&mut valid_isolate,
&mut levels,
&mut effective,
i,
span_level == 1,
);
}
BidiClass::PDI => {
if overflow_isolate > 0 {
overflow_isolate -= 1;
} else if valid_isolate > 0 {
overflow_embedding = 0;
while let Some(top) = stack.last() {
if top.isolate {
break;
}
stack.pop();
}
stack.pop();
valid_isolate -= 1;
}
let top = stack.last().expect("stack invariant: paragraph frame");
levels[i] = top.level;
effective[i] = match top.overrride {
OverrideStatus::Ltr => BidiClass::L,
OverrideStatus::Rtl => BidiClass::R,
OverrideStatus::Neutral => BidiClass::PDI,
};
}
BidiClass::PDF => {
if overflow_isolate > 0 {
} else if overflow_embedding > 0 {
overflow_embedding -= 1;
} else if stack.len() >= 2 && !stack.last().unwrap().isolate {
stack.pop();
}
levels[i] = stack.last().unwrap().level;
}
BidiClass::B => {
levels[i] = paragraph_level;
}
BidiClass::BN => {
let top = stack.last().expect("stack invariant: paragraph frame");
levels[i] = top.level;
}
_ => {
let top = stack.last().expect("stack invariant: paragraph frame");
levels[i] = top.level;
effective[i] = match top.overrride {
OverrideStatus::Ltr => BidiClass::L,
OverrideStatus::Rtl => BidiClass::R,
OverrideStatus::Neutral => effective[i],
};
}
}
if matches!(
cls,
BidiClass::RLE
| BidiClass::LRE
| BidiClass::RLO
| BidiClass::LRO
| BidiClass::PDF
| BidiClass::BN
) {
removed[i] = true;
}
}
ExplicitLevels {
levels,
effective_classes: effective,
removed,
}
}
fn apply_embedding(
stack: &mut Vec<StackFrame>,
overflow_isolate: &mut u32,
overflow_embedding: &mut u32,
levels: &mut [u8],
i: usize,
target_odd: bool,
override_status: OverrideStatus,
) {
let enclosing_level = stack
.last()
.expect("stack invariant: paragraph frame")
.level;
let new_level = if target_odd {
least_greater_odd(enclosing_level)
} else {
least_greater_even(enclosing_level)
};
if new_level <= MAX_DEPTH && *overflow_isolate == 0 && *overflow_embedding == 0 {
stack.push(StackFrame {
level: new_level,
overrride: override_status,
isolate: false,
});
} else if *overflow_isolate == 0 {
*overflow_embedding = overflow_embedding.saturating_add(1);
}
levels[i] = stack
.last()
.expect("stack invariant: paragraph frame")
.level;
}
#[allow(clippy::too_many_arguments)]
fn apply_isolate(
stack: &mut Vec<StackFrame>,
overflow_isolate: &mut u32,
overflow_embedding: &mut u32,
valid_isolate: &mut u32,
levels: &mut [u8],
effective: &mut [BidiClass],
i: usize,
target_odd: bool,
) {
let enclosing = *stack.last().expect("stack invariant: paragraph frame");
levels[i] = enclosing.level;
effective[i] = match enclosing.overrride {
OverrideStatus::Ltr => BidiClass::L,
OverrideStatus::Rtl => BidiClass::R,
OverrideStatus::Neutral => effective[i],
};
let new_level = if target_odd {
least_greater_odd(enclosing.level)
} else {
least_greater_even(enclosing.level)
};
if new_level <= MAX_DEPTH && *overflow_isolate == 0 && *overflow_embedding == 0 {
*valid_isolate = valid_isolate.saturating_add(1);
stack.push(StackFrame {
level: new_level,
overrride: OverrideStatus::Neutral,
isolate: true,
});
} else {
*overflow_isolate = overflow_isolate.saturating_add(1);
}
}
const fn least_greater_odd(level: u8) -> u8 {
if level & 1 == 0 {
level.saturating_add(1)
} else {
level.saturating_add(2)
}
}
const fn least_greater_even(level: u8) -> u8 {
if level & 1 == 0 {
level.saturating_add(2)
} else {
level.saturating_add(1)
}
}
fn fsi_inner_level(classes: &[BidiClass], start: usize) -> u8 {
let mut depth: u32 = 0;
for cls in classes.iter().skip(start) {
match *cls {
BidiClass::LRI | BidiClass::RLI | BidiClass::FSI => {
depth = depth.saturating_add(1);
}
BidiClass::PDI => {
if depth == 0 {
return 0;
}
depth -= 1;
}
_ if depth > 0 => {} BidiClass::L => return 0,
BidiClass::R | BidiClass::AL => return 1,
_ => {}
}
}
0
}
pub fn resolve_weak_types(classes: &mut [BidiClass], sos: BidiClass, eos: BidiClass) {
let _ = eos; if classes.is_empty() {
return;
}
for i in 0..classes.len() {
if classes[i] != BidiClass::NSM {
continue;
}
let prev = if i == 0 { sos } else { classes[i - 1] };
classes[i] = match prev {
BidiClass::LRI | BidiClass::RLI | BidiClass::FSI | BidiClass::PDI => BidiClass::ON,
other => other,
};
}
{
let mut last_strong = if sos.is_strong() { sos } else { BidiClass::L };
for cls in classes.iter_mut() {
match *cls {
BidiClass::L | BidiClass::R | BidiClass::AL => last_strong = *cls,
BidiClass::EN if last_strong == BidiClass::AL => {
*cls = BidiClass::AN;
}
_ => {}
}
}
}
for cls in classes.iter_mut() {
if *cls == BidiClass::AL {
*cls = BidiClass::R;
}
}
if classes.len() >= 3 {
for i in 1..classes.len() - 1 {
let cur = classes[i];
let prev = classes[i - 1];
let next = classes[i + 1];
match cur {
BidiClass::ES if prev == BidiClass::EN && next == BidiClass::EN => {
classes[i] = BidiClass::EN;
}
BidiClass::CS if prev == BidiClass::EN && next == BidiClass::EN => {
classes[i] = BidiClass::EN;
}
BidiClass::CS if prev == BidiClass::AN && next == BidiClass::AN => {
classes[i] = BidiClass::AN;
}
_ => {}
}
}
}
{
let n = classes.len();
let mut i = 0;
while i < n {
if classes[i] != BidiClass::ET {
i += 1;
continue;
}
let start = i;
while i < n && classes[i] == BidiClass::ET {
i += 1;
}
let end = i; let left_en = start > 0 && classes[start - 1] == BidiClass::EN;
let right_en = end < n && classes[end] == BidiClass::EN;
if left_en || right_en {
for cls in &mut classes[start..end] {
*cls = BidiClass::EN;
}
}
}
}
for cls in classes.iter_mut() {
if matches!(*cls, BidiClass::ES | BidiClass::ET | BidiClass::CS) {
*cls = BidiClass::ON;
}
}
{
let mut last_strong = if matches!(sos, BidiClass::L | BidiClass::R) {
sos
} else {
BidiClass::L
};
for cls in classes.iter_mut() {
match *cls {
BidiClass::L | BidiClass::R => last_strong = *cls,
BidiClass::EN if last_strong == BidiClass::L => {
*cls = BidiClass::L;
}
_ => {}
}
}
}
}
pub fn resolve_neutral_types(
classes: &mut [BidiClass],
embedding_level: u8,
sos: BidiClass,
eos: BidiClass,
) {
if classes.is_empty() {
return;
}
fn strong_dir(c: BidiClass) -> Option<BidiClass> {
match c {
BidiClass::L => Some(BidiClass::L),
BidiClass::R | BidiClass::EN | BidiClass::AN => Some(BidiClass::R),
_ => None,
}
}
let sos_dir = strong_dir(sos).unwrap_or(BidiClass::L);
let eos_dir = strong_dir(eos).unwrap_or(BidiClass::L);
let embedding_dir = if embedding_level % 2 == 0 {
BidiClass::L
} else {
BidiClass::R
};
let n = classes.len();
let mut i = 0;
while i < n {
if !classes[i].is_neutral_or_isolate() {
i += 1;
continue;
}
let start = i;
while i < n && classes[i].is_neutral_or_isolate() {
i += 1;
}
let end = i;
let mut left = sos_dir;
if start > 0 {
let mut k = start;
while k > 0 {
k -= 1;
if let Some(d) = strong_dir(classes[k]) {
left = d;
break;
}
if k == 0 {
break;
}
}
}
let mut right = eos_dir;
{
let mut k = end;
while k < n {
if let Some(d) = strong_dir(classes[k]) {
right = d;
break;
}
k += 1;
}
}
let target = if left == right { left } else { embedding_dir };
for cls in &mut classes[start..end] {
*cls = target;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BracketKind {
Open,
Close,
}
#[must_use]
pub fn paired_bracket(c: char) -> Option<(char, BracketKind)> {
data::bracket_lookup(c)
}
#[must_use]
pub fn bracket_pairs(chars: &[char], classes: &[BidiClass]) -> Vec<(usize, usize)> {
assert_eq!(
chars.len(),
classes.len(),
"bracket_pairs: chars / classes length mismatch ({} != {})",
chars.len(),
classes.len(),
);
const BRACKET_STACK_CAP: usize = 63;
let mut stack: Vec<(char, usize)> = Vec::with_capacity(BRACKET_STACK_CAP);
let mut pairs: Vec<(usize, usize)> = Vec::new();
const fn canon(c: char) -> char {
if c as u32 == 0x232A {
'\u{3009}'
} else {
c
}
}
for (i, &c) in chars.iter().enumerate() {
if classes[i] != BidiClass::ON {
continue;
}
let Some((other, kind)) = paired_bracket(c) else {
continue;
};
match kind {
BracketKind::Open => {
if stack.len() == BRACKET_STACK_CAP {
return Vec::new();
}
stack.push((other, i));
}
BracketKind::Close => {
let mut hit: Option<usize> = None;
for depth in (0..stack.len()).rev() {
if canon(stack[depth].0) == canon(c) {
hit = Some(depth);
break;
}
}
if let Some(depth) = hit {
let (_closer, open_pos) = stack[depth];
pairs.push((open_pos, i));
stack.truncate(depth);
}
}
}
}
pairs.sort_by_key(|&(open, _close)| open);
pairs
}
pub fn resolve_bracket_pairs(
classes: &mut [BidiClass],
pairs: &[(usize, usize)],
embedding_level: u8,
sos: BidiClass,
) {
if pairs.is_empty() {
return;
}
debug_assert!(
pairs.windows(2).all(|w| w[0].0 < w[1].0),
"resolve_bracket_pairs: pairs must be sorted by opener and non-overlapping at the opener"
);
fn strong_dir_n0(c: BidiClass) -> Option<BidiClass> {
match c {
BidiClass::L => Some(BidiClass::L),
BidiClass::R | BidiClass::EN | BidiClass::AN => Some(BidiClass::R),
_ => None,
}
}
let sos_dir = strong_dir_n0(sos).unwrap_or(BidiClass::L);
let embedding_dir = if embedding_level % 2 == 0 {
BidiClass::L
} else {
BidiClass::R
};
let n = classes.len();
for &(open, close) in pairs {
debug_assert!(
open < close && close < n,
"resolve_bracket_pairs: pair ({open}, {close}) out of bounds for {n} classes",
);
let mut saw_matching = false;
let mut saw_opposite = false;
for slot in &classes[open + 1..close] {
if let Some(dir) = strong_dir_n0(*slot) {
if dir == embedding_dir {
saw_matching = true;
break;
}
saw_opposite = true;
}
}
let target = if saw_matching {
embedding_dir
} else if saw_opposite {
let mut preceding = sos_dir;
for i in (0..open).rev() {
if let Some(dir) = strong_dir_n0(classes[i]) {
preceding = dir;
break;
}
}
if preceding != embedding_dir {
preceding
} else {
embedding_dir
}
} else {
continue;
};
classes[open] = target;
classes[close] = target;
let mut k = open + 1;
while k < classes.len() && classes[k] == BidiClass::NSM {
classes[k] = target;
k += 1;
}
let mut k = close + 1;
while k < classes.len() && classes[k] == BidiClass::NSM {
classes[k] = target;
k += 1;
}
}
}
#[must_use]
pub fn resolve_implicit_levels(classes: &[BidiClass], embedding_level: u8) -> Vec<u8> {
let even = embedding_level % 2 == 0;
classes
.iter()
.map(|c| match c {
BidiClass::L => {
if even {
embedding_level
} else {
embedding_level + 1
}
}
BidiClass::R => {
if even {
embedding_level + 1
} else {
embedding_level
}
}
BidiClass::EN | BidiClass::AN => {
if even {
embedding_level + 2
} else {
embedding_level + 1
}
}
_ => embedding_level,
})
.collect()
}
pub fn reset_trailing_levels(orig_classes: &[BidiClass], levels: &mut [u8], paragraph_level: u8) {
assert_eq!(
orig_classes.len(),
levels.len(),
"reset_trailing_levels: class slice and level slice must be the same length",
);
let n = orig_classes.len();
if n == 0 {
return;
}
for (i, &cls) in orig_classes.iter().enumerate() {
if matches!(cls, BidiClass::S | BidiClass::B) {
levels[i] = paragraph_level;
let mut j = i;
while j > 0 && is_l1_trailing_filler(orig_classes[j - 1]) {
j -= 1;
levels[j] = paragraph_level;
}
}
}
let mut k = n;
while k > 0 && is_l1_trailing_filler(orig_classes[k - 1]) {
k -= 1;
levels[k] = paragraph_level;
}
}
fn is_l1_trailing_filler(c: BidiClass) -> bool {
matches!(
c,
BidiClass::WS | BidiClass::LRI | BidiClass::RLI | BidiClass::FSI | BidiClass::PDI
)
}
#[must_use]
pub fn reorder_line(levels: &[u8]) -> Vec<usize> {
let n = levels.len();
let mut visual: Vec<usize> = (0..n).collect();
if n == 0 {
return visual;
}
let max_level = *levels.iter().max().unwrap_or(&0);
let lowest_odd_level = levels
.iter()
.copied()
.filter(|l| l % 2 == 1)
.min()
.unwrap_or(u8::MAX);
if lowest_odd_level == u8::MAX {
return visual;
}
let mut level = max_level;
loop {
let mut i = 0;
while i < n {
if levels[i] >= level {
let mut j = i + 1;
while j < n && levels[j] >= level {
j += 1;
}
visual[i..j].reverse();
i = j;
} else {
i += 1;
}
}
if level == lowest_odd_level {
break;
}
level -= 1;
}
visual
}
pub fn reorder_combining_marks(orig_classes: &[BidiClass], levels: &[u8], visual: &mut [usize]) {
assert_eq!(
orig_classes.len(),
levels.len(),
"reorder_combining_marks: class slice and level slice must be the same length",
);
assert_eq!(
orig_classes.len(),
visual.len(),
"reorder_combining_marks: class slice and visual slice must be the same length",
);
let n = visual.len();
if n == 0 {
return;
}
let mut vi = 0;
while vi < n {
let logical = visual[vi];
if orig_classes[logical] == BidiClass::NSM && levels[logical] % 2 == 1 {
let lvl = levels[logical];
let mut vj = vi;
let mut expected = logical;
while vj < n {
let lj = visual[vj];
if orig_classes[lj] == BidiClass::NSM && levels[lj] == lvl && lj == expected {
vj += 1;
expected = expected.saturating_sub(1);
} else {
break;
}
}
if vj < n {
let lb = visual[vj];
if orig_classes[lb] != BidiClass::NSM && levels[lb] == lvl && lb == expected {
visual[vi..=vj].reverse();
vi = vj + 1;
continue;
}
}
vi = vj.max(vi + 1);
} else {
vi += 1;
}
}
}
#[must_use]
pub fn mirrored_glyph(c: char) -> Option<char> {
data::mirror_lookup(c)
}
pub fn apply_mirroring(chars: &mut [char], levels: &[u8]) {
assert_eq!(
chars.len(),
levels.len(),
"apply_mirroring: chars / levels length mismatch ({} != {})",
chars.len(),
levels.len(),
);
for (c, &level) in chars.iter_mut().zip(levels.iter()) {
if level % 2 == 1 {
if let Some(mirror) = mirrored_glyph(*c) {
*c = mirror;
}
}
}
}
#[must_use]
pub fn split_paragraphs(text: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut start = 0usize;
for (i, c) in text.char_indices() {
if bidi_class(c) == BidiClass::B {
let end = i + c.len_utf8();
out.push(&text[start..end]);
start = end;
}
}
if start < text.len() {
out.push(&text[start..]);
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LevelRun {
pub start: usize,
pub end: usize,
pub level: u8,
}
impl LevelRun {
#[must_use]
pub fn len(&self) -> usize {
self.end - self.start
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.end == self.start
}
}
#[must_use]
pub fn level_runs(levels: &[u8]) -> Vec<LevelRun> {
let mut out = Vec::new();
let n = levels.len();
if n == 0 {
return out;
}
let mut start = 0usize;
let mut cur = levels[0];
for (i, &lvl) in levels.iter().enumerate().skip(1) {
if lvl != cur {
out.push(LevelRun {
start,
end: i,
level: cur,
});
start = i;
cur = lvl;
}
}
out.push(LevelRun {
start,
end: n,
level: cur,
});
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IsolatingRunSequence {
pub runs: Vec<LevelRun>,
pub level: u8,
pub sos: BidiClass,
pub eos: BidiClass,
}
impl IsolatingRunSequence {
pub fn indices<'a>(&'a self, removed: &'a [bool]) -> impl Iterator<Item = usize> + 'a {
self.runs
.iter()
.flat_map(move |r| (r.start..r.end).filter(move |&i| !removed[i]))
}
}
fn level_to_sos_eos(level: u8) -> BidiClass {
if level % 2 == 0 {
BidiClass::L
} else {
BidiClass::R
}
}
fn matching_pdi(classes: &[BidiClass], start: usize) -> Option<usize> {
let mut counter: i32 = 1;
for (offset, cls) in classes.iter().enumerate().skip(start + 1) {
match cls {
BidiClass::LRI | BidiClass::RLI | BidiClass::FSI => counter += 1,
BidiClass::PDI => {
counter -= 1;
if counter == 0 {
return Some(offset);
}
}
_ => {}
}
}
None
}
#[must_use]
pub fn isolating_run_sequences(
classes: &[BidiClass],
explicit: &ExplicitLevels,
paragraph_level: u8,
) -> Vec<IsolatingRunSequence> {
let runs = level_runs(&explicit.levels);
if runs.is_empty() {
return Vec::new();
}
let mut run_of_index = vec![0usize; explicit.levels.len()];
for (ri, r) in runs.iter().enumerate() {
for slot in &mut run_of_index[r.start..r.end] {
*slot = ri;
}
}
let nruns = runs.len();
let mut next_run: Vec<Option<usize>> = vec![None; nruns];
let mut chained_from_prior: Vec<bool> = vec![false; nruns];
for (ri, r) in runs.iter().enumerate() {
let mut last_idx: Option<usize> = None;
for i in (r.start..r.end).rev() {
if !explicit.removed[i] {
last_idx = Some(i);
break;
}
}
let Some(last) = last_idx else {
continue;
};
if matches!(
classes[last],
BidiClass::LRI | BidiClass::RLI | BidiClass::FSI
) {
if let Some(pdi_idx) = matching_pdi(classes, last) {
let pdi_run = run_of_index[pdi_idx];
if runs[pdi_run].start == pdi_idx {
next_run[ri] = Some(pdi_run);
chained_from_prior[pdi_run] = true;
}
}
}
}
let mut sequences: Vec<IsolatingRunSequence> = Vec::with_capacity(nruns);
let mut visited: Vec<bool> = vec![false; nruns];
for seed in 0..nruns {
if chained_from_prior[seed] || visited[seed] {
continue;
}
let mut chain: Vec<LevelRun> = Vec::new();
let mut cur = Some(seed);
while let Some(idx) = cur {
if visited[idx] {
break;
}
visited[idx] = true;
chain.push(runs[idx]);
cur = next_run[idx];
}
debug_assert!(!chain.is_empty(), "BD13 seed produces non-empty chain");
let level = chain[0].level;
let first_idx = chain[0].start;
let last_idx = chain.last().unwrap().end - 1;
let mut sos_other = paragraph_level;
for i in (0..first_idx).rev() {
if !explicit.removed[i] {
sos_other = explicit.levels[i];
break;
}
}
let sos_level = sos_other.max(level);
let mut eos_fallback = false;
let mut last_run_last_nonremoved: Option<usize> = None;
for i in (chain.last().unwrap().start..chain.last().unwrap().end).rev() {
if !explicit.removed[i] {
last_run_last_nonremoved = Some(i);
break;
}
}
if let Some(j) = last_run_last_nonremoved {
if matches!(classes[j], BidiClass::LRI | BidiClass::RLI | BidiClass::FSI)
&& matching_pdi(classes, j).is_none()
{
eos_fallback = true;
}
}
let mut eos_other = paragraph_level;
if !eos_fallback {
let mut found = false;
for i in (last_idx + 1)..explicit.levels.len() {
if !explicit.removed[i] {
eos_other = explicit.levels[i];
found = true;
break;
}
}
if !found {
eos_other = paragraph_level;
}
}
let eos_level = eos_other.max(level);
sequences.push(IsolatingRunSequence {
runs: chain,
level,
sos: level_to_sos_eos(sos_level),
eos: level_to_sos_eos(eos_level),
});
}
sequences
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParagraphBidi {
pub paragraph_level: u8,
pub classes: Vec<BidiClass>,
pub effective_classes: Vec<BidiClass>,
pub removed: Vec<bool>,
pub levels: Vec<u8>,
}
impl ParagraphBidi {
#[must_use]
pub fn reorder_paragraph(&self) -> Vec<usize> {
let mut levels = self.levels.clone();
reset_trailing_levels(&self.classes, &mut levels, self.paragraph_level);
reorder_line(&levels)
}
#[must_use]
pub fn reorder_line_range(&self, line: core::ops::Range<usize>) -> Vec<usize> {
assert!(
line.start <= line.end && line.end <= self.levels.len(),
"reorder_line_range: line range {line:?} out of bounds for {} chars",
self.levels.len(),
);
let cls = &self.classes[line.clone()];
let mut lvl = self.levels[line].to_vec();
reset_trailing_levels(cls, &mut lvl, self.paragraph_level);
reorder_line(&lvl)
}
}
#[must_use]
pub fn process_paragraph_classes(classes: &[BidiClass], base_level: Option<u8>) -> ParagraphBidi {
let pl = base_level
.map(|l| l & 1)
.unwrap_or_else(|| paragraph_level_from_classes(classes));
let explicit = resolve_explicit_levels(classes, pl);
let mut levels = explicit.levels.clone();
let sequences = isolating_run_sequences(classes, &explicit, pl);
for seq in &sequences {
let seq_indices: Vec<usize> = seq.indices(&explicit.removed).collect();
if seq_indices.is_empty() {
continue;
}
let mut seq_classes: Vec<BidiClass> = seq_indices
.iter()
.map(|&i| explicit.effective_classes[i])
.collect();
resolve_weak_types(&mut seq_classes, seq.sos, seq.eos);
resolve_neutral_types(&mut seq_classes, seq.level, seq.sos, seq.eos);
let seq_levels = resolve_implicit_levels(&seq_classes, seq.level);
for (offset, ¶graph_idx) in seq_indices.iter().enumerate() {
levels[paragraph_idx] = seq_levels[offset];
}
}
ParagraphBidi {
paragraph_level: pl,
classes: classes.to_vec(),
effective_classes: explicit.effective_classes,
removed: explicit.removed,
levels,
}
}
fn paragraph_level_from_classes(classes: &[BidiClass]) -> u8 {
let n = classes.len();
let mut i = 0;
while i < n {
match classes[i] {
BidiClass::L => return 0,
BidiClass::R | BidiClass::AL => return 1,
BidiClass::LRI | BidiClass::RLI | BidiClass::FSI => {
if let Some(pdi) = matching_pdi(classes, i) {
i = pdi + 1;
} else {
return 0;
}
}
_ => i += 1,
}
}
0
}
#[must_use]
pub fn process_paragraph(text: &str, base_level: Option<u8>) -> (ParagraphBidi, Vec<usize>) {
let mut char_byte_offsets = Vec::new();
let mut classes = Vec::new();
for (i, c) in text.char_indices() {
char_byte_offsets.push(i);
classes.push(bidi_class(c));
}
let carrier = process_paragraph_classes(&classes, base_level);
(carrier, char_byte_offsets)
}
#[must_use]
pub fn process_paragraph_classes_with_brackets(
classes: &[BidiClass],
chars: &[char],
base_level: Option<u8>,
) -> ParagraphBidi {
assert_eq!(
classes.len(),
chars.len(),
"process_paragraph_classes_with_brackets: classes / chars length mismatch ({} != {})",
classes.len(),
chars.len(),
);
let pl = base_level
.map(|l| l & 1)
.unwrap_or_else(|| paragraph_level_from_classes(classes));
let explicit = resolve_explicit_levels(classes, pl);
let mut levels = explicit.levels.clone();
let sequences = isolating_run_sequences(classes, &explicit, pl);
for seq in &sequences {
let seq_indices: Vec<usize> = seq.indices(&explicit.removed).collect();
if seq_indices.is_empty() {
continue;
}
let mut seq_classes: Vec<BidiClass> = seq_indices
.iter()
.map(|&i| explicit.effective_classes[i])
.collect();
let seq_chars: Vec<char> = seq_indices.iter().map(|&i| chars[i]).collect();
resolve_weak_types(&mut seq_classes, seq.sos, seq.eos);
let pairs = bracket_pairs(&seq_chars, &seq_classes);
resolve_bracket_pairs(&mut seq_classes, &pairs, seq.level, seq.sos);
resolve_neutral_types(&mut seq_classes, seq.level, seq.sos, seq.eos);
let seq_levels = resolve_implicit_levels(&seq_classes, seq.level);
for (offset, ¶graph_idx) in seq_indices.iter().enumerate() {
levels[paragraph_idx] = seq_levels[offset];
}
}
ParagraphBidi {
paragraph_level: pl,
classes: classes.to_vec(),
effective_classes: explicit.effective_classes,
removed: explicit.removed,
levels,
}
}
#[must_use]
pub fn process_paragraph_with_brackets(
text: &str,
base_level: Option<u8>,
) -> (ParagraphBidi, Vec<usize>) {
let mut char_byte_offsets = Vec::new();
let mut chars = Vec::new();
let mut classes = Vec::new();
for (i, c) in text.char_indices() {
char_byte_offsets.push(i);
chars.push(c);
classes.push(bidi_class(c));
}
let carrier = process_paragraph_classes_with_brackets(&classes, &chars, base_level);
(carrier, char_byte_offsets)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParagraphSlice {
pub byte_range: core::ops::Range<usize>,
pub char_offset: usize,
pub bidi: ParagraphBidi,
pub char_byte_offsets: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextBidi {
pub paragraphs: Vec<ParagraphSlice>,
pub total_chars: usize,
}
impl TextBidi {
#[must_use]
pub fn len(&self) -> usize {
self.paragraphs.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.paragraphs.is_empty()
}
#[must_use]
pub fn locate_char(&self, k: usize) -> Option<(usize, usize)> {
for (pi, p) in self.paragraphs.iter().enumerate() {
let end = p.char_offset + p.bidi.levels.len();
if k < end {
return Some((pi, k - p.char_offset));
}
}
None
}
}
#[must_use]
pub fn process_text(text: &str, base_level: Option<u8>) -> TextBidi {
let slices = split_paragraphs(text);
let mut paragraphs = Vec::with_capacity(slices.len());
let mut total_chars = 0usize;
let mut byte_start = 0usize;
for slice in slices {
let len = slice.len();
let byte_range = byte_start..byte_start + len;
let mut classes = Vec::new();
let mut char_byte_offsets = Vec::new();
for (off, c) in slice.char_indices() {
char_byte_offsets.push(byte_start + off);
classes.push(bidi_class(c));
}
let bidi = process_paragraph_classes(&classes, base_level);
let char_offset = total_chars;
total_chars += bidi.levels.len();
paragraphs.push(ParagraphSlice {
byte_range,
char_offset,
bidi,
char_byte_offsets,
});
byte_start += len;
}
TextBidi {
paragraphs,
total_chars,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explicit_format_codepoints_have_canonical_classes() {
assert_eq!(bidi_class('\u{202A}'), BidiClass::LRE);
assert_eq!(bidi_class('\u{202B}'), BidiClass::RLE);
assert_eq!(bidi_class('\u{202C}'), BidiClass::PDF);
assert_eq!(bidi_class('\u{202D}'), BidiClass::LRO);
assert_eq!(bidi_class('\u{202E}'), BidiClass::RLO);
assert_eq!(bidi_class('\u{2066}'), BidiClass::LRI);
assert_eq!(bidi_class('\u{2067}'), BidiClass::RLI);
assert_eq!(bidi_class('\u{2068}'), BidiClass::FSI);
assert_eq!(bidi_class('\u{2069}'), BidiClass::PDI);
assert_eq!(bidi_class('\u{200E}'), BidiClass::L);
assert_eq!(bidi_class('\u{200F}'), BidiClass::R);
assert_eq!(bidi_class('\u{061C}'), BidiClass::AL);
}
#[test]
fn isolate_initiator_predicate_only_fires_for_three() {
assert!(BidiClass::LRI.is_isolate_initiator());
assert!(BidiClass::RLI.is_isolate_initiator());
assert!(BidiClass::FSI.is_isolate_initiator());
assert!(!BidiClass::PDI.is_isolate_initiator());
assert!(!BidiClass::LRE.is_isolate_initiator());
assert!(!BidiClass::RLE.is_isolate_initiator());
assert!(!BidiClass::LRO.is_isolate_initiator());
assert!(!BidiClass::RLO.is_isolate_initiator());
assert!(!BidiClass::L.is_isolate_initiator());
assert!(!BidiClass::R.is_isolate_initiator());
assert!(!BidiClass::AL.is_isolate_initiator());
assert!(!BidiClass::EN.is_isolate_initiator());
assert!(!BidiClass::ON.is_isolate_initiator());
}
#[test]
fn strong_predicate_fires_only_for_l_r_al() {
assert!(BidiClass::L.is_strong());
assert!(BidiClass::R.is_strong());
assert!(BidiClass::AL.is_strong());
for c in [
BidiClass::EN,
BidiClass::ES,
BidiClass::ET,
BidiClass::AN,
BidiClass::CS,
BidiClass::NSM,
BidiClass::BN,
] {
assert!(!c.is_strong(), "{c:?} should not be strong");
}
for c in [BidiClass::B, BidiClass::S, BidiClass::WS, BidiClass::ON] {
assert!(!c.is_strong());
}
for c in [
BidiClass::LRE,
BidiClass::LRO,
BidiClass::RLE,
BidiClass::RLO,
BidiClass::PDF,
BidiClass::LRI,
BidiClass::RLI,
BidiClass::FSI,
BidiClass::PDI,
] {
assert!(!c.is_strong());
}
}
#[test]
fn ascii_classes_match_uax9_table_4() {
assert_eq!(bidi_class('A'), BidiClass::L);
assert_eq!(bidi_class('a'), BidiClass::L);
assert_eq!(bidi_class('Z'), BidiClass::L);
assert_eq!(bidi_class('z'), BidiClass::L);
assert_eq!(bidi_class('0'), BidiClass::EN);
assert_eq!(bidi_class('5'), BidiClass::EN);
assert_eq!(bidi_class('9'), BidiClass::EN);
assert_eq!(bidi_class('+'), BidiClass::ES);
assert_eq!(bidi_class('-'), BidiClass::ES);
assert_eq!(bidi_class(','), BidiClass::CS);
assert_eq!(bidi_class('.'), BidiClass::CS);
assert_eq!(bidi_class('/'), BidiClass::CS);
assert_eq!(bidi_class(':'), BidiClass::CS);
assert_eq!(bidi_class('#'), BidiClass::ET);
assert_eq!(bidi_class('$'), BidiClass::ET);
assert_eq!(bidi_class('%'), BidiClass::ET);
assert_eq!(bidi_class(' '), BidiClass::WS);
assert_eq!(bidi_class('\t'), BidiClass::S);
assert_eq!(bidi_class('\n'), BidiClass::B);
assert_eq!(bidi_class('\r'), BidiClass::B);
assert_eq!(bidi_class('\0'), BidiClass::BN);
assert_eq!(bidi_class('\u{00A0}'), BidiClass::CS);
assert_eq!(bidi_class('\u{00A3}'), BidiClass::ET); assert_eq!(bidi_class('\u{00A5}'), BidiClass::ET); assert_eq!(bidi_class('\u{00B0}'), BidiClass::ET);
assert_eq!(bidi_class('\u{00AD}'), BidiClass::BN);
}
#[test]
fn hebrew_letters_are_r() {
assert_eq!(bidi_class('\u{05D0}'), BidiClass::R);
assert_eq!(bidi_class('\u{05E0}'), BidiClass::R);
assert_eq!(bidi_class('\u{05EA}'), BidiClass::R);
}
#[test]
fn arabic_letters_are_al_and_digits_split_en_an() {
assert_eq!(bidi_class('\u{0627}'), BidiClass::AL);
assert_eq!(bidi_class('\u{0628}'), BidiClass::AL);
assert_eq!(bidi_class('\u{064A}'), BidiClass::AL);
assert_eq!(bidi_class('\u{0660}'), BidiClass::AN);
assert_eq!(bidi_class('\u{0669}'), BidiClass::AN);
assert_eq!(bidi_class('\u{06F0}'), BidiClass::EN);
assert_eq!(bidi_class('\u{06F9}'), BidiClass::EN);
assert_eq!(bidi_class('\u{064B}'), BidiClass::NSM);
assert_eq!(bidi_class('\u{0651}'), BidiClass::NSM);
assert_eq!(bidi_class('\u{0640}'), BidiClass::AL);
assert_eq!(bidi_class('\u{FE8E}'), BidiClass::AL); assert_eq!(bidi_class('\u{FEFC}'), BidiClass::AL); }
#[test]
fn combining_diacriticals_are_nsm() {
assert_eq!(bidi_class('\u{0301}'), BidiClass::NSM);
assert_eq!(bidi_class('\u{0308}'), BidiClass::NSM);
assert_eq!(bidi_class('\u{036F}'), BidiClass::NSM);
}
#[test]
fn split_paragraphs_keeps_b_with_previous() {
let v = split_paragraphs("Hello\nWorld");
assert_eq!(v, vec!["Hello\n", "World"]);
let v = split_paragraphs("Hi\n");
assert_eq!(v, vec!["Hi\n"]);
let v = split_paragraphs("A\n\nB");
assert_eq!(v, vec!["A\n", "\n", "B"]);
let v = split_paragraphs("no separators here");
assert_eq!(v, vec!["no separators here"]);
let v = split_paragraphs("");
assert!(v.is_empty());
let v = split_paragraphs("a\u{2029}b");
assert_eq!(v, vec!["a\u{2029}", "b"]);
}
#[test]
fn paragraph_level_p3_pure_latin_is_zero() {
assert_eq!(paragraph_level("Hello, world!"), 0);
assert_eq!(paragraph_level(""), 0); assert_eq!(paragraph_level(" "), 0); assert_eq!(paragraph_level("123"), 0); }
#[test]
fn paragraph_level_p3_pure_hebrew_is_one() {
assert_eq!(paragraph_level("\u{05E9}\u{05DC}\u{05D5}\u{05DD}"), 1);
}
#[test]
fn paragraph_level_p3_pure_arabic_is_one() {
assert_eq!(
paragraph_level("\u{0645}\u{0631}\u{062D}\u{0628}\u{0627}"),
1
);
}
#[test]
fn paragraph_level_first_strong_after_neutrals_decides() {
assert_eq!(paragraph_level(" \"Hello\""), 0);
assert_eq!(paragraph_level(" \u{0627}"), 1);
}
#[test]
fn paragraph_level_p2_skips_isolate_regions() {
let s = "\u{2066}Hello\u{2069}\u{05D0}";
assert_eq!(paragraph_level(s), 1);
let s = "\u{2067}\u{05D0}\u{2069}Hello";
assert_eq!(paragraph_level(s), 0);
let s = "\u{2066}\u{2067}\u{0627}\u{2069}\u{2069}World";
assert_eq!(paragraph_level(s), 0);
let s = "\u{2066}\u{05D0}";
assert_eq!(paragraph_level(s), 0);
let s = "\u{2068}\u{05D0}\u{2069}World";
assert_eq!(paragraph_level(s), 0);
}
#[test]
fn paragraph_level_embedding_initiators_do_not_skip() {
let s = "\u{202B}\u{05D0}\u{202C}Hello";
assert_eq!(paragraph_level(s), 1); let s = "\u{202B}Hello\u{202C}\u{05D0}";
assert_eq!(paragraph_level(s), 0);
}
#[test]
fn paragraph_level_unmatched_pdi_is_ignored() {
let s = "\u{2069}Hello";
assert_eq!(paragraph_level(s), 0);
}
#[test]
fn neutral_or_isolate_predicate_covers_uax9_ni_alias() {
for c in [
BidiClass::B,
BidiClass::S,
BidiClass::WS,
BidiClass::ON,
BidiClass::FSI,
BidiClass::LRI,
BidiClass::RLI,
BidiClass::PDI,
] {
assert!(c.is_neutral_or_isolate(), "{c:?} should be NI");
}
for c in [
BidiClass::L,
BidiClass::R,
BidiClass::AL,
BidiClass::EN,
BidiClass::ES,
BidiClass::ET,
BidiClass::AN,
BidiClass::CS,
BidiClass::NSM,
BidiClass::BN,
BidiClass::LRE,
BidiClass::LRO,
BidiClass::RLE,
BidiClass::RLO,
BidiClass::PDF,
] {
assert!(!c.is_neutral_or_isolate(), "{c:?} should not be NI");
}
}
#[test]
fn w_rules_empty_input_is_noop() {
let mut cls: Vec<BidiClass> = vec![];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert!(cls.is_empty());
}
#[test]
fn w1_consecutive_nsm_inherit_first_strongs_type() {
let mut cls = vec![BidiClass::AL, BidiClass::NSM, BidiClass::NSM];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::R, BidiClass::R, BidiClass::R]);
}
#[test]
fn w1_nsm_at_sequence_start_takes_sos_type() {
let mut cls = vec![BidiClass::NSM, BidiClass::L];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::R, BidiClass::L]);
}
#[test]
fn w1_nsm_after_isolate_initiator_or_pdi_becomes_on() {
let mut cls = vec![BidiClass::LRI, BidiClass::NSM];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::LRI, BidiClass::ON]);
let mut cls = vec![BidiClass::PDI, BidiClass::NSM];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::PDI, BidiClass::ON]);
let mut cls = vec![BidiClass::RLI, BidiClass::NSM];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::RLI, BidiClass::ON]);
let mut cls = vec![BidiClass::FSI, BidiClass::NSM];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::FSI, BidiClass::ON]);
}
#[test]
fn w2_en_after_al_strong_becomes_an() {
let mut cls = vec![BidiClass::AL, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::R, BidiClass::AN]);
let mut cls = vec![BidiClass::AL, BidiClass::ON, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::R, BidiClass::ON, BidiClass::AN]);
}
#[test]
fn w2_en_with_no_al_predecessor_stays_en() {
let mut cls = vec![BidiClass::ON, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::ON, BidiClass::L]);
let mut cls = vec![BidiClass::L, BidiClass::ON, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::L, BidiClass::ON, BidiClass::L]);
let mut cls = vec![BidiClass::R, BidiClass::ON, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::R, BidiClass::ON, BidiClass::EN]);
}
#[test]
fn w2_sos_alone_does_not_flip_en() {
let mut cls = vec![BidiClass::ON, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::ON, BidiClass::L]);
let mut cls = vec![BidiClass::ON, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::ON, BidiClass::EN]);
}
#[test]
fn w3_all_remaining_al_become_r() {
let mut cls = vec![BidiClass::AL, BidiClass::AL, BidiClass::AL];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::R, BidiClass::R, BidiClass::R]);
}
#[test]
fn w4_single_es_or_cs_between_two_ens_collapses_to_en() {
let mut cls = vec![BidiClass::EN, BidiClass::ES, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::EN, BidiClass::EN, BidiClass::EN]);
let mut cls = vec![BidiClass::EN, BidiClass::CS, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::EN, BidiClass::EN, BidiClass::EN]);
let mut cls = vec![BidiClass::AN, BidiClass::CS, BidiClass::AN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::AN, BidiClass::AN, BidiClass::AN]);
}
#[test]
fn w4_does_not_collapse_mixed_or_multiple_separators() {
let mut cls = vec![BidiClass::EN, BidiClass::CS, BidiClass::AN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::EN, BidiClass::ON, BidiClass::AN]);
let mut cls = vec![BidiClass::EN, BidiClass::ES, BidiClass::ES, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(
cls,
vec![BidiClass::EN, BidiClass::ON, BidiClass::ON, BidiClass::EN]
);
let mut cls = vec![BidiClass::AN, BidiClass::ES, BidiClass::AN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::AN, BidiClass::ON, BidiClass::AN]);
}
#[test]
fn w5_ets_adjacent_to_en_collapse() {
let mut cls = vec![BidiClass::ET, BidiClass::ET, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::EN, BidiClass::EN, BidiClass::EN]);
let mut cls = vec![BidiClass::EN, BidiClass::ET, BidiClass::ET];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::EN, BidiClass::EN, BidiClass::EN]);
let mut cls = vec![BidiClass::AN, BidiClass::ET, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::AN, BidiClass::EN, BidiClass::EN]);
}
#[test]
fn w5_isolated_ets_do_not_collapse() {
let mut cls = vec![BidiClass::R, BidiClass::ET, BidiClass::R];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::R, BidiClass::ON, BidiClass::R]);
let mut cls = vec![BidiClass::ET, BidiClass::ET];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::ON, BidiClass::ON]);
}
#[test]
fn w6_remaining_separators_terminators_become_on() {
let mut cls = vec![BidiClass::AN, BidiClass::ET];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::AN, BidiClass::ON]);
let mut cls = vec![BidiClass::L, BidiClass::ES, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::L, BidiClass::ON, BidiClass::L]);
let mut cls = vec![BidiClass::EN, BidiClass::CS, BidiClass::AN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::EN, BidiClass::ON, BidiClass::AN]);
let mut cls = vec![BidiClass::ET, BidiClass::AN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::ON, BidiClass::AN]);
}
#[test]
fn w7_en_after_l_becomes_l() {
let mut cls = vec![BidiClass::L, BidiClass::ON, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::L, BidiClass::ON, BidiClass::L]);
let mut cls = vec![BidiClass::R, BidiClass::ON, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::R, BidiClass::ON, BidiClass::EN]);
}
#[test]
fn w7_with_sos_l_flips_lone_en() {
let mut cls = vec![BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::L]);
let mut cls = vec![BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::EN]);
}
#[test]
fn w_rules_compose_w2_before_w3_before_w7() {
let mut cls = vec![BidiClass::AL, BidiClass::EN, BidiClass::EN];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::R, BidiClass::AN, BidiClass::AN]);
}
#[test]
fn n_rules_empty_input_is_noop() {
let mut cls: Vec<BidiClass> = vec![];
resolve_neutral_types(&mut cls, 0, BidiClass::L, BidiClass::L);
assert!(cls.is_empty());
}
#[test]
fn n1_l_ni_l_collapses_to_l() {
let mut cls = vec![BidiClass::L, BidiClass::ON, BidiClass::L];
resolve_neutral_types(&mut cls, 0, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::L, BidiClass::L, BidiClass::L]);
}
#[test]
fn n1_r_ni_r_collapses_to_r() {
let mut cls = vec![BidiClass::R, BidiClass::ON, BidiClass::R];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::R, BidiClass::R, BidiClass::R]);
}
#[test]
fn n1_numbers_count_as_r_for_surrounding_check() {
let mut cls = vec![BidiClass::R, BidiClass::ON, BidiClass::AN];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::R, BidiClass::R, BidiClass::AN]);
let mut cls = vec![BidiClass::R, BidiClass::ON, BidiClass::EN];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::R, BidiClass::R, BidiClass::EN]);
let mut cls = vec![BidiClass::AN, BidiClass::ON, BidiClass::R];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::AN, BidiClass::R, BidiClass::R]);
let mut cls = vec![BidiClass::AN, BidiClass::ON, BidiClass::AN];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::AN, BidiClass::R, BidiClass::AN]);
let mut cls = vec![BidiClass::AN, BidiClass::ON, BidiClass::EN];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::AN, BidiClass::R, BidiClass::EN]);
let mut cls = vec![BidiClass::EN, BidiClass::ON, BidiClass::R];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::EN, BidiClass::R, BidiClass::R]);
let mut cls = vec![BidiClass::EN, BidiClass::ON, BidiClass::AN];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::EN, BidiClass::R, BidiClass::AN]);
let mut cls = vec![BidiClass::EN, BidiClass::ON, BidiClass::EN];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::EN, BidiClass::R, BidiClass::EN]);
}
#[test]
fn n2_differing_strong_context_takes_embedding_direction() {
let mut cls = vec![BidiClass::L, BidiClass::ON, BidiClass::R];
resolve_neutral_types(&mut cls, 0, BidiClass::L, BidiClass::R);
assert_eq!(cls, vec![BidiClass::L, BidiClass::L, BidiClass::R]);
let mut cls = vec![BidiClass::L, BidiClass::ON, BidiClass::R];
resolve_neutral_types(&mut cls, 1, BidiClass::L, BidiClass::R);
assert_eq!(cls, vec![BidiClass::L, BidiClass::R, BidiClass::R]);
let mut cls = vec![BidiClass::R, BidiClass::ON, BidiClass::L];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::L);
assert_eq!(cls, vec![BidiClass::R, BidiClass::R, BidiClass::L]);
}
#[test]
fn n_rules_sos_eos_drive_boundary_runs() {
let mut cls = vec![BidiClass::ON, BidiClass::L];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::L);
assert_eq!(cls, vec![BidiClass::R, BidiClass::L]);
let mut cls = vec![BidiClass::ON, BidiClass::L];
resolve_neutral_types(&mut cls, 0, BidiClass::R, BidiClass::L);
assert_eq!(cls, vec![BidiClass::L, BidiClass::L]);
let mut cls = vec![BidiClass::ON, BidiClass::WS];
resolve_neutral_types(&mut cls, 0, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::L, BidiClass::L]);
let mut cls = vec![BidiClass::ON, BidiClass::WS];
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(cls, vec![BidiClass::R, BidiClass::R]);
}
#[test]
fn n_rules_long_ni_run_collapses_uniformly() {
let mut cls = vec![
BidiClass::L,
BidiClass::WS,
BidiClass::ON,
BidiClass::LRI,
BidiClass::PDI,
BidiClass::S,
BidiClass::B,
BidiClass::L,
];
resolve_neutral_types(&mut cls, 0, BidiClass::L, BidiClass::L);
assert_eq!(
cls,
vec![
BidiClass::L,
BidiClass::L,
BidiClass::L,
BidiClass::L,
BidiClass::L,
BidiClass::L,
BidiClass::L,
BidiClass::L,
]
);
}
#[test]
fn n_rules_leave_nsm_and_bn_alone() {
let mut cls = vec![BidiClass::L, BidiClass::NSM, BidiClass::BN, BidiClass::L];
resolve_neutral_types(&mut cls, 0, BidiClass::L, BidiClass::L);
assert_eq!(
cls,
vec![BidiClass::L, BidiClass::NSM, BidiClass::BN, BidiClass::L,]
);
}
#[test]
fn n_rules_nsm_does_not_terminate_ni_run() {
let mut cls = vec![
BidiClass::L,
BidiClass::NSM,
BidiClass::ON,
BidiClass::ON,
BidiClass::L,
];
resolve_neutral_types(&mut cls, 0, BidiClass::L, BidiClass::L);
assert_eq!(
cls,
vec![
BidiClass::L,
BidiClass::NSM,
BidiClass::L,
BidiClass::L,
BidiClass::L,
]
);
}
#[test]
fn n_rules_multiple_independent_ni_runs() {
let mut cls = vec![
BidiClass::R,
BidiClass::ON,
BidiClass::R,
BidiClass::ON,
BidiClass::ON,
BidiClass::L,
];
resolve_neutral_types(&mut cls, 0, BidiClass::R, BidiClass::L);
assert_eq!(
cls,
vec![
BidiClass::R,
BidiClass::R,
BidiClass::R,
BidiClass::L,
BidiClass::L,
BidiClass::L,
]
);
}
#[test]
fn n_rules_ni_only_sequence_uses_sos_eos() {
let mut cls = vec![BidiClass::ON, BidiClass::WS, BidiClass::ON];
resolve_neutral_types(&mut cls, 0, BidiClass::L, BidiClass::L);
assert_eq!(cls, vec![BidiClass::L, BidiClass::L, BidiClass::L]);
let mut cls = vec![BidiClass::ON, BidiClass::WS, BidiClass::ON];
resolve_neutral_types(&mut cls, 1, BidiClass::L, BidiClass::R);
assert_eq!(cls, vec![BidiClass::R, BidiClass::R, BidiClass::R]);
}
#[test]
fn n_rules_compose_with_w_rules_realistic_run() {
let mut cls = vec![
BidiClass::AL,
BidiClass::NSM,
BidiClass::EN,
BidiClass::ET,
BidiClass::EN,
BidiClass::CS,
BidiClass::AN,
];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
assert_eq!(
cls,
vec![
BidiClass::R,
BidiClass::R,
BidiClass::AN,
BidiClass::R,
BidiClass::AN,
BidiClass::AN,
BidiClass::AN,
]
);
}
#[test]
fn i1_even_level_l_stays_r_goes_up_one() {
let cls = vec![BidiClass::L, BidiClass::R, BidiClass::L, BidiClass::R];
assert_eq!(resolve_implicit_levels(&cls, 0), vec![0, 1, 0, 1]);
assert_eq!(resolve_implicit_levels(&cls, 2), vec![2, 3, 2, 3]);
}
#[test]
fn i1_even_level_an_en_go_up_two() {
let cls = vec![BidiClass::AN, BidiClass::EN, BidiClass::L, BidiClass::R];
assert_eq!(resolve_implicit_levels(&cls, 0), vec![2, 2, 0, 1]);
assert_eq!(resolve_implicit_levels(&cls, 2), vec![4, 4, 2, 3]);
}
#[test]
fn i2_odd_level_l_en_an_go_up_one_r_stays() {
let cls = vec![BidiClass::L, BidiClass::R, BidiClass::EN, BidiClass::AN];
assert_eq!(resolve_implicit_levels(&cls, 1), vec![2, 1, 2, 2]);
assert_eq!(resolve_implicit_levels(&cls, 3), vec![4, 3, 4, 4]);
}
#[test]
fn implicit_levels_ignore_bn() {
let cls = vec![BidiClass::L, BidiClass::BN, BidiClass::R];
assert_eq!(resolve_implicit_levels(&cls, 0), vec![0, 0, 1]);
assert_eq!(resolve_implicit_levels(&cls, 1), vec![2, 1, 1]);
}
#[test]
fn implicit_levels_nsm_stays_at_embedding_level() {
let cls = vec![BidiClass::L, BidiClass::NSM, BidiClass::R];
assert_eq!(resolve_implicit_levels(&cls, 0), vec![0, 0, 1]);
}
#[test]
fn implicit_levels_empty_input_yields_empty_output() {
assert_eq!(resolve_implicit_levels(&[], 0), Vec::<u8>::new());
assert_eq!(resolve_implicit_levels(&[], 1), Vec::<u8>::new());
}
#[test]
fn implicit_levels_compose_after_n_rules() {
let mut cls = vec![BidiClass::L, BidiClass::ON, BidiClass::L];
resolve_weak_types(&mut cls, BidiClass::L, BidiClass::L);
resolve_neutral_types(&mut cls, 0, BidiClass::L, BidiClass::L);
let levels = resolve_implicit_levels(&cls, 0);
assert_eq!(levels, vec![0, 0, 0]);
}
#[test]
fn implicit_levels_arabic_with_numbers_realistic() {
let mut cls = vec![
BidiClass::AL,
BidiClass::NSM,
BidiClass::EN,
BidiClass::ET,
BidiClass::EN,
BidiClass::CS,
BidiClass::AN,
];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
resolve_neutral_types(&mut cls, 1, BidiClass::R, BidiClass::R);
let levels = resolve_implicit_levels(&cls, 1);
assert_eq!(levels, vec![1, 1, 2, 1, 2, 2, 2]);
}
#[test]
fn implicit_levels_max_depth_overflow_is_explicit() {
let cls = vec![BidiClass::L, BidiClass::R, BidiClass::EN];
assert_eq!(resolve_implicit_levels(&cls, 124), vec![124, 125, 126]);
assert_eq!(resolve_implicit_levels(&cls, 125), vec![126, 125, 126]);
}
#[test]
fn w_rules_full_pipeline_realistic_run() {
let mut cls = vec![
BidiClass::AL,
BidiClass::NSM,
BidiClass::EN,
BidiClass::ET,
BidiClass::EN,
BidiClass::CS,
BidiClass::AN,
];
resolve_weak_types(&mut cls, BidiClass::R, BidiClass::R);
assert_eq!(
cls,
vec![
BidiClass::R,
BidiClass::R,
BidiClass::AN,
BidiClass::ON,
BidiClass::AN,
BidiClass::AN,
BidiClass::AN,
]
);
}
#[test]
fn l1_segment_separator_resets_to_paragraph_level() {
let cls = vec![BidiClass::R, BidiClass::S, BidiClass::R];
let mut lvl = vec![1, 2, 1];
reset_trailing_levels(&cls, &mut lvl, 1);
assert_eq!(lvl, vec![1, 1, 1]);
}
#[test]
fn l1_paragraph_separator_resets_to_paragraph_level() {
let cls = vec![BidiClass::R, BidiClass::R, BidiClass::B];
let mut lvl = vec![1, 1, 2];
reset_trailing_levels(&cls, &mut lvl, 1);
assert_eq!(lvl, vec![1, 1, 1]);
}
#[test]
fn l1_whitespace_before_separator_resets() {
let cls = vec![
BidiClass::R,
BidiClass::R,
BidiClass::WS,
BidiClass::WS,
BidiClass::S,
];
let mut lvl = vec![1, 1, 2, 2, 2];
reset_trailing_levels(&cls, &mut lvl, 1);
assert_eq!(lvl, vec![1, 1, 1, 1, 1]);
}
#[test]
fn l1_isolate_formatting_before_separator_resets() {
let cls = vec![
BidiClass::L,
BidiClass::WS,
BidiClass::PDI,
BidiClass::LRI,
BidiClass::B,
];
let mut lvl = vec![0, 1, 1, 1, 1];
reset_trailing_levels(&cls, &mut lvl, 0);
assert_eq!(lvl, vec![0, 0, 0, 0, 0]);
}
#[test]
fn l1_trailing_whitespace_at_end_of_line_resets() {
let cls = vec![BidiClass::R, BidiClass::R, BidiClass::WS, BidiClass::WS];
let mut lvl = vec![1, 1, 2, 2];
reset_trailing_levels(&cls, &mut lvl, 1);
assert_eq!(lvl, vec![1, 1, 1, 1]);
}
#[test]
fn l1_leading_whitespace_is_left_alone() {
let cls = vec![BidiClass::WS, BidiClass::WS, BidiClass::R, BidiClass::R];
let mut lvl = vec![2, 2, 1, 1];
reset_trailing_levels(&cls, &mut lvl, 1);
assert_eq!(lvl, vec![2, 2, 1, 1]);
}
#[test]
fn l1_interior_whitespace_is_left_alone() {
let cls = vec![BidiClass::R, BidiClass::WS, BidiClass::R];
let mut lvl = vec![1, 2, 1];
reset_trailing_levels(&cls, &mut lvl, 1);
assert_eq!(lvl, vec![1, 2, 1]);
}
#[test]
fn l1_empty_line_is_noop() {
let cls: Vec<BidiClass> = Vec::new();
let mut lvl: Vec<u8> = Vec::new();
reset_trailing_levels(&cls, &mut lvl, 0);
assert!(lvl.is_empty());
}
#[test]
fn l1_uses_original_classes_not_post_w_rules() {
let cls_orig = vec![BidiClass::R, BidiClass::R, BidiClass::B];
let mut lvl = vec![1, 1, 2];
reset_trailing_levels(&cls_orig, &mut lvl, 1);
assert_eq!(lvl, vec![1, 1, 1]);
}
#[test]
fn l1_multiple_separators_each_pull_their_preceding_whitespace() {
let cls = vec![
BidiClass::R,
BidiClass::WS,
BidiClass::S,
BidiClass::R,
BidiClass::WS,
BidiClass::S,
];
let mut lvl = vec![1, 2, 2, 1, 2, 2];
reset_trailing_levels(&cls, &mut lvl, 1);
assert_eq!(lvl, vec![1, 1, 1, 1, 1, 1]);
}
#[test]
#[should_panic(expected = "same length")]
fn l1_length_mismatch_panics() {
let cls = vec![BidiClass::L, BidiClass::L];
let mut lvl = vec![0];
reset_trailing_levels(&cls, &mut lvl, 0);
}
#[test]
fn l2_all_ltr_is_identity() {
assert_eq!(reorder_line(&[0, 0, 0, 0]), vec![0, 1, 2, 3]);
}
#[test]
fn l2_empty_input_is_empty() {
let out = reorder_line(&[]);
assert!(out.is_empty());
}
#[test]
fn l2_all_rtl_is_full_reverse() {
assert_eq!(reorder_line(&[1, 1, 1, 1]), vec![3, 2, 1, 0]);
}
#[test]
fn l2_uax9_example_1_car_means_car_dot() {
let lv = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0];
let visual = reorder_line(&lv);
assert_eq!(visual, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 11, 10, 13]);
}
#[test]
fn l2_uax9_example_2_nested_level_1_and_2() {
let lv = [0, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0];
let visual = reorder_line(&lv);
assert_eq!(
visual,
vec![0, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 1, 2, 3, 15]
);
}
#[test]
fn l2_uax9_example_4_rtl_paragraph_deep_nesting() {
let lv = [
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, ];
let visual = reorder_line(&lv);
assert_eq!(visual[0], 41);
assert_eq!(visual[1], 40);
assert_eq!(visual[2], 39);
assert_eq!(visual.last().copied(), Some(0));
let mut sorted = visual.clone();
sorted.sort_unstable();
assert_eq!(sorted, (0..42).collect::<Vec<_>>());
}
#[test]
fn l2_output_is_always_a_permutation() {
for lv in [
vec![0u8, 1, 0, 1],
vec![1, 0, 1, 0],
vec![0, 2, 1, 2, 0],
vec![3, 3, 1, 1, 3, 3],
vec![5, 4, 3, 2, 1, 0],
vec![0],
vec![1],
vec![0, 0, 0, 0, 0, 0, 0, 0],
] {
let n = lv.len();
let visual = reorder_line(&lv);
assert_eq!(visual.len(), n);
let mut sorted = visual.clone();
sorted.sort_unstable();
assert_eq!(sorted, (0..n).collect::<Vec<_>>());
}
}
#[test]
fn l2_reverse_is_idempotent_when_applied_to_uniform_levels() {
let lv = [1u8, 1, 1, 1, 1];
let visual = reorder_line(&lv);
assert_eq!(visual, vec![4, 3, 2, 1, 0]);
}
#[test]
fn l1_then_l2_pipeline_trailing_space_in_rtl_paragraph() {
let cls = vec![BidiClass::R, BidiClass::R, BidiClass::WS];
let mut lvl = vec![1, 1, 2];
reset_trailing_levels(&cls, &mut lvl, 1);
assert_eq!(lvl, vec![1, 1, 1]);
let visual = reorder_line(&lvl);
assert_eq!(visual, vec![2, 1, 0]);
}
#[test]
fn l3_empty_input_no_op() {
let mut visual: Vec<usize> = Vec::new();
reorder_combining_marks(&[], &[], &mut visual);
assert!(visual.is_empty());
}
#[test]
fn l3_all_ltr_no_op() {
let cls = [BidiClass::L, BidiClass::NSM, BidiClass::NSM];
let lvl = [0, 0, 0];
let mut visual = reorder_line(&lvl);
assert_eq!(visual, vec![0, 1, 2]);
reorder_combining_marks(&cls, &lvl, &mut visual);
assert_eq!(visual, vec![0, 1, 2]);
}
#[test]
fn l3_rtl_base_with_one_mark_swaps_to_base_first() {
let cls = [BidiClass::R, BidiClass::NSM];
let lvl = [1, 1];
let mut visual = reorder_line(&lvl);
assert_eq!(visual, vec![1, 0]);
reorder_combining_marks(&cls, &lvl, &mut visual);
assert_eq!(visual, vec![0, 1]);
}
#[test]
fn l3_rtl_base_with_multiple_marks_preserves_mark_order() {
let cls = [BidiClass::R, BidiClass::NSM, BidiClass::NSM, BidiClass::NSM];
let lvl = [1, 1, 1, 1];
let mut visual = reorder_line(&lvl);
assert_eq!(visual, vec![3, 2, 1, 0]);
reorder_combining_marks(&cls, &lvl, &mut visual);
assert_eq!(visual, vec![0, 1, 2, 3]);
}
#[test]
fn l3_rtl_run_inside_ltr_paragraph() {
let cls = [
BidiClass::L,
BidiClass::L,
BidiClass::R,
BidiClass::NSM,
BidiClass::L,
BidiClass::L,
];
let lvl = [0, 0, 1, 1, 0, 0];
let mut visual = reorder_line(&lvl);
assert_eq!(visual, vec![0, 1, 3, 2, 4, 5]);
reorder_combining_marks(&cls, &lvl, &mut visual);
assert_eq!(visual, vec![0, 1, 2, 3, 4, 5]);
}
#[test]
fn l3_multiple_rtl_clusters_each_rotate_independently() {
let cls = [BidiClass::R, BidiClass::NSM, BidiClass::R, BidiClass::NSM];
let lvl = [1, 1, 1, 1];
let mut visual = reorder_line(&lvl);
assert_eq!(visual, vec![3, 2, 1, 0]);
reorder_combining_marks(&cls, &lvl, &mut visual);
assert_eq!(visual, vec![2, 3, 0, 1]);
}
#[test]
fn l3_leading_orphan_nsm_in_rtl_run_is_left_alone() {
let cls = [BidiClass::NSM, BidiClass::NSM];
let lvl = [1, 1];
let mut visual = reorder_line(&lvl);
assert_eq!(visual, vec![1, 0]);
reorder_combining_marks(&cls, &lvl, &mut visual);
assert_eq!(visual, vec![1, 0]);
}
#[test]
fn l3_idempotent_when_applied_twice() {
let cls = [BidiClass::R, BidiClass::NSM, BidiClass::NSM];
let lvl = [1, 1, 1];
let mut visual = reorder_line(&lvl);
reorder_combining_marks(&cls, &lvl, &mut visual);
let once = visual.clone();
reorder_combining_marks(&cls, &lvl, &mut visual);
assert_eq!(visual, once);
}
#[test]
fn l3_mixed_level_nsm_left_alone() {
let cls = [BidiClass::R, BidiClass::NSM];
let lvl = [1, 0];
let mut visual = reorder_line(&lvl);
assert_eq!(visual, vec![0, 1]);
reorder_combining_marks(&cls, &lvl, &mut visual);
assert_eq!(visual, vec![0, 1]);
}
#[test]
fn x_rules_empty_paragraph() {
let out = resolve_explicit_levels(&[], 0);
assert!(out.levels.is_empty());
assert!(out.effective_classes.is_empty());
assert!(out.removed.is_empty());
}
#[test]
fn x_rules_plain_latin_stays_level_zero() {
let cls = vec![BidiClass::L, BidiClass::L];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![0, 0]);
assert_eq!(out.effective_classes, cls);
assert_eq!(out.removed, vec![false, false]);
}
#[test]
fn x_rules_rtl_paragraph_assigns_level_one() {
let cls = vec![BidiClass::AL, BidiClass::AL];
let out = resolve_explicit_levels(&cls, 1);
assert_eq!(out.levels, vec![1, 1]);
assert_eq!(out.removed, vec![false, false]);
}
#[test]
fn x_rules_rle_pushes_odd_level_pdf_pops() {
let cls = vec![BidiClass::RLE, BidiClass::L, BidiClass::PDF];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![1, 1, 0]);
assert_eq!(out.removed, vec![true, false, true]);
}
#[test]
fn x_rules_lre_pushes_least_greater_even() {
let cls = vec![BidiClass::LRE, BidiClass::L, BidiClass::PDF];
let out0 = resolve_explicit_levels(&cls, 0);
assert_eq!(out0.levels, vec![2, 2, 0]);
let out1 = resolve_explicit_levels(&cls, 1);
assert_eq!(out1.levels, vec![2, 2, 1]);
}
#[test]
fn x_rules_rlo_overrides_to_r() {
let cls = vec![BidiClass::RLO, BidiClass::L, BidiClass::PDF];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![1, 1, 0]);
assert_eq!(out.effective_classes[1], BidiClass::R);
assert_eq!(out.removed, vec![true, false, true]);
}
#[test]
fn x_rules_lro_overrides_to_l() {
let cls = vec![BidiClass::LRO, BidiClass::AL, BidiClass::PDF];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![2, 2, 0]);
assert_eq!(out.effective_classes[1], BidiClass::L);
}
#[test]
fn x_rules_rli_pushes_isolate_pdi_pops() {
let cls = vec![BidiClass::RLI, BidiClass::L, BidiClass::PDI];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![0, 1, 0]);
assert_eq!(out.removed, vec![false, false, false]);
}
#[test]
fn x_rules_lri_pushes_least_greater_even() {
let cls = vec![BidiClass::LRI, BidiClass::AL, BidiClass::PDI];
let out = resolve_explicit_levels(&cls, 1);
assert_eq!(out.levels, vec![1, 2, 1]);
}
#[test]
fn x_rules_fsi_with_strong_l_inside_resolves_lri() {
let cls = vec![BidiClass::FSI, BidiClass::L, BidiClass::PDI];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![0, 2, 0]);
}
#[test]
fn x_rules_fsi_with_strong_r_inside_resolves_rli() {
let cls = vec![BidiClass::FSI, BidiClass::AL, BidiClass::PDI];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![0, 1, 0]);
}
#[test]
fn x_rules_fsi_with_no_strong_inside_resolves_lri() {
let cls = vec![BidiClass::FSI, BidiClass::WS, BidiClass::PDI];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![0, 2, 0]);
}
#[test]
fn x_rules_b_assigned_paragraph_level() {
let cls = vec![BidiClass::RLE, BidiClass::L, BidiClass::B];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![1, 1, 0]);
}
#[test]
fn x_rules_bn_removed_by_x9() {
let cls = vec![BidiClass::RLE, BidiClass::BN, BidiClass::L, BidiClass::PDF];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![1, 1, 1, 0]);
assert_eq!(out.removed, vec![true, true, false, true]);
}
#[test]
fn x_rules_nested_embeddings_at_most_three_deep() {
let cls = vec![
BidiClass::RLE,
BidiClass::LRE,
BidiClass::RLE,
BidiClass::L,
BidiClass::PDF,
BidiClass::PDF,
BidiClass::PDF,
];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels[3], 3);
assert_eq!(out.levels.len(), 7);
assert_eq!(out.removed, vec![true, true, true, false, true, true, true]);
}
#[test]
fn x_rules_overflow_embedding_at_max_depth() {
let mut cls = vec![BidiClass::RLE; 64];
cls.push(BidiClass::L);
cls.push(BidiClass::PDF);
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels[64], 125);
}
#[test]
fn x_rules_unmatched_pdf_at_paragraph_level_ignored() {
let cls = vec![BidiClass::PDF, BidiClass::L];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![0, 0]);
assert_eq!(out.removed, vec![true, false]);
}
#[test]
fn x_rules_unmatched_pdi_ignored() {
let cls = vec![BidiClass::PDI, BidiClass::L];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![0, 0]);
assert_eq!(out.removed, vec![false, false]);
}
#[test]
fn x_rules_pdi_pops_embeddings_inside_isolate() {
let cls = vec![
BidiClass::RLI,
BidiClass::RLE,
BidiClass::L,
BidiClass::PDI,
BidiClass::L,
];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels[0], 0);
assert_eq!(out.levels[2], 3);
assert_eq!(out.levels[3], 0);
assert_eq!(out.levels[4], 0);
}
#[test]
fn x_rules_least_greater_odd_helper_table() {
assert_eq!(least_greater_odd(0), 1);
assert_eq!(least_greater_odd(1), 3);
assert_eq!(least_greater_odd(2), 3);
assert_eq!(least_greater_odd(3), 5);
assert_eq!(least_greater_odd(4), 5);
assert_eq!(least_greater_even(0), 2);
assert_eq!(least_greater_even(1), 2);
assert_eq!(least_greater_even(2), 4);
assert_eq!(least_greater_even(3), 4);
assert_eq!(least_greater_even(4), 6);
}
#[test]
fn x_rules_rle_inside_isolate_pdf_only_matches_inside() {
let cls = vec![
BidiClass::RLI,
BidiClass::RLE,
BidiClass::L,
BidiClass::PDF,
BidiClass::PDI,
BidiClass::L,
];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels[2], 3);
assert_eq!(out.levels[5], 0);
}
#[test]
fn x_rules_pdi_inside_overflow_isolate_decrements_overflow_isolate() {
let cls = vec![
BidiClass::RLI,
BidiClass::RLI,
BidiClass::L,
BidiClass::PDI,
BidiClass::PDI,
BidiClass::L,
];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels[2], 3);
assert_eq!(out.levels[5], 0);
}
#[test]
fn x_rules_max_depth_constant_is_125() {
assert_eq!(MAX_DEPTH, 125);
}
#[test]
fn level_runs_empty_input_returns_empty() {
assert!(level_runs(&[]).is_empty());
}
#[test]
fn level_runs_single_level_collapses_to_one_run() {
let runs = level_runs(&[0, 0, 0]);
assert_eq!(runs.len(), 1);
assert_eq!((runs[0].start, runs[0].end, runs[0].level), (0, 3, 0));
assert_eq!(runs[0].len(), 3);
assert!(!runs[0].is_empty());
}
#[test]
fn level_runs_split_on_level_change() {
let runs = level_runs(&[0, 0, 1, 1, 1, 0, 2]);
assert_eq!(runs.len(), 4);
assert_eq!((runs[0].start, runs[0].end, runs[0].level), (0, 2, 0));
assert_eq!((runs[1].start, runs[1].end, runs[1].level), (2, 5, 1));
assert_eq!((runs[2].start, runs[2].end, runs[2].level), (5, 6, 0));
assert_eq!((runs[3].start, runs[3].end, runs[3].level), (6, 7, 2));
}
#[test]
fn level_runs_cover_input_exactly() {
let levels = [0, 0, 1, 0, 1, 1, 0];
let runs = level_runs(&levels);
let mut expect_next = 0;
for r in &runs {
assert_eq!(r.start, expect_next);
assert!(r.end > r.start);
expect_next = r.end;
}
assert_eq!(expect_next, levels.len());
}
fn build(s: &str) -> (Vec<BidiClass>, ExplicitLevels, u8) {
let cls: Vec<BidiClass> = s.chars().map(bidi_class).collect();
let pl = paragraph_level(s);
let out = resolve_explicit_levels(&cls, pl);
(cls, out, pl)
}
#[test]
fn x10_empty_paragraph_no_sequences() {
let cls: Vec<BidiClass> = Vec::new();
let out = resolve_explicit_levels(&cls, 0);
let seqs = isolating_run_sequences(&cls, &out, 0);
assert!(seqs.is_empty());
}
#[test]
fn x10_pure_l_one_sequence_one_run() {
let (cls, out, pl) = build("Hello");
let seqs = isolating_run_sequences(&cls, &out, pl);
assert_eq!(seqs.len(), 1);
assert_eq!(seqs[0].runs.len(), 1);
assert_eq!(seqs[0].level, 0);
assert_eq!(seqs[0].sos, BidiClass::L);
assert_eq!(seqs[0].eos, BidiClass::L);
}
#[test]
fn x10_arabic_paragraph_rtl_sos_eos_r() {
let (cls, out, pl) = build("\u{05E9}\u{05DC}\u{05D5}\u{05DD}");
assert_eq!(pl, 1);
let seqs = isolating_run_sequences(&cls, &out, pl);
assert_eq!(seqs.len(), 1);
assert_eq!(seqs[0].level, 1);
assert_eq!(seqs[0].sos, BidiClass::R);
assert_eq!(seqs[0].eos, BidiClass::R);
}
#[test]
fn x10_rle_split_emits_three_sequences_with_correct_sos_eos() {
let cls = vec![
BidiClass::L,
BidiClass::RLE,
BidiClass::L,
BidiClass::PDF,
BidiClass::L,
];
let out = resolve_explicit_levels(&cls, 0);
let seqs = isolating_run_sequences(&cls, &out, 0);
assert_eq!(seqs.len(), 3);
assert_eq!(seqs[0].level, 0);
assert_eq!(seqs[0].sos, BidiClass::L);
assert_eq!(seqs[0].eos, BidiClass::R);
assert_eq!(seqs[1].level, 1);
assert_eq!(seqs[1].sos, BidiClass::R);
assert_eq!(seqs[1].eos, BidiClass::R);
assert_eq!(seqs[2].level, 0);
assert_eq!(seqs[2].sos, BidiClass::R);
assert_eq!(seqs[2].eos, BidiClass::L);
}
#[test]
fn x10_lri_chains_outer_runs_into_one_sequence() {
let cls = vec![
BidiClass::L,
BidiClass::LRI,
BidiClass::L,
BidiClass::PDI,
BidiClass::L,
];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![0, 0, 2, 0, 0]);
let runs = level_runs(&out.levels);
assert_eq!(runs.len(), 3);
let seqs = isolating_run_sequences(&cls, &out, 0);
assert_eq!(seqs.len(), 2);
assert_eq!(seqs[0].runs.len(), 2);
assert_eq!(seqs[0].runs[0].start, 0);
assert_eq!(seqs[0].runs[0].end, 2);
assert_eq!(seqs[0].runs[1].start, 3);
assert_eq!(seqs[0].runs[1].end, 5);
assert_eq!(seqs[0].level, 0);
assert_eq!(seqs[1].runs.len(), 1);
assert_eq!(seqs[1].runs[0].start, 2);
assert_eq!(seqs[1].runs[0].end, 3);
assert_eq!(seqs[1].level, 2);
}
#[test]
fn x10_unmatched_isolate_initiator_triggers_eos_paragraph_fallback() {
let cls = vec![BidiClass::L, BidiClass::LRI, BidiClass::L];
let out = resolve_explicit_levels(&cls, 0);
assert_eq!(out.levels, vec![0, 0, 2]);
let seqs = isolating_run_sequences(&cls, &out, 0);
assert_eq!(seqs.len(), 2);
assert_eq!(seqs[0].runs[0].start, 0);
assert_eq!(seqs[0].runs[0].end, 2);
assert_eq!(seqs[0].sos, BidiClass::L);
assert_eq!(seqs[0].eos, BidiClass::L);
assert_eq!(seqs[1].sos, BidiClass::L);
assert_eq!(seqs[1].eos, BidiClass::L);
}
#[test]
fn x10_paragraph_level_1_rtl_default_sos_eos() {
let cls = vec![BidiClass::R, BidiClass::R, BidiClass::R];
let out = resolve_explicit_levels(&cls, 1);
assert_eq!(out.levels, vec![1, 1, 1]);
let seqs = isolating_run_sequences(&cls, &out, 1);
assert_eq!(seqs.len(), 1);
assert_eq!(seqs[0].sos, BidiClass::R);
assert_eq!(seqs[0].eos, BidiClass::R);
}
#[test]
fn x10_each_level_run_belongs_to_exactly_one_sequence() {
let cls = vec![
BidiClass::L, BidiClass::RLE, BidiClass::L, BidiClass::LRI, BidiClass::L, BidiClass::PDI, BidiClass::L, BidiClass::PDF, BidiClass::L, ];
let out = resolve_explicit_levels(&cls, 0);
let runs = level_runs(&out.levels);
let total_run_count = runs.len();
let seqs = isolating_run_sequences(&cls, &out, 0);
let mut counted = 0;
for s in &seqs {
counted += s.runs.len();
for r in &s.runs {
assert_eq!(r.level, s.level);
}
}
assert_eq!(
counted, total_run_count,
"every level run belongs to exactly one sequence"
);
}
#[test]
fn x10_indices_iterator_skips_x9_removed() {
let cls = vec![
BidiClass::L,
BidiClass::RLE,
BidiClass::L,
BidiClass::PDF,
BidiClass::L,
];
let out = resolve_explicit_levels(&cls, 0);
let seqs = isolating_run_sequences(&cls, &out, 0);
assert_eq!(seqs.len(), 3);
let s0: Vec<usize> = seqs[0].indices(&out.removed).collect();
assert_eq!(s0, vec![0]);
let s1: Vec<usize> = seqs[1].indices(&out.removed).collect();
assert_eq!(s1, vec![2]);
let s2: Vec<usize> = seqs[2].indices(&out.removed).collect();
assert_eq!(s2, vec![4]);
}
#[test]
fn x10_indices_for_chained_isolate_sequence_concatenates_runs() {
let cls = vec![
BidiClass::L,
BidiClass::LRI,
BidiClass::L,
BidiClass::PDI,
BidiClass::L,
];
let out = resolve_explicit_levels(&cls, 0);
let seqs = isolating_run_sequences(&cls, &out, 0);
let s0: Vec<usize> = seqs[0].indices(&out.removed).collect();
assert_eq!(s0, vec![0, 1, 3, 4]);
}
#[test]
fn x10_sequences_compose_with_existing_w_n_i_pipeline() {
let cls = vec![
BidiClass::L,
BidiClass::RLE,
BidiClass::AL,
BidiClass::EN,
BidiClass::PDF,
BidiClass::L,
];
let out = resolve_explicit_levels(&cls, 0);
let seqs = isolating_run_sequences(&cls, &out, 0);
assert!(!seqs.is_empty());
for s in &seqs {
assert!(matches!(s.sos, BidiClass::L | BidiClass::R));
assert!(matches!(s.eos, BidiClass::L | BidiClass::R));
let mut indices: Vec<usize> = s.indices(&out.removed).collect();
let mut seq_classes: Vec<BidiClass> =
indices.iter().map(|&i| out.effective_classes[i]).collect();
resolve_weak_types(&mut seq_classes, s.sos, s.eos);
assert!(seq_classes.iter().all(|c| !matches!(c, BidiClass::AL)));
let _ = indices.drain(..);
}
}
#[test]
fn x10_matching_pdi_skips_non_isolate_formatting() {
let cls = vec![
BidiClass::L,
BidiClass::LRI, BidiClass::RLE, BidiClass::L, BidiClass::PDF, BidiClass::PDI, BidiClass::L,
];
let pdi = matching_pdi(&cls, 1);
assert_eq!(pdi, Some(5));
}
#[test]
fn x10_matching_pdi_returns_none_for_unmatched_initiator() {
let cls = vec![BidiClass::L, BidiClass::LRI, BidiClass::L];
assert_eq!(matching_pdi(&cls, 1), None);
}
#[test]
fn x10_matching_pdi_counts_nested_isolates_correctly() {
let cls = vec![
BidiClass::LRI,
BidiClass::LRI,
BidiClass::PDI,
BidiClass::PDI,
];
assert_eq!(matching_pdi(&cls, 0), Some(3));
assert_eq!(matching_pdi(&cls, 1), Some(2));
}
#[test]
fn process_paragraph_classes_empty_input_returns_empty_carrier() {
let p = process_paragraph_classes(&[], None);
assert_eq!(p.paragraph_level, 0);
assert!(p.classes.is_empty());
assert!(p.effective_classes.is_empty());
assert!(p.removed.is_empty());
assert!(p.levels.is_empty());
}
#[test]
fn process_paragraph_classes_first_strong_l_is_ltr() {
let cls = vec![BidiClass::L, BidiClass::L, BidiClass::L];
let p = process_paragraph_classes(&cls, None);
assert_eq!(p.paragraph_level, 0);
assert_eq!(p.levels, vec![0, 0, 0]);
}
#[test]
fn process_paragraph_classes_first_strong_r_is_rtl() {
let cls = vec![BidiClass::R, BidiClass::R, BidiClass::R];
let p = process_paragraph_classes(&cls, None);
assert_eq!(p.paragraph_level, 1);
assert_eq!(p.levels, vec![1, 1, 1]);
}
#[test]
fn process_paragraph_classes_first_strong_al_is_rtl() {
let cls = vec![BidiClass::AL, BidiClass::AL, BidiClass::AL];
let p = process_paragraph_classes(&cls, None);
assert_eq!(p.paragraph_level, 1);
assert_eq!(p.levels, vec![1, 1, 1]);
assert_eq!(p.effective_classes, vec![BidiClass::AL; 3]);
}
#[test]
fn process_paragraph_classes_p3_fallback_is_ltr() {
let cls = vec![BidiClass::ON, BidiClass::WS, BidiClass::ON];
let p = process_paragraph_classes(&cls, None);
assert_eq!(p.paragraph_level, 0);
assert_eq!(p.levels, vec![0, 0, 0]);
}
#[test]
fn process_paragraph_classes_base_level_overrides_first_strong() {
let cls = vec![BidiClass::L, BidiClass::L, BidiClass::L];
let p = process_paragraph_classes(&cls, Some(1));
assert_eq!(p.paragraph_level, 1);
assert_eq!(p.levels, vec![2, 2, 2]);
}
#[test]
fn process_paragraph_classes_base_level_clamps_to_low_bit() {
let cls = vec![BidiClass::L];
let p = process_paragraph_classes(&cls, Some(5));
assert_eq!(p.paragraph_level, 1);
}
#[test]
fn process_paragraph_classes_isolate_span_skipped_by_p2() {
let cls = vec![BidiClass::LRI, BidiClass::L, BidiClass::PDI, BidiClass::R];
let p = process_paragraph_classes(&cls, None);
assert_eq!(p.paragraph_level, 1);
}
#[test]
fn process_paragraph_classes_unmatched_isolate_initiator_returns_p3_default() {
let cls = vec![BidiClass::LRI, BidiClass::R, BidiClass::R];
let p = process_paragraph_classes(&cls, None);
assert_eq!(p.paragraph_level, 0);
}
#[test]
fn process_paragraph_classes_x9_removed_chars_retain_x_level() {
let cls = vec![
BidiClass::L,
BidiClass::RLE,
BidiClass::L,
BidiClass::PDF,
BidiClass::L,
];
let p = process_paragraph_classes(&cls, None);
assert_eq!(p.paragraph_level, 0);
assert_eq!(p.removed, vec![false, true, false, true, false]);
assert_eq!(p.levels[0], 0);
assert_eq!(p.levels[2], 2);
assert_eq!(p.levels[4], 0);
}
#[test]
fn process_paragraph_classes_mixed_l_r_compose_full_pipeline() {
let cls = vec![
BidiClass::L,
BidiClass::L,
BidiClass::L, BidiClass::WS,
BidiClass::R,
BidiClass::R,
BidiClass::R, ];
let p = process_paragraph_classes(&cls, None);
assert_eq!(p.paragraph_level, 0);
assert_eq!(p.levels, vec![0, 0, 0, 0, 1, 1, 1]);
}
#[test]
fn process_paragraph_classes_rtl_paragraph_lifts_l_to_level_2() {
let cls = vec![
BidiClass::R,
BidiClass::R, BidiClass::WS,
BidiClass::L,
BidiClass::L,
BidiClass::L, BidiClass::WS,
BidiClass::R,
BidiClass::R, ];
let p = process_paragraph_classes(&cls, None);
assert_eq!(p.paragraph_level, 1);
assert_eq!(p.levels, vec![1, 1, 1, 2, 2, 2, 1, 1, 1]);
}
#[test]
fn process_paragraph_text_byte_offsets_track_chars() {
let (p, offsets) = process_paragraph("Hello", None);
assert_eq!(p.paragraph_level, 0);
assert_eq!(p.levels, vec![0; 5]);
assert_eq!(offsets, vec![0, 1, 2, 3, 4]);
}
#[test]
fn process_paragraph_text_multi_byte_chars_byte_offsets_advance() {
let (p, offsets) = process_paragraph("\u{03A9}a", None);
assert_eq!(p.classes.len(), 2);
assert_eq!(offsets, vec![0, 2]);
assert_eq!(p.levels, vec![0, 0]);
}
#[test]
fn reorder_paragraph_ltr_only_is_identity() {
let p = process_paragraph_classes(&[BidiClass::L; 4], None);
let perm = p.reorder_paragraph();
assert_eq!(perm, vec![0, 1, 2, 3]);
}
#[test]
fn reorder_paragraph_rtl_block_reverses() {
let cls = vec![
BidiClass::L,
BidiClass::L,
BidiClass::L,
BidiClass::R,
BidiClass::R,
BidiClass::R,
];
let p = process_paragraph_classes(&cls, None);
let perm = p.reorder_paragraph();
assert_eq!(perm, vec![0, 1, 2, 5, 4, 3]);
}
#[test]
fn reorder_line_range_per_line_works() {
let p = process_paragraph_classes(&[BidiClass::L; 5], None);
let line1 = p.reorder_line_range(0..2);
let line2 = p.reorder_line_range(2..5);
assert_eq!(line1, vec![0, 1]);
assert_eq!(line2, vec![0, 1, 2]);
}
#[test]
#[should_panic]
fn reorder_line_range_out_of_bounds_panics() {
let p = process_paragraph_classes(&[BidiClass::L; 3], None);
let _ = p.reorder_line_range(0..10);
}
#[test]
fn paragraph_level_from_classes_matches_text_walker() {
for s in &[
"Hello",
"\u{05D0}\u{05D1}\u{05D2}", "\u{0627}\u{0628}\u{0629}", "Hi \u{05D0}\u{05D1}", "\u{05D0}\u{05D1} Hi", "\u{2066}A\u{2069}\u{05D0}", ] {
let cls: Vec<_> = s.chars().map(bidi_class).collect();
assert_eq!(
paragraph_level_from_classes(&cls),
paragraph_level(s),
"mismatch for input {s:?}",
);
}
}
#[test]
fn process_text_empty_input_returns_empty_carrier() {
let t = process_text("", None);
assert!(t.is_empty());
assert_eq!(t.len(), 0);
assert_eq!(t.total_chars, 0);
assert!(t.paragraphs.is_empty());
}
#[test]
fn process_text_no_paragraph_separator_single_paragraph() {
let t = process_text("Hello", None);
assert_eq!(t.len(), 1);
assert_eq!(t.total_chars, 5);
let p = &t.paragraphs[0];
assert_eq!(p.byte_range, 0..5);
assert_eq!(p.char_offset, 0);
assert_eq!(p.bidi.paragraph_level, 0);
assert_eq!(p.bidi.levels, vec![0; 5]);
}
#[test]
fn process_text_lf_terminator_kept_with_preceding_paragraph_per_p1() {
let t = process_text("Hi\nyo", None);
assert_eq!(t.len(), 2);
assert_eq!(t.total_chars, 5);
let p0 = &t.paragraphs[0];
let p1 = &t.paragraphs[1];
assert_eq!(p0.byte_range, 0..3);
assert_eq!(p0.char_offset, 0);
assert_eq!(p0.bidi.levels.len(), 3);
assert_eq!(p1.byte_range, 3..5);
assert_eq!(p1.char_offset, 3);
assert_eq!(p1.bidi.levels.len(), 2);
}
#[test]
fn process_text_terminal_lf_does_not_create_phantom_paragraph() {
let t = process_text("Hi\n", None);
assert_eq!(t.len(), 1);
assert_eq!(t.total_chars, 3);
let p = &t.paragraphs[0];
assert_eq!(p.byte_range, 0..3);
assert_eq!(p.bidi.classes.last(), Some(&BidiClass::B));
}
#[test]
fn process_text_per_paragraph_p2_runs_independently() {
let t = process_text("Hi\n\u{05D0}\u{05D1}", None);
assert_eq!(t.len(), 2);
assert_eq!(t.paragraphs[0].bidi.paragraph_level, 0);
assert_eq!(t.paragraphs[1].bidi.paragraph_level, 1);
}
#[test]
fn process_text_base_level_override_applies_uniformly() {
let t = process_text("Hi\nyo", Some(1));
assert_eq!(t.len(), 2);
for p in &t.paragraphs {
assert_eq!(p.bidi.paragraph_level, 1);
}
}
#[test]
fn process_text_char_byte_offsets_are_whole_input_indices() {
let s = "A\n\u{05D0}\u{05D1}";
let t = process_text(s, None);
assert_eq!(t.len(), 2);
assert_eq!(t.paragraphs[0].char_byte_offsets, vec![0, 1]);
assert_eq!(t.paragraphs[1].char_byte_offsets, vec![2, 4]);
for p in &t.paragraphs {
for (i, &off) in p.char_byte_offsets.iter().enumerate() {
let c = s[off..].chars().next().expect("char at byte offset");
assert_eq!(bidi_class(c), p.bidi.classes[i]);
}
}
}
#[test]
fn process_text_byte_range_covers_whole_input_with_no_gap() {
let s = "AAA\nBBB\nCCC";
let t = process_text(s, None);
assert_eq!(t.len(), 3);
assert_eq!(t.paragraphs[0].byte_range.start, 0);
assert_eq!(
t.paragraphs[2].byte_range.end,
s.len(),
"last paragraph end equals input length",
);
for w in t.paragraphs.windows(2) {
assert_eq!(
w[0].byte_range.end, w[1].byte_range.start,
"paragraph ranges tile contiguously",
);
}
}
#[test]
fn process_text_char_offset_accumulates_correctly() {
let t = process_text("AB\nCDE\nF", None);
assert_eq!(t.len(), 3);
assert_eq!(t.paragraphs[0].char_offset, 0);
assert_eq!(t.paragraphs[1].char_offset, 3);
assert_eq!(t.paragraphs[2].char_offset, 7);
assert_eq!(t.total_chars, 8);
}
#[test]
fn text_bidi_locate_char_returns_paragraph_and_local_index() {
let t = process_text("AB\nCD", None);
assert_eq!(t.locate_char(0), Some((0, 0)));
assert_eq!(t.locate_char(1), Some((0, 1)));
assert_eq!(t.locate_char(2), Some((0, 2)));
assert_eq!(t.locate_char(3), Some((1, 0)));
assert_eq!(t.locate_char(4), Some((1, 1)));
assert_eq!(t.locate_char(5), None);
assert_eq!(t.locate_char(99), None);
}
#[test]
fn process_text_splits_on_every_paragraph_separator_class_b_codepoint() {
for sep in &[
'\u{000A}', '\u{000D}', '\u{0085}', '\u{001C}', '\u{001D}', '\u{001E}', '\u{2029}', ] {
assert_eq!(
bidi_class(*sep),
BidiClass::B,
"test-input invariant: {:#x} should be class B",
*sep as u32,
);
let s = format!("A{sep}B");
let t = process_text(&s, None);
assert_eq!(t.len(), 2, "{sep:?} should split into 2 paragraphs");
}
}
#[test]
fn process_text_matches_per_paragraph_process_paragraph_call() {
let s = "Hi \u{05D0}\u{05D1}\n\u{0627}\u{0628}\nBye";
let t = process_text(s, None);
let slices = split_paragraphs(s);
assert_eq!(t.paragraphs.len(), slices.len());
let mut byte_start = 0usize;
for (carrier, slice) in t.paragraphs.iter().zip(slices.iter()) {
let (expected, expected_offsets) = process_paragraph(slice, None);
assert_eq!(carrier.bidi, expected);
let shifted: Vec<usize> = expected_offsets.iter().map(|o| o + byte_start).collect();
assert_eq!(carrier.char_byte_offsets, shifted);
byte_start += slice.len();
}
}
#[test]
fn process_text_total_chars_matches_input_char_count() {
for s in &[
"",
"Hi",
"Hi\nyo",
"Hi\n\u{05D0}\u{05D1}",
"A\nB\nC\n",
"\u{05D0}\u{05D1}\n\u{0627}\u{0628}",
] {
let t = process_text(s, None);
assert_eq!(t.total_chars, s.chars().count(), "for input {s:?}");
}
}
#[test]
fn process_text_base_level_low_bit_clamp_per_paragraph() {
let t = process_text("Hi\nyo", Some(5));
for p in &t.paragraphs {
assert_eq!(p.bidi.paragraph_level, 1);
}
}
#[test]
fn process_text_locate_char_round_trips_with_offset_arithmetic() {
let t = process_text("AB\nCD\nE", None);
for k in 0..t.total_chars {
let (pi, ki) = t.locate_char(k).expect("k in bounds");
assert_eq!(t.paragraphs[pi].char_offset + ki, k);
}
}
#[test]
fn paired_bracket_round_trips_each_ascii_pair() {
for (open, close) in [('(', ')'), ('[', ']'), ('{', '}')] {
let (other, kind) = paired_bracket(open).expect("opener recognised");
assert_eq!(other, close);
assert_eq!(kind, BracketKind::Open);
let (other, kind) = paired_bracket(close).expect("closer recognised");
assert_eq!(other, open);
assert_eq!(kind, BracketKind::Close);
}
}
#[test]
fn paired_bracket_returns_none_for_non_bracket_codepoints() {
for c in [
'a',
'Z',
'0',
' ',
',',
'!',
'\u{0028}'.to_ascii_uppercase(),
] {
if matches!(c, '(' | ')' | '[' | ']' | '{' | '}') {
continue;
}
assert!(paired_bracket(c).is_none(), "{c:?} should not pair");
}
for (open, close) in [
('\u{2329}', '\u{232A}'),
('\u{3008}', '\u{3009}'),
('\u{27E6}', '\u{27E7}'),
] {
assert_eq!(paired_bracket(open), Some((close, BracketKind::Open)));
assert_eq!(paired_bracket(close), Some((open, BracketKind::Close)));
}
for c in ['<', '>', '\u{00AB}', '\u{00BB}'] {
assert!(paired_bracket(c).is_none(), "{c:?} should not pair");
}
}
#[test]
fn bracket_pairs_empty_input_yields_empty_list() {
let pairs = bracket_pairs(&[], &[]);
assert!(pairs.is_empty());
}
#[test]
fn bracket_pairs_simple_single_pair() {
let chars: Vec<char> = "a(b)c".chars().collect();
let classes: Vec<_> = chars.iter().copied().map(bidi_class).collect();
assert_eq!(bracket_pairs(&chars, &classes), vec![(1, 3)]);
}
#[test]
fn bracket_pairs_unbalanced_closer_only_yields_nothing() {
let chars: Vec<char> = "a)b(c".chars().collect();
let classes: Vec<_> = chars.iter().copied().map(bidi_class).collect();
assert!(bracket_pairs(&chars, &classes).is_empty());
}
#[test]
fn bracket_pairs_mismatched_closer_does_not_pop() {
let chars: Vec<char> = "a(b]c".chars().collect();
let classes: Vec<_> = chars.iter().copied().map(bidi_class).collect();
assert!(bracket_pairs(&chars, &classes).is_empty());
}
#[test]
fn bracket_pairs_nested_returns_sorted_by_opener() {
let chars: Vec<char> = "a(b(c)d)".chars().collect();
let classes: Vec<_> = chars.iter().copied().map(bidi_class).collect();
assert_eq!(bracket_pairs(&chars, &classes), vec![(1, 7), (3, 5)]);
}
#[test]
fn bracket_pairs_unmatched_inner_opener_still_pairs_outer() {
let chars: Vec<char> = "a(b[c)d]".chars().collect();
let classes: Vec<_> = chars.iter().copied().map(bidi_class).collect();
assert_eq!(bracket_pairs(&chars, &classes), vec![(1, 5)]);
}
#[test]
fn bracket_pairs_curly_matches_curly() {
let chars: Vec<char> = "a(b{c}d)".chars().collect();
let classes: Vec<_> = chars.iter().copied().map(bidi_class).collect();
assert_eq!(bracket_pairs(&chars, &classes), vec![(1, 7), (3, 5)]);
}
#[test]
fn bracket_pairs_overflow_returns_empty_list() {
let chars: Vec<char> = vec!['('; 64];
let classes: Vec<_> = chars.iter().copied().map(bidi_class).collect();
assert!(bracket_pairs(&chars, &classes).is_empty());
}
#[test]
fn bracket_pairs_non_on_position_skipped() {
let chars: Vec<char> = "a(b)c".chars().collect();
let mut classes: Vec<_> = chars.iter().copied().map(bidi_class).collect();
classes[1] = BidiClass::R; assert!(bracket_pairs(&chars, &classes).is_empty());
}
#[test]
fn resolve_bracket_pairs_n0b_inside_strong_matches_embedding() {
let chars: Vec<char> = "a(b)c".chars().collect();
let mut cls: Vec<_> = chars.iter().copied().map(bidi_class).collect();
let pairs = bracket_pairs(&chars, &cls);
resolve_bracket_pairs(&mut cls, &pairs, 0, BidiClass::L);
assert_eq!(cls[1], BidiClass::L);
assert_eq!(cls[3], BidiClass::L);
}
#[test]
fn resolve_bracket_pairs_n0b_en_inside_counts_as_r() {
let mut cls = vec![
BidiClass::R,
BidiClass::ON,
BidiClass::EN,
BidiClass::ON,
BidiClass::R,
];
let pairs = vec![(1usize, 3usize)];
resolve_bracket_pairs(&mut cls, &pairs, 1, BidiClass::R);
assert_eq!(cls[1], BidiClass::R);
assert_eq!(cls[3], BidiClass::R);
}
#[test]
fn resolve_bracket_pairs_n0c1_opposite_inside_picks_preceding_strong() {
let mut cls = vec![BidiClass::L, BidiClass::ON, BidiClass::L, BidiClass::ON];
let pairs = vec![(1usize, 3usize)];
resolve_bracket_pairs(&mut cls, &pairs, 1, BidiClass::R);
assert_eq!(cls[1], BidiClass::L);
assert_eq!(cls[3], BidiClass::L);
}
#[test]
fn resolve_bracket_pairs_n0c2_opposite_inside_falls_to_embedding() {
let mut cls = vec![BidiClass::R, BidiClass::ON, BidiClass::L, BidiClass::ON];
let pairs = vec![(1usize, 3usize)];
resolve_bracket_pairs(&mut cls, &pairs, 1, BidiClass::R);
assert_eq!(cls[1], BidiClass::R);
assert_eq!(cls[3], BidiClass::R);
}
#[test]
fn resolve_bracket_pairs_n0c1_uses_sos_when_no_preceding_strong() {
let mut cls = vec![BidiClass::ON, BidiClass::L, BidiClass::ON];
let pairs = vec![(0usize, 2usize)];
resolve_bracket_pairs(&mut cls, &pairs, 1, BidiClass::L);
assert_eq!(cls[0], BidiClass::L);
assert_eq!(cls[2], BidiClass::L);
}
#[test]
fn resolve_bracket_pairs_n0d_nothing_strong_leaves_pair_untouched() {
let mut cls = vec![
BidiClass::L,
BidiClass::ON,
BidiClass::WS,
BidiClass::ON,
BidiClass::L,
];
let pairs = vec![(1usize, 3usize)];
resolve_bracket_pairs(&mut cls, &pairs, 0, BidiClass::L);
assert_eq!(cls[1], BidiClass::ON);
assert_eq!(cls[3], BidiClass::ON);
}
#[test]
fn resolve_bracket_pairs_sequential_lets_inner_see_outer_rewrite() {
let mut cls = vec![
BidiClass::R, BidiClass::ON, BidiClass::R, BidiClass::ON, BidiClass::L, BidiClass::ON, BidiClass::R, BidiClass::ON, BidiClass::R, ];
let pairs = vec![(1usize, 7usize), (3usize, 5usize)];
resolve_bracket_pairs(&mut cls, &pairs, 1, BidiClass::R);
assert_eq!(cls[1], BidiClass::R);
assert_eq!(cls[7], BidiClass::R);
assert_eq!(cls[3], BidiClass::R);
assert_eq!(cls[5], BidiClass::R);
}
#[test]
fn resolve_bracket_pairs_trailing_nsm_inherits_bracket_type() {
let mut cls = vec![
BidiClass::L,
BidiClass::ON,
BidiClass::L,
BidiClass::ON,
BidiClass::NSM,
];
let pairs = vec![(1usize, 3usize)];
resolve_bracket_pairs(&mut cls, &pairs, 0, BidiClass::L);
assert_eq!(cls[3], BidiClass::L);
assert_eq!(cls[4], BidiClass::L);
}
#[test]
fn resolve_bracket_pairs_no_pairs_is_noop() {
let mut cls = vec![BidiClass::L, BidiClass::ON, BidiClass::L];
let before = cls.clone();
resolve_bracket_pairs(&mut cls, &[], 0, BidiClass::L);
assert_eq!(cls, before);
}
#[test]
fn process_paragraph_with_brackets_smoke_ltr() {
let (p, _offsets) = process_paragraph_with_brackets("(a)", None);
assert_eq!(p.paragraph_level, 0);
assert_eq!(p.levels, vec![0, 0, 0]);
}
#[test]
fn process_paragraph_with_brackets_n0b_promotes_brackets_in_rtl() {
let text = "\u{05D0}\u{05D1}(\u{05D2}\u{05D3})";
let (p, _offsets) = process_paragraph_with_brackets(text, None);
assert_eq!(p.paragraph_level, 1);
assert_eq!(p.levels, vec![1, 1, 1, 1, 1, 1]);
}
#[test]
fn process_paragraph_with_brackets_n0c2_diverges_from_plain_n() {
let text = "\u{05D0}(abc)\u{05D1}";
let (p, _offsets) = process_paragraph_with_brackets(text, None);
assert_eq!(p.paragraph_level, 1);
assert_eq!(p.levels[0], 1); assert_eq!(p.levels[1], 1); assert_eq!(p.levels[2], 2); assert_eq!(p.levels[3], 2); assert_eq!(p.levels[4], 2); assert_eq!(p.levels[5], 1); assert_eq!(p.levels[6], 1); }
#[test]
fn process_paragraph_with_brackets_n0d_unchanged_when_no_inside_strong() {
let (p, _offsets) = process_paragraph_with_brackets("( )", None);
assert_eq!(p.paragraph_level, 0);
assert_eq!(p.levels, vec![0, 0, 0, 0]);
}
#[test]
fn process_paragraph_classes_with_brackets_length_mismatch_panics() {
let result = std::panic::catch_unwind(|| {
let chars = vec!['a', 'b'];
let classes = vec![BidiClass::L];
let _ = process_paragraph_classes_with_brackets(&classes, &chars, None);
});
assert!(result.is_err(), "length mismatch must panic");
}
#[test]
fn l4_mirrored_glyph_covers_each_ascii_bracket_pair() {
for (a, b) in [('(', ')'), ('[', ']'), ('{', '}')] {
assert_eq!(mirrored_glyph(a), Some(b));
assert_eq!(mirrored_glyph(b), Some(a));
}
}
#[test]
fn l4_mirrored_glyph_is_an_involution() {
for c in ['(', ')', '[', ']', '{', '}'] {
let m = mirrored_glyph(c).expect("seed-set member must have a mirror");
assert_eq!(mirrored_glyph(m), Some(c), "mirror of mirror returns {c}");
}
}
#[test]
fn l4_mirrored_glyph_none_outside_seed_set() {
for c in ['a', 'Z', '0', ' ', ',', '.', '\u{05D0}', '\u{0627}'] {
assert_eq!(
mirrored_glyph(c),
None,
"{c:?} has no mirror in the seed set"
);
}
}
#[test]
fn l4_ornate_parentheses_not_mirrored_per_spec_note() {
assert_eq!(mirrored_glyph('\u{FD3E}'), None);
assert_eq!(mirrored_glyph('\u{FD3F}'), None);
}
#[test]
fn l4_mirrored_glyph_agrees_with_paired_bracket_on_seed_set() {
for c in ['(', ')', '[', ']', '{', '}'] {
let (paired, _kind) = paired_bracket(c).expect("seed-set member is a paired bracket");
assert_eq!(mirrored_glyph(c), Some(paired));
}
}
#[test]
fn l4_even_levels_leave_characters_unchanged() {
let mut chars = ['(', 'a', ')'];
apply_mirroring(&mut chars, &[0, 0, 0]);
assert_eq!(chars, ['(', 'a', ')']);
let mut chars2 = ['[', 'b', ']'];
apply_mirroring(&mut chars2, &[2, 2, 2]);
assert_eq!(chars2, ['[', 'b', ']']);
}
#[test]
fn l4_odd_levels_mirror_bracket_positions() {
let mut chars = ['(', 'a', ')'];
apply_mirroring(&mut chars, &[1, 1, 1]);
assert_eq!(chars, [')', 'a', '(']);
}
#[test]
fn l4_mixed_levels_mirror_only_odd_positions() {
let mut chars = ['(', '(', ')', ')'];
apply_mirroring(&mut chars, &[0, 1, 1, 0]);
assert_eq!(chars, ['(', ')', '(', ')']);
}
#[test]
fn l4_non_mirrored_characters_untouched_at_odd_levels() {
let mut chars = ['\u{05D0}', 'a', '7', '.'];
apply_mirroring(&mut chars, &[1, 1, 1, 1]);
assert_eq!(chars, ['\u{05D0}', 'a', '7', '.']);
}
#[test]
fn l4_empty_input_no_op() {
let mut chars: [char; 0] = [];
apply_mirroring(&mut chars, &[]);
assert!(chars.is_empty());
}
#[test]
fn l4_double_application_restores_original() {
let original = ['{', '\u{05D0}', '}'];
let mut chars = original;
let levels = [1, 1, 1];
apply_mirroring(&mut chars, &levels);
assert_eq!(chars, ['}', '\u{05D0}', '{']);
apply_mirroring(&mut chars, &levels);
assert_eq!(chars, original);
}
#[test]
fn l4_length_mismatch_panics() {
let result = std::panic::catch_unwind(|| {
let mut chars = ['(', ')'];
apply_mirroring(&mut chars, &[1]);
});
assert!(result.is_err(), "length mismatch must panic");
}
#[test]
fn l4_composes_with_bracket_driver_on_rtl_paragraph() {
let (p, _offsets) = process_paragraph_with_brackets("\u{05D0}(\u{05D1})\u{05D2}", None);
assert_eq!(p.paragraph_level, 1);
let mut chars: Vec<char> = "\u{05D0}(\u{05D1})\u{05D2}".chars().collect();
apply_mirroring(&mut chars, &p.levels);
assert_eq!(chars, vec!['\u{05D0}', ')', '\u{05D1}', '(', '\u{05D2}']);
}
}