use crate::unicode::{BidiClass, bidi_class};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Direction {
Neutral,
Left,
Right,
LeftWeak,
}
impl Direction {
#[must_use]
pub fn of(code: u32) -> Self {
match bidi_class(code) {
BidiClass::L => Self::Left,
BidiClass::An
| BidiClass::En
| BidiClass::Nsm
| BidiClass::Cs
| BidiClass::Es
| BidiClass::Et
| BidiClass::Bn => Self::LeftWeak,
BidiClass::R | BidiClass::Al => Self::Right,
BidiClass::On
| BidiClass::S
| BidiClass::Ws
| BidiClass::B
| BidiClass::Rlo
| BidiClass::Rle
| BidiClass::Lro
| BidiClass::Lre
| BidiClass::Pdf => Self::Neutral,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Segment {
pub start: usize,
pub count: usize,
pub direction: Direction,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BidiLine {
segments: Vec<Segment>,
overall: Direction,
}
impl BidiLine {
#[must_use]
pub fn segments(&self) -> &[Segment] {
&self.segments
}
#[must_use]
pub fn overall(&self) -> Direction {
self.overall
}
pub fn set_right(&mut self) {
if self.overall != Direction::Right {
self.segments.reverse();
self.overall = Direction::Right;
}
}
}
#[must_use]
pub fn segments(codes: &[u32], auto_order: bool) -> BidiLine {
let mut out: Vec<Segment> = Vec::new();
let mut current = Segment {
start: 0,
count: 0,
direction: Direction::Neutral,
};
let start_new = |current: &mut Segment, direction: Direction| -> Segment {
let completed = *current;
current.start += current.count;
current.count = 0;
current.direction = direction;
completed
};
for &code in codes {
let direction = Direction::of(code);
if direction != current.direction {
out.push(start_new(&mut current, direction));
}
current.count += 1;
}
let last = start_new(&mut current, Direction::Neutral);
if last.count > 0 {
out.push(last);
}
let mut line = BidiLine {
segments: out,
overall: Direction::Left,
};
if auto_order {
let count = |want: Direction| line.segments.iter().filter(|s| s.direction == want).count();
if count(Direction::Right) > count(Direction::Left) {
line.set_right();
}
}
line
}
#[must_use]
pub fn is_right_to_left(codes: &[u32]) -> bool {
segments(codes, true).overall() == Direction::Right
}
#[cfg(test)]
mod tests {
#![allow(
clippy::float_cmp,
clippy::indexing_slicing,
clippy::unreadable_literal,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::*;
fn codes(text: &str) -> Vec<u32> {
text.chars().map(u32::from).collect()
}
#[test]
fn characters_land_in_the_four_buckets() {
assert_eq!(Direction::of(u32::from('a')), Direction::Left);
assert_eq!(Direction::of(0x05D0), Direction::Right); assert_eq!(Direction::of(0x0627), Direction::Right); assert_eq!(Direction::of(u32::from('1')), Direction::LeftWeak); assert_eq!(Direction::of(0x0660), Direction::LeftWeak); assert_eq!(Direction::of(0x0300), Direction::LeftWeak); assert_eq!(Direction::of(u32::from(' ')), Direction::Neutral); assert_eq!(Direction::of(u32::from('(')), Direction::Neutral); assert_eq!(Direction::of(0x202B), Direction::Neutral); }
#[test]
fn a_leading_zero_count_neutral_segment_is_emitted() {
let line = segments(&codes("abc"), false);
assert_eq!(line.segments()[0].count, 0);
assert_eq!(line.segments()[0].direction, Direction::Neutral);
let line = segments(&codes(" abc"), false);
assert_eq!(line.segments().len(), 2);
assert_eq!(line.segments()[0].count, 1);
assert_eq!(line.segments()[0].direction, Direction::Neutral);
assert_eq!(line.segments()[1].count, 3);
}
#[test]
fn segment_starts_and_counts_tile_the_input() {
let text = codes("ab \u{05D0}\u{05D1}1x");
let line = segments(&text, false);
let mut at = 0;
for segment in line.segments() {
assert_eq!(segment.start, at, "{segment:?}");
at += segment.count;
}
assert_eq!(at, text.len());
}
#[test]
fn an_empty_line_produces_no_segments() {
let line = segments(&[], false);
assert!(line.segments().is_empty());
assert_eq!(line.overall(), Direction::Left);
}
#[test]
fn auto_order_flips_only_on_a_strict_majority() {
let text = codes("\u{05D0} a \u{05D1}");
assert!(is_right_to_left(&text));
let text = codes("\u{05D0} a");
assert!(!is_right_to_left(&text));
assert!(!is_right_to_left(&codes("hello")));
assert!(!is_right_to_left(&codes("123 ... 456")));
}
#[test]
fn flipping_reverses_the_segment_order_and_is_idempotent() {
let mut line = segments(&codes("ab\u{05D0}"), false);
let before: Vec<Segment> = line.segments().to_vec();
line.set_right();
let reversed: Vec<Segment> = line.segments().to_vec();
assert_eq!(reversed, before.iter().rev().copied().collect::<Vec<_>>());
line.set_right();
assert_eq!(line.segments(), reversed.as_slice());
assert_eq!(line.overall(), Direction::Right);
}
#[test]
fn segmentation_splits_wherever_the_bucket_changes() {
let line = segments(&codes("a \u{05D0}1"), false);
let kinds: Vec<Direction> = line.segments().iter().map(|s| s.direction).collect();
assert_eq!(
kinds,
[
Direction::Neutral,
Direction::Left,
Direction::Neutral,
Direction::Right,
Direction::LeftWeak,
]
);
}
#[test]
fn a_latin_line_resolves_left_with_a_leading_neutral_segment() {
let latin: Vec<u32> = "abc".chars().map(u32::from).collect();
let line = segments(&latin, false);
assert_eq!(line.overall(), Direction::Left);
assert_eq!(line.segments().len(), 2);
assert_eq!(line.segments()[0].count, 0);
assert_eq!(line.segments()[1].count, 3);
}
}