use std::sync::OnceLock;
use crate::CharSet;
pub const WORDLIST: &str = include_str!("../data/profanity_wordlist.txt");
const ALPHABETIC_UNICODE: &str = include_str!("../data/alphabetic_unicode.json");
pub const CHARS_MAPPING: &[(char, &[char])] = &[
('a', &['a', '@', '*', '4']),
('i', &['i', '*', 'l', '1']),
('o', &['o', '*', '0', '@']),
('u', &['u', '*', 'v']),
('v', &['v', '*', 'u']),
('l', &['l', '1']),
('e', &['e', '*', '3']),
('s', &['s', '$', '5']),
('t', &['t', '7']),
];
pub fn allowed_characters() -> &'static CharSet {
static ALLOWED: OnceLock<CharSet> = OnceLock::new();
ALLOWED.get_or_init(|| {
let ascii = ('a'..='z')
.chain('A'..='Z')
.chain('0'..='9')
.chain("@$*\"'".chars());
CharSet::new(
ascii.chain(parse_string_array(ALPHABETIC_UNICODE)),
is_python_space,
)
})
}
pub fn is_python_space(c: char) -> bool {
matches!(
c,
'\t'..='\r'
| '\x1c'..=' '
| '\u{85}'
| '\u{a0}'
| '\u{1680}'
| '\u{2000}'..='\u{200a}'
| '\u{2028}'
| '\u{2029}'
| '\u{202f}'
| '\u{205f}'
| '\u{3000}'
)
}
pub fn read_wordlist(text: &str) -> impl Iterator<Item = &str> {
text.split(['\n', '\r'])
.map(|line| line.trim_matches(is_python_space))
.filter(|line| !line.is_empty())
}
fn parse_string_array(src: &str) -> Vec<char> {
let mut out = Vec::new();
let mut chars = src.chars();
let mut in_string = false;
let mut high: Option<u32> = None;
while let Some(c) = chars.next() {
if !in_string {
in_string = c == '"';
continue;
}
let code = match c {
'"' => {
in_string = false;
continue;
}
'\\' => match chars.next() {
Some('u') => {
let hex: String = chars.by_ref().take(4).collect();
u32::from_str_radix(&hex, 16).expect("valid \\u escape")
}
Some('n') => '\n' as u32,
Some('t') => '\t' as u32,
Some('r') => '\r' as u32,
Some('b') => 8,
Some('f') => 12,
Some(other) => other as u32,
None => break,
},
other => other as u32,
};
match (high.take(), code) {
(None, 0xD800..=0xDBFF) => high = Some(code),
(Some(h), 0xDC00..=0xDFFF) => {
out.extend(char::from_u32(
0x10000 + ((h - 0xD800) << 10) + (code - 0xDC00),
));
}
(_, code) => out.extend(char::from_u32(code)),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allowed_characters_match_python() {
let unicode = parse_string_array(ALPHABETIC_UNICODE);
assert_eq!(unicode.len(), 5849);
assert_eq!(unicode[26], '\u{b5}');
assert_eq!(unicode.iter().max(), Some(&'\u{e01ef}'));
let allowed = allowed_characters();
assert_eq!(allowed.len(), 5849 + 10 + 5);
assert!(allowed.contains('@') && allowed.contains('é') && allowed.contains('\u{e01ef}'));
assert!(!allowed.contains(' ') && !allowed.contains('-') && !allowed.contains('_'));
}
#[test]
fn wordlist_has_916_words() {
assert_eq!(read_wordlist(WORDLIST).count(), 916);
let words: Vec<&str> = read_wordlist(" a \r\n\r\nb\u{85}\rc\n\n").collect();
assert_eq!(words, ["a", "b", "c"]);
}
}