Add support for banned word exceptions

This commit is contained in:
2025-08-18 22:04:46 -03:00
parent 23969006c2
commit a994c86f98
6 changed files with 192 additions and 52 deletions
+187 -47
View File
@@ -247,7 +247,7 @@ fn regex_from_phrase(phrase: &str) -> String {
///
/// Returns:
/// [`Regex`] - The merged regex for the phrases.
fn regex_from_phrases_list(phrases: &[&str]) -> Regex {
fn regex_from_phrases_list(phrases: &[String]) -> Regex {
let mut pattern = String::new();
for phrase in phrases {
@@ -405,31 +405,43 @@ pub trait Rule: Send + Sync {
/// * `file_content` - The content of the file to parse.
///
/// Returns:
/// [`Vec<&str>`] - The list of phrases found in the file.
fn parse_phrases_file(file_content: &str) -> Vec<&str> {
file_content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with("//") {
Some(trimmed)
/// [`(Vec<String>, Vec<String>)`] - The list of phrases found in the file.
fn parse_phrases_file(file_content: &str) -> (Vec<String>, Vec<String>) {
let mut banned = vec![];
let mut exceptions = vec![];
for line in file_content.lines() {
let trimmed = line.trim().to_ascii_lowercase();
if !trimmed.is_empty() && !trimmed.starts_with("//") {
if let Some((phrase, phrase_exceptions)) = trimmed.split_once("/") {
banned.push(phrase.trim().to_string());
exceptions.extend(phrase_exceptions.split(',').map(|s| s.trim().to_string()));
} else {
None
banned.push(trimmed.to_string());
}
})
.collect()
}
}
(banned, exceptions)
}
struct HateSpeechAndSlursRule(Regex);
struct HateSpeechAndSlursRule {
pattern: Regex,
exceptions: Vec<String>,
}
impl HateSpeechAndSlursRule {
fn new() -> Self {
let phrases: Vec<&str> =
let (phrases, exceptions) =
parse_phrases_file(include_str!("lists/hate-speech-and-slurs.txt"));
let regex = regex_from_phrases_list(&phrases);
Self(regex)
Self {
pattern: regex,
exceptions,
}
}
}
@@ -445,24 +457,43 @@ impl Rule for HateSpeechAndSlursRule {
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
(
vec![],
self.0
self.pattern
.find_iter(normalized)
.map(|m| normalized[m.start()..m.end()].to_string())
.filter_map(|m| {
let string = normalized[m.start()..m.end()].to_string();
if self.exceptions.contains(
&string
.split_ascii_whitespace()
.collect::<String>()
.to_ascii_lowercase(),
) {
None
} else {
Some(string)
}
})
.collect(),
)
}
}
struct ThreatsAndSelfHarmRule(Regex);
struct ThreatsAndSelfHarmRule {
pattern: Regex,
exceptions: Vec<String>,
}
impl ThreatsAndSelfHarmRule {
fn new() -> Self {
let phrases: Vec<&str> =
let (phrases, exceptions) =
parse_phrases_file(include_str!("lists/threats-and-self-harm.txt"));
let regex = regex_from_phrases_list(&phrases);
Self(regex)
Self {
pattern: regex,
exceptions,
}
}
}
@@ -478,24 +509,43 @@ impl Rule for ThreatsAndSelfHarmRule {
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
(
vec![],
self.0
self.pattern
.find_iter(normalized)
.map(|m| normalized[m.start()..m.end()].to_string())
.filter_map(|m| {
let string = normalized[m.start()..m.end()].to_string();
if self.exceptions.contains(
&string
.split_ascii_whitespace()
.collect::<String>()
.to_ascii_lowercase(),
) {
None
} else {
Some(string)
}
})
.collect(),
)
}
}
struct ExplicitSexualLanguageRule(Regex);
struct ExplicitSexualLanguageRule {
pattern: Regex,
exceptions: Vec<String>,
}
impl ExplicitSexualLanguageRule {
fn new() -> Self {
let phrases: Vec<&str> =
let (phrases, exceptions) =
parse_phrases_file(include_str!("lists/explicit-sexual-language.txt"));
let regex = regex_from_phrases_list(&phrases);
Self(regex)
Self {
pattern: regex,
exceptions,
}
}
}
@@ -511,23 +561,43 @@ impl Rule for ExplicitSexualLanguageRule {
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
(
vec![],
self.0
self.pattern
.find_iter(normalized)
.map(|m| normalized[m.start()..m.end()].to_string())
.filter_map(|m| {
let string = normalized[m.start()..m.end()].to_string();
if self.exceptions.contains(
&string
.split_ascii_whitespace()
.collect::<String>()
.to_ascii_lowercase(),
) {
None
} else {
Some(string)
}
})
.collect(),
)
}
}
struct SuggestiveLanguageRule(Regex);
struct SuggestiveLanguageRule {
pattern: Regex,
exceptions: Vec<String>,
}
impl SuggestiveLanguageRule {
fn new() -> Self {
let phrases: Vec<&str> = parse_phrases_file(include_str!("lists/suggestive-language.txt"));
let (phrases, exceptions) =
parse_phrases_file(include_str!("lists/suggestive-language.txt"));
let regex = regex_from_phrases_list(&phrases);
Self(regex)
Self {
pattern: regex,
exceptions,
}
}
}
@@ -543,9 +613,22 @@ impl Rule for SuggestiveLanguageRule {
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
(
vec![],
self.0
self.pattern
.find_iter(normalized)
.map(|m| normalized[m.start()..m.end()].to_string())
.filter_map(|m| {
let string = normalized[m.start()..m.end()].to_string();
if self.exceptions.contains(
&string
.split_ascii_whitespace()
.collect::<String>()
.to_ascii_lowercase(),
) {
None
} else {
Some(string)
}
})
.collect(),
)
}
@@ -580,17 +663,23 @@ impl Rule for AdvertisingRule {
}
}
struct SensitiveTopicsAndTriggeringContentRule(Regex);
struct SensitiveTopicsAndTriggeringContentRule {
pattern: Regex,
exceptions: Vec<String>,
}
impl SensitiveTopicsAndTriggeringContentRule {
fn new() -> Self {
let phrases: Vec<&str> = parse_phrases_file(include_str!(
let (phrases, exceptions) = parse_phrases_file(include_str!(
"lists/sensitive-topics-and-triggering-content.txt"
));
let regex = regex_from_phrases_list(&phrases);
Self(regex)
Self {
pattern: regex,
exceptions,
}
}
}
@@ -606,23 +695,42 @@ impl Rule for SensitiveTopicsAndTriggeringContentRule {
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
(
vec![],
self.0
self.pattern
.find_iter(normalized)
.map(|m| normalized[m.start()..m.end()].to_string())
.filter_map(|m| {
let string = normalized[m.start()..m.end()].to_string();
if self.exceptions.contains(
&string
.split_ascii_whitespace()
.collect::<String>()
.to_ascii_lowercase(),
) {
None
} else {
Some(string)
}
})
.collect(),
)
}
}
pub struct MildSwearWordsRule(Regex);
pub struct MildSwearWordsRule {
pattern: Regex,
exceptions: Vec<String>,
}
impl MildSwearWordsRule {
fn new() -> Self {
let phrases: Vec<&str> = parse_phrases_file(include_str!("lists/mild-swear-words.txt"));
let (phrases, exceptions) = parse_phrases_file(include_str!("lists/mild-swear-words.txt"));
let regex = regex_from_phrases_list(&phrases);
Self(regex)
Self {
pattern: regex,
exceptions,
}
}
}
@@ -638,24 +746,43 @@ impl Rule for MildSwearWordsRule {
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
(
vec![],
self.0
self.pattern
.find_iter(normalized)
.map(|m| normalized[m.start()..m.end()].to_string())
.filter_map(|m| {
let string = normalized[m.start()..m.end()].to_string();
if self.exceptions.contains(
&string
.split_ascii_whitespace()
.collect::<String>()
.to_ascii_lowercase(),
) {
None
} else {
Some(string)
}
})
.collect(),
)
}
}
pub struct CasualInsultsAndTeasing(Regex);
pub struct CasualInsultsAndTeasing {
pattern: Regex,
exceptions: Vec<String>,
}
impl CasualInsultsAndTeasing {
fn new() -> Self {
let phrases: Vec<&str> =
let (phrases, exceptions) =
parse_phrases_file(include_str!("lists/casual-insults-and-teasing.txt"));
let regex = regex_from_phrases_list(&phrases);
Self(regex)
Self {
pattern: regex,
exceptions,
}
}
}
@@ -671,9 +798,22 @@ impl Rule for CasualInsultsAndTeasing {
fn detect(&self, _original: &str, normalized: &str) -> (Vec<String>, Vec<String>) {
(
vec![],
self.0
self.pattern
.find_iter(normalized)
.map(|m| normalized[m.start()..m.end()].to_string())
.filter_map(|m| {
let string = normalized[m.start()..m.end()].to_string();
if self.exceptions.contains(
&string
.split_ascii_whitespace()
.collect::<String>()
.to_ascii_lowercase(),
) {
None
} else {
Some(string)
}
})
.collect(),
)
}