use icu_casemap::CaseMapper;
use icu_segmenter::WordSegmenter;
use icu_segmenter::options::WordBreakInvariantOptions;
fn fold(locale: &str, s: &str) -> String {
let mapper = CaseMapper::new();
let lang = locale.split(['-', '_']).next().unwrap_or("");
match lang {
"tr" | "az" => mapper.fold_turkic_string(s).into_owned(),
_ => mapper.fold_string(s).into_owned(),
}
}
fn word_starts(text: &str) -> Vec<usize> {
let segmenter = WordSegmenter::new_auto(WordBreakInvariantOptions::default());
let mut starts = vec![0usize];
let mut prev = 0usize;
for (boundary, word_type) in segmenter.segment_str(text).iter_with_word_type() {
if boundary > prev && word_type.is_word_like() && prev != 0 {
starts.push(prev);
}
prev = boundary;
}
starts
}
pub fn matches_search_in(locale: &str, text: &str, query: &str) -> bool {
let needle = fold(locale, query.trim());
if needle.is_empty() {
return true;
}
word_starts(text)
.into_iter()
.any(|start| fold(locale, &text[start..]).starts_with(&needle))
}
pub fn matches_search(text: &str, query: &str) -> bool {
let locale = crate::locale().get();
matches_search_in(&locale, text, query)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_query_matches_the_start_of_any_word() {
let titles = [
"About",
"Canvas & shapes",
"Controls",
"Device & sensors",
"Platform services",
"Stack",
"Toolbars",
];
let hit: Vec<&str> = titles
.iter()
.copied()
.filter(|t| matches_search_in("en", t, "s"))
.collect();
assert_eq!(
hit,
[
"Canvas & shapes",
"Device & sensors",
"Platform services",
"Stack"
]
);
assert!(!matches_search_in("en", "Toolbars", "s"));
assert!(!matches_search_in("en", "Controls", "s"));
}
#[test]
fn matching_ignores_case_in_both_directions() {
assert!(matches_search_in("en", "About", "abo"));
assert!(matches_search_in("en", "about", "ABO"));
assert!(matches_search_in("en", "ABOUT", "aBo"));
}
#[test]
fn an_empty_query_matches_everything() {
assert!(matches_search_in("en", "Anything", ""));
assert!(matches_search_in("en", "Anything", " "));
assert!(matches_search_in("en", "", ""));
assert!(!matches_search_in("en", "", "a"));
}
#[test]
fn a_multi_word_query_matches_from_a_word_start() {
assert!(matches_search_in("en", "Canvas & shapes", "canvas &"));
assert!(matches_search_in("en", "Platform services", "platform ser"));
assert!(!matches_search_in(
"en",
"Platform services",
"services platform"
));
}
#[test]
fn a_query_inside_a_word_does_not_match() {
assert!(!matches_search_in("en", "Localization", "cal"));
assert!(matches_search_in("en", "Localization", "loc"));
}
#[test]
fn folding_handles_the_cases_lowercasing_gets_wrong() {
assert!(matches_search_in("de", "Straße", "STRASSE"));
assert!(matches_search_in("de", "STRASSE", "straße"));
assert!(matches_search_in("el", "Οδός", "οδός"));
}
#[test]
fn turkish_folds_the_two_letter_is_apart() {
assert!(!matches_search_in("tr", "Irmak", "i"));
assert!(matches_search_in("tr", "Irmak", "ı"));
assert!(matches_search_in("en", "Irmak", "i"));
}
#[test]
fn word_starts_are_found_in_scripts_without_spaces() {
assert!(matches_search_in("ja", "日本語入力", "入力"));
assert!(matches_search_in("ja", "日本語入力", "日本"));
assert!(!matches_search_in("ja", "日本語入力", "語"));
}
#[test]
fn arabic_matches_by_word_prefix() {
assert!(matches_search_in("ar", "شريط الأدوات", "شريط"));
assert!(matches_search_in("ar", "شريط الأدوات", "الأ"));
assert!(!matches_search_in("ar", "شريط الأدوات", "دوات"));
}
#[test]
fn digits_count_as_a_word() {
assert!(matches_search_in("en", "Day 2026 release", "2026"));
assert!(matches_search_in("en", "Day 2026 release", "20"));
}
}
#[cfg(test)]
mod self_match {
use super::*;
#[test]
fn every_title_matches_itself() {
for (locale, title) in [
("en", "Stack"),
("fr", "Pile"),
("ar", "المكدّس"),
("zh-CN", "堆栈"),
("zh", "堆栈"),
("zh-CN", "工具栏"),
] {
assert!(
matches_search_in(locale, title, title),
"{locale}: {title:?} does not match itself"
);
}
}
}