1use crate::charinfo::{CharBox, CharType};
21use std::fmt;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
33pub struct CharIndex(usize);
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
42pub struct TextIndex(usize);
43
44macro_rules! index_newtype {
45 ($name:ident, $what:literal) => {
46 impl $name {
47 #[doc = concat!("The ", $what, " at this position.")]
48 #[must_use]
49 pub const fn new(index: usize) -> Self {
50 Self(index)
51 }
52
53 #[must_use]
55 pub const fn get(self) -> usize {
56 self.0
57 }
58 }
59
60 impl From<usize> for $name {
61 fn from(index: usize) -> Self {
62 Self(index)
63 }
64 }
65
66 impl From<$name> for usize {
67 fn from(index: $name) -> Self {
68 index.0
69 }
70 }
71
72 impl fmt::Display for $name {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 self.0.fmt(f)
75 }
76 }
77 };
78}
79
80index_newtype!(CharIndex, "character-list position");
81index_newtype!(TextIndex, "text position");
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct CharSegment {
86 pub index: u32,
88 pub count: u32,
90}
91
92#[derive(Debug, Clone, Default, PartialEq, Eq)]
99pub struct IndexMap {
100 segments: Vec<CharSegment>,
101}
102
103#[must_use]
111pub(crate) fn build(chars: &[CharBox]) -> IndexMap {
112 let mut segments: Vec<CharSegment> = Vec::new();
113 if !chars.is_empty() {
114 segments.push(CharSegment { index: 0, count: 0 });
115 }
116 let mut started = false;
118 for (position, info) in chars.iter().enumerate() {
119 let counts = info.char_type == CharType::Generated || info.is_normal();
120 let next = u32::try_from(position.saturating_add(1)).unwrap_or(u32::MAX);
121 if counts {
122 if let Some(last) = segments.last_mut() {
123 last.count = last.count.saturating_add(1);
124 }
125 started = true;
126 } else if started {
127 segments.push(CharSegment {
128 index: next,
129 count: 0,
130 });
131 started = false;
132 } else if let Some(last) = segments.last_mut() {
133 last.index = next;
134 }
135 }
136 IndexMap { segments }
137}
138
139impl IndexMap {
140 #[must_use]
142 pub fn segments(&self) -> &[CharSegment] {
143 &self.segments
144 }
145
146 #[must_use]
148 pub fn text_len(&self) -> usize {
149 self.segments
150 .iter()
151 .map(|segment| segment.count as usize)
152 .sum()
153 }
154
155 #[must_use]
163 pub fn char_index(&self, text_index: TextIndex) -> Option<CharIndex> {
164 let mut remaining = text_index.get();
165 for segment in &self.segments {
166 let count = segment.count as usize;
167 if remaining < count {
168 return Some(CharIndex::new(segment.index as usize + remaining));
169 }
170 remaining -= count;
171 }
172 None
173 }
174
175 #[must_use]
178 pub fn text_index(&self, char_index: CharIndex) -> Option<TextIndex> {
179 let char_index = char_index.get();
180 let mut before = 0usize;
181 for segment in &self.segments {
182 let start = segment.index as usize;
183 let count = segment.count as usize;
184 if char_index < start {
185 return None;
186 }
187 if char_index < start + count {
188 return Some(TextIndex::new(before + (char_index - start)));
189 }
190 before += count;
191 }
192 None
193 }
194
195 #[must_use]
201 pub fn text_index_at_or_after(&self, char_index: CharIndex) -> Option<TextIndex> {
202 let char_index = char_index.get();
203 let mut before = 0usize;
204 for segment in &self.segments {
205 let start = segment.index as usize;
206 let count = segment.count as usize;
207 if count == 0 {
208 continue;
209 }
210 if char_index < start {
211 return Some(TextIndex::new(before));
212 }
213 if char_index < start + count {
214 return Some(TextIndex::new(before + (char_index - start)));
215 }
216 before += count;
217 }
218 None
219 }
220
221 #[must_use]
224 pub fn text_index_end(&self, char_index: CharIndex) -> TextIndex {
225 let char_index = char_index.get();
226 let mut before = 0usize;
227 let mut end = 0usize;
228 for segment in &self.segments {
229 let start = segment.index as usize;
230 let count = segment.count as usize;
231 if count == 0 {
232 continue;
233 }
234 if char_index < start {
235 break;
236 }
237 end = if char_index < start + count {
238 before + (char_index - start) + 1
239 } else {
240 before + count
241 };
242 before += count;
243 }
244 TextIndex::new(end)
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 #![allow(
254 clippy::float_cmp,
255 clippy::indexing_slicing,
256 clippy::unreadable_literal,
257 clippy::cast_precision_loss,
258 clippy::cast_possible_truncation,
259 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
260 )]
261
262 use super::*;
263 use kurbo::{Affine, Point, Rect};
264 use pdfrum_font::CharCode;
265
266 fn info(char_type: CharType, unicode: u32, code: Option<u32>) -> CharBox {
267 CharBox {
268 char_type,
269 unicode,
270 code: code.map(CharCode),
271 origin: Point::ZERO,
272 char_box: Rect::ZERO,
273 loose_char_box: Rect::ZERO,
274 matrix: Affine::IDENTITY,
275 object: None,
276 font_size: 1.0,
277 angle: 0.0,
278 }
279 }
280
281 fn normal(ch: char) -> CharBox {
282 info(CharType::Normal, u32::from(ch), Some(u32::from(ch)))
283 }
284
285 #[test]
286 fn a_run_with_nothing_stripped_is_one_segment() {
287 let chars: Vec<CharBox> = "hello".chars().map(normal).collect();
288 let index = build(&chars);
289 assert_eq!(index.segments(), [CharSegment { index: 0, count: 5 }]);
290 assert_eq!(index.text_len(), 5);
291 for at in 0..5 {
292 assert_eq!(
293 index.char_index(TextIndex::new(at)),
294 Some(CharIndex::new(at))
295 );
296 assert_eq!(
297 index.text_index(CharIndex::new(at)),
298 Some(TextIndex::new(at))
299 );
300 }
301 assert_eq!(index.char_index(TextIndex::new(5)), None);
302 }
303
304 #[test]
305 fn an_empty_page_has_no_segments() {
306 let index = build(&[]);
307 assert!(index.segments().is_empty());
308 assert_eq!(index.text_len(), 0);
309 assert_eq!(index.char_index(TextIndex::new(0)), None);
310 assert_eq!(index.text_index(CharIndex::new(0)), None);
311 }
312
313 #[test]
314 fn a_stripped_character_splits_the_segments_and_keeps_the_char_index() {
315 let mut chars: Vec<CharBox> = "Hello".chars().map(normal).collect();
319 chars.push(info(CharType::Normal, 0x02, Some(2)));
320 chars.push(info(CharType::Normal, 0x03, Some(3)));
321 chars.extend("world".chars().map(normal));
322 let index = build(&chars);
323 assert_eq!(index.text_len(), 10);
324 assert_eq!(index.char_index(TextIndex::new(5)), Some(CharIndex::new(7)));
326 assert_eq!(index.text_index(CharIndex::new(7)), Some(TextIndex::new(5)));
327 assert_eq!(index.text_index(CharIndex::new(5)), None);
329 assert_eq!(index.text_index(CharIndex::new(6)), None);
330 }
331
332 #[test]
333 fn leading_stripped_characters_slide_the_first_segment_forward() {
334 let mut chars = vec![info(CharType::Normal, 0x02, Some(2))];
337 chars.extend("ab".chars().map(normal));
338 let index = build(&chars);
339 assert_eq!(index.segments(), [CharSegment { index: 1, count: 2 }]);
340 assert_eq!(index.char_index(TextIndex::new(0)), Some(CharIndex::new(1)));
341 }
342
343 #[test]
344 fn a_generated_character_always_counts() {
345 let mut chars: Vec<CharBox> = "ab".chars().map(normal).collect();
348 chars.push(info(CharType::Generated, u32::from('\r'), None));
349 chars.push(info(CharType::Generated, u32::from('\n'), None));
350 chars.extend("cd".chars().map(normal));
351 let index = build(&chars);
352 assert_eq!(index.segments(), [CharSegment { index: 0, count: 6 }]);
353 }
354
355 #[test]
356 fn the_charcode_zero_placeholder_is_stripped() {
357 let mut chars: Vec<CharBox> =
360 std::iter::repeat_n(info(CharType::Normal, 0, Some(0)), 22).collect();
361 chars.extend("hello".chars().map(normal));
362 let index = build(&chars);
363 assert_eq!(index.text_len(), 5);
364 assert_eq!(
365 index.char_index(TextIndex::new(0)),
366 Some(CharIndex::new(22))
367 );
368 assert_eq!(
369 index.text_index(CharIndex::new(22)),
370 Some(TextIndex::new(0))
371 );
372 }
373
374 #[test]
375 fn the_forward_and_backward_bounds_skip_stripped_characters() {
376 let mut chars: Vec<CharBox> = "ab".chars().map(normal).collect();
377 chars.push(info(CharType::Normal, 0x02, Some(2)));
378 chars.extend("cd".chars().map(normal));
379 let index = build(&chars);
380 assert_eq!(
382 index.text_index_at_or_after(CharIndex::new(2)),
383 Some(TextIndex::new(2))
384 );
385 assert_eq!(index.text_index_end(CharIndex::new(2)), TextIndex::new(2));
387 assert_eq!(index.text_index_end(CharIndex::new(3)), TextIndex::new(3));
388 assert_eq!(index.text_index_end(CharIndex::new(99)), TextIndex::new(4));
389 }
390
391 #[test]
392 fn the_two_index_spaces_are_different_types() {
393 assert_eq!(CharIndex::new(7).get(), 7);
399 assert_eq!(TextIndex::new(7).get(), 7);
400 assert_eq!(CharIndex::from(3usize), CharIndex::new(3));
401 assert_eq!(TextIndex::from(3usize), TextIndex::new(3));
402 assert_eq!(usize::from(CharIndex::new(3)), 3);
403 assert_eq!(usize::from(TextIndex::new(3)), 3);
404 assert_eq!(CharIndex::new(41).to_string(), "41");
406 assert_eq!(TextIndex::new(41).to_string(), "41");
407 assert!(CharIndex::new(1) < CharIndex::new(2));
409 assert!(TextIndex::new(1) < TextIndex::new(2));
410 assert_eq!(CharIndex::default(), CharIndex::new(0));
412 assert_eq!(TextIndex::default(), TextIndex::new(0));
413 }
414
415 #[test]
416 fn the_conversions_round_trip_over_a_known_segment_table() {
417 let mut chars: Vec<CharBox> = "Hello".chars().map(normal).collect();
421 chars.push(info(CharType::Normal, 0x02, Some(2)));
422 chars.push(info(CharType::Normal, 0x03, Some(3)));
423 chars.extend("world".chars().map(normal));
424 let map = build(&chars);
425
426 for at in 0..map.text_len() {
427 let text = TextIndex::new(at);
428 let ch = map.char_index(text).expect("every text offset has a char");
429 assert_eq!(map.text_index(ch), Some(text), "round trip at {text}");
430 assert_eq!(map.text_index_at_or_after(ch), Some(text));
433 assert_eq!(map.text_index_end(ch), TextIndex::new(at + 1));
435 }
436 assert_eq!(map.char_index(TextIndex::new(map.text_len())), None);
438
439 let stripped: Vec<usize> = (0..chars.len())
443 .filter(|at| map.text_index(CharIndex::new(*at)).is_none())
444 .collect();
445 assert_eq!(stripped, [5, 6]);
446 assert_eq!(
447 map.text_index_at_or_after(CharIndex::new(5)),
448 Some(TextIndex::new(5))
449 );
450 assert_eq!(
451 map.text_index_at_or_after(CharIndex::new(6)),
452 Some(TextIndex::new(5))
453 );
454 assert_eq!(map.text_index_end(CharIndex::new(5)), TextIndex::new(5));
456 assert_eq!(map.text_index_end(CharIndex::new(6)), TextIndex::new(5));
457 }
458}