use std::borrow::Cow;
use std::sync::LazyLock;
use regex::{Captures, Regex, RegexBuilder};
use super::utf16::is_upper_unit;
pub fn clean(str: &str, strong: bool) -> String {
if strong {
let str = apply_replacements(&fix_explicit(str), STRONG_MASKS);
apply_boundary_masks(
&str,
[(*b"ass", *b"***"), (*b"Ass", *b"***"), (*b"hoe", *b"***")],
)
} else {
let str = apply_replacements(str, PARTIAL_MASKS);
apply_boundary_masks(
&str,
[(*b"ass", *b"a*s"), (*b"Ass", *b"A*s"), (*b"hoe", *b"h*e")],
)
}
}
fn apply_replacements(str: &str, masks: &[(&str, &str)]) -> String {
masks.iter().fold(str.to_string(), |text, (word, mask)| {
text.replace(word, mask)
})
}
const STRONG_MASKS: &[(&str, &str)] = &[
("bitches", "*****"),
("Bitches", "*****"),
("bitch", "*****"),
("Bitch", "*****"),
("damn", "****"),
("Damn", "****"),
("dammit", "******"),
("Dammit", "******"),
("dick", "****"),
("Dick", "****"),
("dope", "****"),
("Dope", "****"),
("fuck", "****"),
("Fuck", "****"),
("nigga", "*****"),
("Nigga", "*****"),
("nigras", "******"),
("Nigras", "******"),
("pussy", "*****"),
("Pussy", "*****"),
("sex", "***"),
("Sex", "***"),
("shit", "****"),
("Shit", "****"),
("weed", "****"),
("Weed", "****"),
("whore", "*****"),
("Whore", "*****"),
("cocaine", "*******"),
("Cocaine", "*******"),
("drug", "****"),
("Drug", "****"),
];
const PARTIAL_MASKS: &[(&str, &str)] = &[
("bitch", "b***h"),
("Bitch", "B***h"),
("damn", "d**n"),
("Damn", "D**n"),
("dammit", "D**mit"),
("Dammit", "D**mit"),
("dick", "d**k"),
("Dick", "D**k"),
("dope", "d**e"),
("Dope", "D**e"),
("fuck", "f**k"),
("Fuck", "F**k"),
("nigga", "n***a"),
("Nigga", "N***a"),
("nigras", "n***as"),
("Nigras", "N***as"),
("pussy", "p***y"),
("Pussy", "P***y"),
("sex", "s*x"),
("Sex", "S*x"),
("shit", "s**t"),
("Shit", "S**t"),
("weed", "w**d"),
("Weed", "W**d"),
("whore", "w***e"),
("Whore", "W***e"),
];
pub fn fix_explicit(str: &str) -> String {
let mut result = str.to_string();
for (regex, replacement) in REPLACEMENT_REGEXES.iter() {
let replaced = regex.replace_all(&result, |caps: &Captures<'_>| -> Cow<'static, str> {
let first_unit = caps[0].encode_utf16().next().unwrap_or_default();
if is_upper_unit(first_unit) {
let mut capitalized = String::with_capacity(replacement.len());
capitalized.push(replacement.as_bytes()[0].to_ascii_uppercase() as char);
capitalized.push_str(&replacement[1..]);
Cow::Owned(capitalized)
} else {
Cow::Borrowed(replacement)
}
});
if let Cow::Owned(replaced) = replaced {
result = replaced;
}
}
result
}
static REPLACEMENTS: &[(&str, &str)] = &[
(r"a\*s", "ass"),
(r"a\*\*", "ass"),
(r"b\*{3}h", "bitch"),
(r"b\*{2}ch", "bitch"),
(r"b\*{5}s", "bitches"),
(r"d\*{2}n", "damn"),
(r"d\*{2}mit", "dammit"),
(r"d\*{2}k", "dick"),
(r"d\*{2}e", "dope"),
(r"f\*{2}k", "fuck"),
(r"f\*ck", "fuck"),
(r"fu\*k", "fuck"),
(r"h\*e", "hoe"),
(r"h\*{2}s", "hoes"),
(r"mother\*{4}", "motherfuck"),
(r"n\*{3}a", "nigga"),
(r"n\*{2}ga", "nigga"),
(r"ni\*{2}a", "nigga"),
(r"n\*{3}as", "nigras"),
(r"p\*{3}y", "pussy"),
(r"p\*{2}sy", "pussy"),
(r"sh\*t", "shit"),
(r"s\*x", "sex"),
(r"s\*{2}t", "shit"),
(r"w\*{2}d", "weed"),
(r"w\*{3}e", "whore"),
(r"c\*{4}e", "cocaine"),
(r"d\*{2}g", "drug"),
];
static REPLACEMENT_REGEXES: LazyLock<Vec<(Regex, &'static str)>> = LazyLock::new(|| {
REPLACEMENTS
.iter()
.map(|(pattern, replacement)| {
let regex = RegexBuilder::new(pattern)
.case_insensitive(true)
.build()
.expect("上游 Replacements 中的正则均为合法表达式");
(regex, *replacement)
})
.collect()
});
fn apply_boundary_masks(str: &str, masks: [([u8; 3], [u8; 3]); 3]) -> String {
let mut units: Vec<u16> = str.encode_utf16().collect();
for (word, replacement) in masks {
fix_boundary_word(&mut units, word, replacement);
}
String::from_utf16_lossy(&units)
}
fn fix_boundary_word(units: &mut [u16], word: [u8; 3], replacement: [u8; 3]) {
const SPACE: u16 = b' ' as u16;
const HYPHEN: u16 = b'-' as u16;
let word = [u16::from(word[0]), u16::from(word[1]), u16::from(word[2])];
let replacement = [
u16::from(replacement[0]),
u16::from(replacement[1]),
u16::from(replacement[2]),
];
let mut i = 0;
while i + 2 < units.len() {
if units[i..i + 3] == word {
let at_left_boundary = i == 0 || units[i - 1] == SPACE || units[i - 1] == HYPHEN;
let at_right_boundary =
i + 3 >= units.len() || units[i + 3] == SPACE || units[i + 3] == HYPHEN;
if at_left_boundary && at_right_boundary {
units[i..i + 3].copy_from_slice(&replacement);
}
i += 3;
} else {
i += 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn weak_clean_keeps_first_and_last_letter() {
assert_eq!(clean("damn", false), "d**n");
assert_eq!(clean("Fuck", false), "F**k");
assert_eq!(
clean("shut up and fucking listen", false),
"shut up and f**king listen"
);
assert_eq!(clean("bitches", false), "b***hes");
assert_eq!(clean("dammit", false), "D**mit");
assert_eq!(clean("cocaine and drug", false), "cocaine and drug");
}
#[test]
fn strong_clean_masks_whole_words() {
assert_eq!(clean("damn", true), "****");
assert_eq!(clean("Fuck", true), "****");
assert_eq!(clean("Bitches be crazy", true), "***** be crazy");
assert_eq!(clean("cocaine and drug", true), "******* and ****");
assert_eq!(clean("ass Damn", true), "*** ****");
}
#[test]
fn strong_clean_restores_masked_forms_before_masking() {
assert_eq!(clean("F**k this s**t", true), "**** this ****");
}
#[test]
fn standalone_ass_and_hoe_are_masked_at_word_boundaries() {
assert_eq!(clean("ass", false), "a*s");
assert_eq!(clean("Ass man", false), "A*s man");
assert_eq!(clean("ass ass", false), "a*s a*s");
assert_eq!(clean("hoe", false), "h*e");
assert_eq!(clean("shoe hoe", false), "shoe h*e");
assert_eq!(clean("ass", true), "***");
assert_eq!(clean("ass-Ass", false), "a*s-A*s");
}
#[test]
fn ass_and_hoe_inside_words_are_left_alone() {
assert_eq!(clean("class", false), "class");
assert_eq!(clean("pass", false), "pass");
assert_eq!(clean("bass-", false), "bass-");
assert_eq!(clean("badass", false), "badass");
assert_eq!(clean("classic", false), "classic");
assert_eq!(
clean("what a badass bass player", false),
"what a badass bass player"
);
assert_eq!(clean("ASS", false), "ASS");
assert_eq!(clean("Hoes", false), "Hoes");
}
#[test]
fn boundary_rule_requires_space_or_hyphen_next_to_the_match() {
assert_eq!(clean("asset", false), "asset");
assert_eq!(clean("asshole", false), "asshole");
assert_eq!(clean("ass.", false), "ass.");
assert_eq!(clean("(ass)", false), "(ass)");
assert_eq!(clean("Ass!", false), "Ass!");
assert_eq!(clean("ass-", false), "a*s-");
assert_eq!(clean("-ass", false), "-a*s");
assert_eq!(clean("hoe-down", false), "h*e-down");
}
#[test]
fn boundary_scan_indexes_utf16_code_units() {
assert_eq!(clean("🎵 ass 🎵", false), "🎵 a*s 🎵");
assert_eq!(clean("你ass好", false), "你ass好");
assert_eq!(clean("🙂ass", false), "🙂ass");
}
#[test]
fn fix_explicit_restores_words_and_preserves_capitalization() {
assert_eq!(fix_explicit("F**k"), "Fuck");
assert_eq!(fix_explicit("s**t"), "shit");
assert_eq!(fix_explicit("F**K"), "Fuck");
assert_eq!(fix_explicit("A*s"), "Ass");
assert_eq!(fix_explicit("B***h"), "Bitch");
assert_eq!(fix_explicit("b*****s"), "bitches");
assert_eq!(fix_explicit("h**s"), "hoes");
assert_eq!(fix_explicit("mother****"), "motherfuck");
assert_eq!(fix_explicit("c****e"), "cocaine");
assert_eq!(fix_explicit("n***as"), "niggas");
assert_eq!(fix_explicit("Fuck this shit"), "Fuck this shit");
}
}