use crate::bidi::{self, Direction};
use crate::charinfo::{CharBox, CharType};
use crate::unicode::{mirror_char, normalize, normalize_space};
#[derive(Debug, Clone, Default)]
pub struct Line {
text: Vec<u32>,
chars: Vec<CharBox>,
}
impl Line {
#[must_use]
pub fn len(&self) -> usize {
self.chars.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.chars.is_empty()
}
#[must_use]
pub fn text(&self) -> &[u32] {
&self.text
}
#[must_use]
pub fn last_unit(&self) -> Option<u32> {
self.text.last().copied()
}
#[must_use]
pub fn last_char(&self) -> Option<&CharBox> {
self.chars.last()
}
pub fn last_char_mut(&mut self) -> Option<&mut CharBox> {
self.chars.last_mut()
}
pub fn push(&mut self, unit: u32, info: CharBox) {
self.text.push(unit);
self.chars.push(info);
}
pub fn pop(&mut self) {
self.text.pop();
self.chars.pop();
}
pub fn set_last_unit(&mut self, unit: u32) {
if let Some(last) = self.text.last_mut() {
*last = unit;
}
}
pub fn reverse_from(&mut self, index: usize) {
if let Some(tail) = self.text.get_mut(index..) {
tail.reverse();
}
if let Some(tail) = self.chars.get_mut(index..) {
tail.reverse();
}
}
pub fn collapse_spaces(&mut self) {
let mut previous_was_space = false;
let mut index = 0;
while index < self.text.len() {
let is_space = self.text.get(index) == Some(&u32::from(b' '));
if !is_space {
previous_was_space = false;
index += 1;
continue;
}
if previous_was_space {
self.text.remove(index);
self.chars.remove(index);
continue;
}
previous_was_space = true;
index += 1;
}
}
pub fn take(&mut self) -> (Vec<u32>, Vec<CharBox>) {
(
std::mem::take(&mut self.text),
std::mem::take(&mut self.chars),
)
}
}
#[derive(Debug, Default)]
pub struct Output {
pub chars: Vec<CharBox>,
pub text: Vec<u32>,
}
pub fn close(line: &mut Line, out: &mut Output, rtl: bool) {
if line.is_empty() {
return;
}
line.collapse_spaces();
let (text, chars) = line.take();
let mut segmented = bidi::segments(&text, false);
if rtl {
segmented.set_right();
}
let mut current = segmented.overall();
for segment in segmented.segments() {
let range = segment.start..segment.start.saturating_add(segment.count);
let (Some(units), Some(infos)) = (text.get(range.clone()), chars.get(range)) else {
continue;
};
let is_right = segment.direction == Direction::Right
|| (segment.direction == Direction::Neutral && current == Direction::Right);
if is_right {
current = Direction::Right;
let actual_text = infos
.first()
.is_some_and(|info| info.char_type == CharType::ActualText);
if actual_text {
for (unit, info) in units.iter().zip(infos) {
add(*unit, *info, true, out);
}
} else {
for (unit, info) in units.iter().zip(infos).rev() {
add(*unit, *info, true, out);
}
}
} else {
if segment.direction != Direction::LeftWeak {
current = Direction::Left;
}
for (unit, info) in units.iter().zip(infos) {
add(*unit, *info, false, out);
}
}
}
}
fn add(unit: u32, info: CharBox, is_rtl: bool, out: &mut Output) {
if !info.is_normal() {
out.chars.push(info);
return;
}
let unit = if is_rtl { mirror_char(unit) } else { unit };
let normalized_unit = normalize_space(unit);
let space_normalized = normalized_unit != unit;
let unit = normalized_unit;
let normalized = if is_rtl || (0xFB00..=0xFB06).contains(&unit) {
normalize(unit)
} else {
Vec::new()
};
let mut modified = info;
if normalized.is_empty() {
out.text.push(unit);
if is_rtl || space_normalized {
modified.unicode = unit;
}
out.chars.push(modified);
return;
}
modified.char_type = CharType::Piece;
for piece in normalized {
modified.unicode = piece;
out.text.push(piece);
out.chars.push(modified);
}
}
#[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::*;
use kurbo::{Affine, Point, Rect};
fn info(unicode: u32) -> CharBox {
CharBox {
char_type: CharType::Normal,
unicode,
code: Some(pdfrum_font::CharCode(unicode)),
origin: Point::ZERO,
char_box: Rect::ZERO,
loose_char_box: Rect::ZERO,
matrix: Affine::IDENTITY,
object: None,
font_size: 1.0,
angle: 0.0,
}
}
fn staged(text: &str) -> Line {
let mut line = Line::default();
for ch in text.chars() {
line.push(u32::from(ch), info(u32::from(ch)));
}
line
}
fn rendered(out: &Output) -> String {
out.text.iter().filter_map(|u| char::from_u32(*u)).collect()
}
fn char_units(out: &Output) -> Vec<u32> {
out.chars.iter().map(|c| c.unicode).collect()
}
#[test]
fn space_runs_collapse_to_their_first() {
let mut line = staged("a b c");
line.collapse_spaces();
let (text, chars) = line.take();
assert_eq!(
text.iter()
.filter_map(|u| char::from_u32(*u))
.collect::<String>(),
"a b c"
);
assert_eq!(chars.len(), text.len());
}
#[test]
fn a_leading_or_trailing_space_run_collapses_too() {
let mut line = staged(" a ");
line.collapse_spaces();
let (text, _) = line.take();
assert_eq!(
text.iter()
.filter_map(|u| char::from_u32(*u))
.collect::<String>(),
" a "
);
}
#[test]
fn a_space_run_split_across_a_line_boundary_is_not_collapsed() {
let mut out = Output::default();
let mut line = staged("a ");
close(&mut line, &mut out, false);
let mut line = staged(" b");
close(&mut line, &mut out, false);
assert_eq!(rendered(&out), "a b");
}
#[test]
fn reversing_from_an_index_moves_both_buffers_together() {
let mut line = staged("abcde");
line.reverse_from(2);
let (text, chars) = line.take();
let as_string: String = text.iter().filter_map(|u| char::from_u32(*u)).collect();
assert_eq!(as_string, "abedc");
let from_chars: String = chars
.iter()
.filter_map(|c| char::from_u32(c.unicode))
.collect();
assert_eq!(from_chars, as_string);
let mut line = staged("ab");
line.reverse_from(9);
assert_eq!(line.len(), 2);
}
#[test]
fn a_left_to_right_line_passes_straight_through() {
let mut out = Output::default();
close(&mut staged("hello"), &mut out, false);
assert_eq!(rendered(&out), "hello");
assert_eq!(out.chars.len(), 5);
}
#[test]
fn a_right_to_left_segment_comes_out_in_logical_order() {
let mut out = Output::default();
close(&mut staged("\u{05D0}\u{05D1}\u{05D2}"), &mut out, false);
assert_eq!(rendered(&out), "\u{05D2}\u{05D1}\u{05D0}");
}
#[test]
fn a_right_to_left_segment_mirrors_its_brackets() {
let mut out = Output::default();
close(&mut staged("\u{05D0}("), &mut out, false);
assert_eq!(rendered(&out), "\u{05D0})");
}
#[test]
fn a_control_character_reaches_the_char_list_but_not_the_text() {
let mut line = Line::default();
line.push(u32::from('a'), info(u32::from('a')));
line.push(0x03, info(0x03));
line.push(u32::from('b'), info(u32::from('b')));
let mut out = Output::default();
close(&mut line, &mut out, false);
assert_eq!(rendered(&out), "ab");
assert_eq!(char_units(&out), [u32::from('a'), 0x03, u32::from('b')]);
}
#[test]
fn the_hyphen_splits_the_two_outputs() {
let mut line = Line::default();
line.push(u32::from('a'), info(u32::from('a')));
let mut hyphen = info(0x02);
hyphen.char_type = CharType::Hyphen;
line.push(0x00AD, hyphen);
line.push(u32::from('s'), info(u32::from('s')));
let mut out = Output::default();
close(&mut line, &mut out, false);
assert_eq!(out.text, [u32::from('a'), 0x00AD, u32::from('s')]);
assert_eq!(char_units(&out), [u32::from('a'), 0x02, u32::from('s')]);
assert!(char::from_u32(0x00AD).is_some());
}
#[test]
fn a_latin_ligature_normalizes_even_left_to_right() {
let mut out = Output::default();
close(&mut staged("a\u{FB01}b"), &mut out, false);
assert_eq!(rendered(&out), "afib");
assert_eq!(out.chars.len(), 4);
assert_eq!(out.chars[1].char_type, CharType::Piece);
assert_eq!(out.chars[2].char_type, CharType::Piece);
}
#[test]
fn a_no_break_space_normalizes_in_either_direction() {
let mut out = Output::default();
close(&mut staged("a\u{00A0}b"), &mut out, false);
assert_eq!(rendered(&out), "a b");
assert_eq!(out.chars[1].char_type, CharType::Normal);
assert_eq!(out.chars[1].unicode, 0x0020);
}
#[test]
fn an_accented_letter_is_not_normalized_left_to_right() {
let mut out = Output::default();
close(&mut staged("a\u{00C0}b"), &mut out, false);
assert_eq!(rendered(&out), "a\u{00C0}b");
assert_eq!(out.chars[1].unicode, 0x00C0);
}
#[test]
fn the_r2l_preference_flips_a_whole_line() {
let mut out = Output::default();
close(&mut staged("ab"), &mut out, true);
assert_eq!(rendered(&out), "ab");
}
#[test]
fn an_empty_line_closes_to_nothing() {
let mut out = Output::default();
close(&mut Line::default(), &mut out, false);
assert!(out.chars.is_empty() && out.text.is_empty());
}
}