const ZERO_WIDTH: &[u32] = &[
0x200B, 0x200C, 0x200D, 0x2060, 0xFEFF, 0x00AD, 0x034F, 0x180E, 0x2061, 0x2062, 0x2063, 0x2064, ];
const BIDI_CONTROLS: &[u32] = &[
0x200E, 0x200F, 0x061C, 0x202A, 0x202B, 0x202C, 0x202D, 0x202E, 0x2066, 0x2067, 0x2068, 0x2069, ];
const INVISIBLE_LETTERS: &[u32] = &[
0x115F, 0x1160, 0x3164, 0xFFA0, ];
const TAG_BLOCK: std::ops::RangeInclusive<u32> = 0xE0000..=0xE007F;
pub fn is_invisible(codepoint: u32) -> bool {
ZERO_WIDTH.contains(&codepoint)
|| BIDI_CONTROLS.contains(&codepoint)
|| INVISIBLE_LETTERS.contains(&codepoint)
|| TAG_BLOCK.contains(&codepoint)
}
pub fn sanitize(text: &str) -> (String, usize) {
let mut removed = 0usize;
let cleaned: String = text
.chars()
.filter(|c| {
if is_invisible(*c as u32) {
removed += 1;
false
} else {
true
}
})
.collect();
(cleaned, removed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_zero_width_and_counts() {
let (out, n) = sanitize("he\u{200B}llo\u{FEFF}");
assert_eq!(out, "hello");
assert_eq!(n, 2);
}
#[test]
fn strips_bidi_override() {
let (out, n) = sanitize("safe\u{202E}reversed");
assert_eq!(out, "safereversed");
assert_eq!(n, 1);
}
#[test]
fn strips_tag_block_payload() {
let payload: String = "AB"
.chars()
.map(|c| char::from_u32(0xE0000 + c as u32).unwrap())
.collect();
let (out, n) = sanitize(&format!("visible{payload}"));
assert_eq!(out, "visible");
assert_eq!(n, 2);
}
#[test]
fn leaves_ordinary_text_untouched() {
let (out, n) = sanitize("普通文本 with ASCII — and em dash");
assert_eq!(out, "普通文本 with ASCII — and em dash");
assert_eq!(n, 0);
}
#[test]
fn keeps_rtl_letters() {
let (out, n) = sanitize("مرحبا\u{202B}");
assert_eq!(out, "مرحبا");
assert_eq!(n, 1);
}
}