Files
pdfcraft/crates/render/tests/search.rs
2026-10-15 02:21:53 -04:00

113 lines
4.5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use pdfcraft_render::{PageText, TextGlyph, text::fold_case};
use std::ops::Range;
fn page(parts: &[&str]) -> PageText {
PageText {
glyphs: parts.iter().map(|text| TextGlyph { text: (*text).into(), rect: [0.0; 4], quad: [[0.0; 2]; 4], direction: [1.0, 0.0] }).collect(),
line_of: vec![0; parts.len()],
space_before: vec![false; parts.len()],
}
}
// The original straightforward search is an independent oracle for valid page metadata.
fn reference(page: &PageText, query: &str, sensitive: bool, whole: bool, limit: usize) -> Vec<Range<usize>> {
let fold = |s: &str| if sensitive { s.to_owned() } else { fold_case(s) };
let needle: Vec<char> = fold(query).split_whitespace().collect::<Vec<_>>().join(" ").chars().collect();
if needle.is_empty() || limit == 0 {
return Vec::new();
}
let mut chars = Vec::new();
for (i, glyph) in page.glyphs.iter().enumerate() {
if i > 0 && (page.space_before[i] || page.line_of[i] != page.line_of[i - 1]) && chars.last().is_some_and(|&(c, _)| c != ' ') {
chars.push((' ', i));
}
for c in fold(&glyph.text).chars() {
let c = if c.is_whitespace() { ' ' } else { c };
if c != ' ' || chars.last().is_none_or(|&(last, _)| last != ' ') {
chars.push((c, i));
}
}
}
let word = |i: Option<usize>| i.and_then(|i| chars.get(i)).is_some_and(|&(c, _)| c.is_alphanumeric());
let mut hits = Vec::new();
let mut start = 0;
while start + needle.len() <= chars.len() {
let end = start + needle.len();
if (!whole || (!word(start.checked_sub(1)) && !word(Some(end)))) && chars[start..end].iter().map(|&(c, _)| c).eq(needle.iter().copied()) {
hits.push(chars[start].1..chars[end - 1].1 + 1);
if hits.len() == limit {
break;
}
start = end;
} else {
start += 1;
}
}
hits
}
#[test]
fn search_tolerates_missing_layout_metadata() {
let mut text = page(&["a", "b", "c"]);
text.line_of.clear();
text.space_before.clear();
assert_eq!(text.find("abc"), vec![0..3]);
text.space_before = vec![false, true];
assert_eq!(text.find("a bc"), vec![0..3]);
text.line_of = vec![0, 1];
assert_eq!(text.find("a bc"), vec![0..3]);
}
#[test]
fn whole_word_rejection_keeps_overlapping_candidates() {
let text = page(&["xaa-aa-aa"]);
assert_eq!(text.find_opts("aa-aa", true, true), vec![0..1]);
// Accepted matches still do not overlap, even within one multi-character glyph.
assert_eq!(page(&["aaaaa"]).find("aaa"), vec![0..1]);
assert_eq!(page(&["aaaaa"]).find("aa"), vec![0..1, 0..1]);
}
#[test]
fn search_matches_reference_for_unicode_gaps_limits_and_word_boundaries() {
let alphabet = ["a", "b", " ", "-", "İ", "ı", "ς", "Σ", "\t", "\n", "ffi", "", "界", "e\u{301}", "العربية"];
let mut seed = 1234567u64;
let mut next = || {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
(seed >> 32) as usize
};
for case in 0..2000 {
let parts: Vec<_> = (0..10 + case % 70).map(|_| alphabet[next() % alphabet.len()]).collect();
let query = if case % 2 == 0 {
parts.iter().skip(case % 8).take(1 + case % 9).copied().collect::<String>()
} else {
(0..1 + case % 9).map(|_| alphabet[next() % alphabet.len()]).collect::<String>()
};
let mut text = page(&parts);
for i in 1..parts.len() {
text.space_before[i] = next() % 11 == 0;
text.line_of[i] = text.line_of[i - 1] + u32::from(next() % 17 == 0);
}
for sensitive in [false, true] {
for whole in [false, true] {
for limit in [0, 1, 2, usize::MAX] {
assert_eq!(
text.find_opts_limited(&query, sensitive, whole, limit),
reference(&text, &query, sensitive, whole, limit),
"case {case}: {query:?}, sensitive={sensitive}, whole={whole}, limit={limit}"
);
}
}
}
}
}
#[test]
fn long_repeated_prefix_search_preserves_the_last_match_and_limit() {
let mut parts = vec!["a"; 131072];
parts.push("b");
let text = page(&parts);
let query = format!("{}b", "a".repeat(4095));
assert_eq!(text.find_opts_limited(&query, false, false, 1), vec![126977..131073]);
assert!(text.find(&format!("{}c", "a".repeat(4095))).is_empty());
}