use alloc::string::String;
use crate::tables::{self, ConfusableResult};
#[inline]
fn confusable_map_char(c: char, out: &mut String) {
if !tables::confusable_bloom_might_contain(c as u32) {
out.push(c);
return;
}
match tables::lookup_confusable(c) {
ConfusableResult::None => out.push(c),
ConfusableResult::Single(mapped) => out.push(mapped),
ConfusableResult::Expansion { offset, length } => {
let data = tables::confusable_expansion_data(offset, length);
for &cp in data {
debug_assert!(cp <= 0x10FFFF && !(0xD800..=0xDFFF).contains(&cp));
let ch = unsafe { char::from_u32_unchecked(cp) };
out.push(ch);
}
},
}
}
pub fn skeleton(input: &str) -> String {
if input.is_empty() {
return String::new();
}
let mut current: String = crate::nfd().normalize(input).into_owned();
for _ in 0..8 {
let mut mapped = String::with_capacity(current.len());
for ch in current.chars() {
confusable_map_char(ch, &mut mapped);
}
let next = crate::nfd().normalize(&mapped).into_owned();
if next == current {
return next;
}
current = next;
}
current
}
pub fn are_confusable(a: &str, b: &str) -> bool {
skeleton(a) == skeleton(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skeleton_empty() {
assert_eq!(skeleton(""), "");
}
#[test]
fn skeleton_ascii_unchanged() {
let s = skeleton("hello");
assert!(!s.is_empty());
}
#[test]
fn confusable_latin_cyrillic_a() {
assert!(
are_confusable("a", "\u{0430}"),
"Latin 'a' and Cyrillic 'а' should be confusable"
);
}
#[test]
fn confusable_latin_cyrillic_word() {
let latin = "apple";
let mixed = "\u{0430}\u{0440}\u{0440}l\u{0435}";
assert!(are_confusable(latin, mixed));
}
#[test]
fn not_confusable_different_strings() {
assert!(!are_confusable("hello", "world"));
}
#[test]
fn confusable_identical_strings() {
assert!(are_confusable("test", "test"));
}
#[test]
fn skeleton_convergence() {
let input = "Hel\u{0430}"; let s1 = skeleton(input);
let s2 = skeleton(&s1);
assert_eq!(s1, s2, "skeleton must be idempotent");
}
#[test]
fn skeleton_idempotent_on_cascading_mapping() {
let input = "\u{1D0E}\u{326}\u{306}";
let s1 = skeleton(input);
let s2 = skeleton(&s1);
assert_eq!(s1, s2, "skeleton must be idempotent for cascading maps");
}
#[test]
fn confusable_fullwidth() {
let s1 = skeleton("A");
let s2 = skeleton("\u{FF21}");
let _ = (s1, s2);
}
}