use crate::charinfo::{CharBox, CharType};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct CharIndex(usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct TextIndex(usize);
macro_rules! index_newtype {
($name:ident, $what:literal) => {
impl $name {
#[doc = concat!("The ", $what, " at this position.")]
#[must_use]
pub const fn new(index: usize) -> Self {
Self(index)
}
#[must_use]
pub const fn get(self) -> usize {
self.0
}
}
impl From<usize> for $name {
fn from(index: usize) -> Self {
Self(index)
}
}
impl From<$name> for usize {
fn from(index: $name) -> Self {
index.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
};
}
index_newtype!(CharIndex, "character-list position");
index_newtype!(TextIndex, "text position");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CharSegment {
pub index: u32,
pub count: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IndexMap {
segments: Vec<CharSegment>,
}
#[must_use]
pub(crate) fn build(chars: &[CharBox]) -> IndexMap {
let mut segments: Vec<CharSegment> = Vec::new();
if !chars.is_empty() {
segments.push(CharSegment { index: 0, count: 0 });
}
let mut started = false;
for (position, info) in chars.iter().enumerate() {
let counts = info.char_type == CharType::Generated || info.is_normal();
let next = u32::try_from(position.saturating_add(1)).unwrap_or(u32::MAX);
if counts {
if let Some(last) = segments.last_mut() {
last.count = last.count.saturating_add(1);
}
started = true;
} else if started {
segments.push(CharSegment {
index: next,
count: 0,
});
started = false;
} else if let Some(last) = segments.last_mut() {
last.index = next;
}
}
IndexMap { segments }
}
impl IndexMap {
#[must_use]
pub fn segments(&self) -> &[CharSegment] {
&self.segments
}
#[must_use]
pub fn text_len(&self) -> usize {
self.segments
.iter()
.map(|segment| segment.count as usize)
.sum()
}
#[must_use]
pub fn char_index(&self, text_index: TextIndex) -> Option<CharIndex> {
let mut remaining = text_index.get();
for segment in &self.segments {
let count = segment.count as usize;
if remaining < count {
return Some(CharIndex::new(segment.index as usize + remaining));
}
remaining -= count;
}
None
}
#[must_use]
pub fn text_index(&self, char_index: CharIndex) -> Option<TextIndex> {
let char_index = char_index.get();
let mut before = 0usize;
for segment in &self.segments {
let start = segment.index as usize;
let count = segment.count as usize;
if char_index < start {
return None;
}
if char_index < start + count {
return Some(TextIndex::new(before + (char_index - start)));
}
before += count;
}
None
}
#[must_use]
pub fn text_index_at_or_after(&self, char_index: CharIndex) -> Option<TextIndex> {
let char_index = char_index.get();
let mut before = 0usize;
for segment in &self.segments {
let start = segment.index as usize;
let count = segment.count as usize;
if count == 0 {
continue;
}
if char_index < start {
return Some(TextIndex::new(before));
}
if char_index < start + count {
return Some(TextIndex::new(before + (char_index - start)));
}
before += count;
}
None
}
#[must_use]
pub fn text_index_end(&self, char_index: CharIndex) -> TextIndex {
let char_index = char_index.get();
let mut before = 0usize;
let mut end = 0usize;
for segment in &self.segments {
let start = segment.index as usize;
let count = segment.count as usize;
if count == 0 {
continue;
}
if char_index < start {
break;
}
end = if char_index < start + count {
before + (char_index - start) + 1
} else {
before + count
};
before += count;
}
TextIndex::new(end)
}
}
#[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};
use pdfrum_font::CharCode;
fn info(char_type: CharType, unicode: u32, code: Option<u32>) -> CharBox {
CharBox {
char_type,
unicode,
code: code.map(CharCode),
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 normal(ch: char) -> CharBox {
info(CharType::Normal, u32::from(ch), Some(u32::from(ch)))
}
#[test]
fn a_run_with_nothing_stripped_is_one_segment() {
let chars: Vec<CharBox> = "hello".chars().map(normal).collect();
let index = build(&chars);
assert_eq!(index.segments(), [CharSegment { index: 0, count: 5 }]);
assert_eq!(index.text_len(), 5);
for at in 0..5 {
assert_eq!(
index.char_index(TextIndex::new(at)),
Some(CharIndex::new(at))
);
assert_eq!(
index.text_index(CharIndex::new(at)),
Some(TextIndex::new(at))
);
}
assert_eq!(index.char_index(TextIndex::new(5)), None);
}
#[test]
fn an_empty_page_has_no_segments() {
let index = build(&[]);
assert!(index.segments().is_empty());
assert_eq!(index.text_len(), 0);
assert_eq!(index.char_index(TextIndex::new(0)), None);
assert_eq!(index.text_index(CharIndex::new(0)), None);
}
#[test]
fn a_stripped_character_splits_the_segments_and_keeps_the_char_index() {
let mut chars: Vec<CharBox> = "Hello".chars().map(normal).collect();
chars.push(info(CharType::Normal, 0x02, Some(2)));
chars.push(info(CharType::Normal, 0x03, Some(3)));
chars.extend("world".chars().map(normal));
let index = build(&chars);
assert_eq!(index.text_len(), 10);
assert_eq!(index.char_index(TextIndex::new(5)), Some(CharIndex::new(7)));
assert_eq!(index.text_index(CharIndex::new(7)), Some(TextIndex::new(5)));
assert_eq!(index.text_index(CharIndex::new(5)), None);
assert_eq!(index.text_index(CharIndex::new(6)), None);
}
#[test]
fn leading_stripped_characters_slide_the_first_segment_forward() {
let mut chars = vec![info(CharType::Normal, 0x02, Some(2))];
chars.extend("ab".chars().map(normal));
let index = build(&chars);
assert_eq!(index.segments(), [CharSegment { index: 1, count: 2 }]);
assert_eq!(index.char_index(TextIndex::new(0)), Some(CharIndex::new(1)));
}
#[test]
fn a_generated_character_always_counts() {
let mut chars: Vec<CharBox> = "ab".chars().map(normal).collect();
chars.push(info(CharType::Generated, u32::from('\r'), None));
chars.push(info(CharType::Generated, u32::from('\n'), None));
chars.extend("cd".chars().map(normal));
let index = build(&chars);
assert_eq!(index.segments(), [CharSegment { index: 0, count: 6 }]);
}
#[test]
fn the_charcode_zero_placeholder_is_stripped() {
let mut chars: Vec<CharBox> =
std::iter::repeat_n(info(CharType::Normal, 0, Some(0)), 22).collect();
chars.extend("hello".chars().map(normal));
let index = build(&chars);
assert_eq!(index.text_len(), 5);
assert_eq!(
index.char_index(TextIndex::new(0)),
Some(CharIndex::new(22))
);
assert_eq!(
index.text_index(CharIndex::new(22)),
Some(TextIndex::new(0))
);
}
#[test]
fn the_forward_and_backward_bounds_skip_stripped_characters() {
let mut chars: Vec<CharBox> = "ab".chars().map(normal).collect();
chars.push(info(CharType::Normal, 0x02, Some(2)));
chars.extend("cd".chars().map(normal));
let index = build(&chars);
assert_eq!(
index.text_index_at_or_after(CharIndex::new(2)),
Some(TextIndex::new(2))
);
assert_eq!(index.text_index_end(CharIndex::new(2)), TextIndex::new(2));
assert_eq!(index.text_index_end(CharIndex::new(3)), TextIndex::new(3));
assert_eq!(index.text_index_end(CharIndex::new(99)), TextIndex::new(4));
}
#[test]
fn the_two_index_spaces_are_different_types() {
assert_eq!(CharIndex::new(7).get(), 7);
assert_eq!(TextIndex::new(7).get(), 7);
assert_eq!(CharIndex::from(3usize), CharIndex::new(3));
assert_eq!(TextIndex::from(3usize), TextIndex::new(3));
assert_eq!(usize::from(CharIndex::new(3)), 3);
assert_eq!(usize::from(TextIndex::new(3)), 3);
assert_eq!(CharIndex::new(41).to_string(), "41");
assert_eq!(TextIndex::new(41).to_string(), "41");
assert!(CharIndex::new(1) < CharIndex::new(2));
assert!(TextIndex::new(1) < TextIndex::new(2));
assert_eq!(CharIndex::default(), CharIndex::new(0));
assert_eq!(TextIndex::default(), TextIndex::new(0));
}
#[test]
fn the_conversions_round_trip_over_a_known_segment_table() {
let mut chars: Vec<CharBox> = "Hello".chars().map(normal).collect();
chars.push(info(CharType::Normal, 0x02, Some(2)));
chars.push(info(CharType::Normal, 0x03, Some(3)));
chars.extend("world".chars().map(normal));
let map = build(&chars);
for at in 0..map.text_len() {
let text = TextIndex::new(at);
let ch = map.char_index(text).expect("every text offset has a char");
assert_eq!(map.text_index(ch), Some(text), "round trip at {text}");
assert_eq!(map.text_index_at_or_after(ch), Some(text));
assert_eq!(map.text_index_end(ch), TextIndex::new(at + 1));
}
assert_eq!(map.char_index(TextIndex::new(map.text_len())), None);
let stripped: Vec<usize> = (0..chars.len())
.filter(|at| map.text_index(CharIndex::new(*at)).is_none())
.collect();
assert_eq!(stripped, [5, 6]);
assert_eq!(
map.text_index_at_or_after(CharIndex::new(5)),
Some(TextIndex::new(5))
);
assert_eq!(
map.text_index_at_or_after(CharIndex::new(6)),
Some(TextIndex::new(5))
);
assert_eq!(map.text_index_end(CharIndex::new(5)), TextIndex::new(5));
assert_eq!(map.text_index_end(CharIndex::new(6)), TextIndex::new(5));
}
}