From 513e810c916963f798b342b15fcda7243dc97d32 Mon Sep 17 00:00:00 2001 From: lashman Date: Tue, 8 Apr 2025 22:48:17 +0300 Subject: [PATCH] blocked word lists --- cruciverb-core/src/dict/blocklist.rs | 82 ++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 cruciverb-core/src/dict/blocklist.rs diff --git a/cruciverb-core/src/dict/blocklist.rs b/cruciverb-core/src/dict/blocklist.rs new file mode 100644 index 0000000..60cdac8 --- /dev/null +++ b/cruciverb-core/src/dict/blocklist.rs @@ -0,0 +1,82 @@ +use std::collections::HashSet; +use std::path::Path; + +use crate::error::Result; + +// embedded at compile time - cannot be removed without recompiling +const BUILTIN_LIST: &str = include_str!("../../../data/banned_words.txt"); + +pub struct Blocklist { + words: HashSet, +} + +fn normalize_leet(s: &str) -> String { + s.chars().map(|c| match c { + '0' | 'O' => 'O', + '1' | 'l' | '!' | '|' => 'I', + '3' => 'E', + '4' | '@' => 'A', + '5' | '$' => 'S', + '7' => 'T', + '8' => 'B', + '9' => 'G', + _ => c.to_ascii_uppercase(), + }).collect() +} + +fn parse_lines(content: &str) -> impl Iterator + '_ { + content.lines() + .map(|l| l.trim().to_uppercase()) + .filter(|l| !l.is_empty() && !l.starts_with('#')) +} + +impl Blocklist { + pub fn empty() -> Self { + // "empty" still includes the built-in list + let words = parse_lines(BUILTIN_LIST).collect(); + Self { words } + } + + pub fn load(path: &Path) -> Result { + // start with built-in, then add from file (additive only) + let mut words: HashSet = parse_lines(BUILTIN_LIST).collect(); + if let Ok(content) = std::fs::read_to_string(path) { + for w in parse_lines(&content) { + words.insert(w); + } + } + Ok(Self { words }) + } + + pub fn is_blocked(&self, word: &str) -> bool { + self.words.contains(word) + } + + pub fn add_word(&mut self, word: &str) { + let w = word.trim().to_uppercase(); + if !w.is_empty() { + self.words.insert(w); + } + } + + pub fn extra_count(&self) -> usize { + let builtin: HashSet = parse_lines(BUILTIN_LIST).collect(); + self.words.len().saturating_sub(builtin.len()) + } + + pub fn contains_blocked(&self, text: &str) -> bool { + let upper = text.to_uppercase(); + let normalized = normalize_leet(text); + let stripped: String = upper.chars().filter(|c| c.is_alphabetic()).collect(); + let stripped_leet: String = normalized.chars().filter(|c| c.is_alphabetic()).collect(); + + for word in &self.words { + if word.len() < 3 { continue; } + if upper.contains(word.as_str()) { return true; } + if normalized.contains(word.as_str()) { return true; } + if stripped.contains(word.as_str()) { return true; } + if stripped_leet.contains(word.as_str()) { return true; } + } + false + } +}