1use once_cell::sync::Lazy;
2use std::collections::HashMap;
3use std::sync::{Arc};
4use parking_lot::{RwLock, RwLockWriteGuard};
5
6use super::structs::*;
7
8use crate::lang::common::{
9 NORMALIZATION_PATTERNS, PAT_PUNCT3
10};
11use crate::lang::LangProvider;
12use crate::util::{remove_duplicates, is_pi_or_e_word};
13use fancy_regex;
14
15impl<'a, L: LangProvider> Censor<'a, L> {
16 pub fn new(lang: &'a L) -> Result<Self, CensorError> {
17 Ok(Self {
18 lang: &lang,
19 data: lang.data(),
20 re_cache: Lazy::new(|| Arc::new(RwLock::new(HashMap::with_capacity(1000))))
21 })
22 }
23
24 fn is_match_cached(&self, pat: &str, text: &str) -> bool {
25 {
27 let cache = self.re_cache.read();
28 if let Some(r) = cache.get(pat) {
29 return r.is_match(text).unwrap_or(false)
30 }
31 }
32
33 let r = fancy_regex::Regex::new(pat)
35 .map_err(|e| CensorError::RegexCompilationFailed(e.to_string())).unwrap();
36 let res = r.is_match(text).unwrap_or(false);
37 {
38 let mut cache = self.re_cache.write();
39 cache.insert(pat.to_string(), r);
40 }
41 res
42 }
43
44 fn compile_and_cache_pattern(&self, pat: &str, cache: &mut RwLockWriteGuard<HashMap<String, fancy_regex::Regex>>) {
54 let r = fancy_regex::Regex::new(pat)
55 .map_err(|e| CensorError::RegexCompilationFailed(e.to_string())).unwrap();
56 cache.insert(pat.to_string(), r);
57 }
58
59 pub fn precompile_all_patterns(&self) {
60 self.precompile_foul_data();
61 self.precompile_foul_core();
62 self.precompile_bad_phrases();
63 self.precompile_bad_semi_phrases();
64 self.precompile_excludes_core();
65 self.precompile_excludes_data();
66 }
67
68 pub fn precompile_foul_data(&self) {
69 let mut cache = self.re_cache.write();
70
71 for (_, pats) in self.data.foul_data {
72 for &pat in pats {
73 self.compile_and_cache_pattern(pat, &mut cache);
74 }
75 }
76 }
77
78 pub fn precompile_foul_core(&self) {
79 let mut cache = self.re_cache.write();
80
81 for (pat, _) in self.data.foul_core {
82 self.compile_and_cache_pattern(pat, &mut cache);
83 }
84 }
85
86 pub fn precompile_bad_phrases(&self) {
87 let mut cache = self.re_cache.write();
88
89 for &pat in self.data.bad_phrases {
90 self.compile_and_cache_pattern(pat, &mut cache);
91 }
92 }
93
94 pub fn precompile_bad_semi_phrases(&self) {
95 let mut cache = self.re_cache.write();
96
97 for &pat in self.data.bad_semi_phrases {
98 self.compile_and_cache_pattern(pat, &mut cache);
99 }
100 }
101
102 pub fn precompile_excludes_core(&self) {
103 let mut cache = self.re_cache.write();
104
105 for (pat, _) in self.data.excludes_core {
106 self.compile_and_cache_pattern(pat, &mut cache);
107 }
108 }
109
110 pub fn precompile_excludes_data(&self) {
111 let mut cache = self.re_cache.write();
112
113 for (_, pats) in self.data.excludes_data {
114 for &pat in pats {
115 self.compile_and_cache_pattern(pat, &mut cache);
116 }
117 }
118 }
119
120 fn replace_all_cached(&self, pat: &str, text: &'a str, repl: &str) -> Option<String> {
121 if !self.is_match_cached(pat, text) {
123 return None;
124 }
125
126 let cache = self.re_cache.read();
128 let compiled = cache.get(pat).unwrap();
129
130 let replaced = compiled.replace_all(text, repl).into_owned();
132 if replaced == text { None } else { Some(replaced) }
133 }
134
135 fn split_line(&self, s: &str) -> Vec<String> {
136 self.lang.split_line(s)
137 }
138
139 fn prepare_word(&self, mut w: String) -> String {
140 if !is_pi_or_e_word(&w) {
141 w = PAT_PUNCT3.replace_all(&w, "").into_owned();
143 }
144 let mut w = w.to_lowercase();
145
146 for (pat, rep) in NORMALIZATION_PATTERNS.iter() {
148 w = pat.replace_all(&w, *rep).into_owned();
149 }
150
151 w = crate::lang::common::translate_similar_chars(&w, self.data.trans_tab);
153
154 remove_duplicates(&w)
156 }
157
158 pub fn is_word_good(&self, raw: &str) -> bool {
159 let w = self.prepare_word(raw.to_string());
160 self.check_word_impl_fast(&w)
161 }
162
163 fn check_word_impl(&self, prepared: &String) -> WordInfo {
164 let mut info = WordInfo::new(Box::from(prepared.as_str()));
165
166 let fl_str = String::from(info.word.chars().next().map(|c| c.to_string()).unwrap_or_default());
168
169 if let Some(pats) = self.data.foul_data.get(fl_str.as_str()) {
171 for &pat in pats {
172 if self.is_match_cached(pat, &info.word) {
173 info.is_good = false;
174 info.accuse.push(Box::from(pat)); break;
176 }
177 }
178 }
179
180 if info.is_good {
182 for (&_key, &pat) in self.data.foul_core.iter() {
183 if self.is_match_cached(pat, prepared) {
184 info.is_good = false;
185 info.accuse.push(Box::from(pat));
186 break;
187 }
188 }
189 }
190
191 if info.is_good {
193 for &pat in self.data.bad_semi_phrases.iter() {
194 if self.is_match_cached(pat, prepared) {
195 info.is_good = false;
196 info.accuse.push(Box::from(pat));
197 break;
198 }
199 }
200 }
201
202 if !info.is_good {
204 for (&_key, &pat) in self.data.excludes_core.iter() {
206 if self.is_match_cached(pat, prepared) {
207 info.is_good = true;
208 info.excuse.push(Box::from(pat));
209 break;
210 }
211 }
212 if !info.is_good {
214 if let Some(pats) = self.data.excludes_data.get(fl_str.as_str()) {
215 for &pat in pats {
216 if self.is_match_cached(pat, prepared) {
217 info.is_good = true;
218 info.excuse.push(Box::from(pat));
219 break;
220 }
221 }
222 }
223 }
224 }
225
226 info
227 }
228
229 fn check_word_impl_fast(&self, prepared: &str) -> bool {
230 let fl_str = String::from(prepared.chars().next().map(|c| c.to_string()).unwrap_or_default());
232
233 if let Some(pats) = self.data.foul_data.get(fl_str.as_str()) {
235 for &pat in pats {
236 if self.is_match_cached(pat, prepared) {
237 return false;
238 }
239 }
240 }
241
242 for (&_key, &pat) in self.data.foul_core.iter() {
244 if self.is_match_cached(pat, prepared) {
245 return false;
246 }
247 }
248
249 for &pat in self.data.bad_semi_phrases.iter() {
251 if self.is_match_cached(pat, prepared) {
252 return false;
253 }
254 }
255
256 for (&_key, &pat) in self.data.excludes_core.iter() {
259 if self.is_match_cached(pat, prepared) {
260 return true;
261 }
262 }
263 if let Some(pats) = self.data.excludes_data.get(fl_str.as_str()) {
265 for &pat in pats {
266 if self.is_match_cached(pat, prepared) {
267 return true;
268 }
269 }
270 }
271
272 false }
274
275 pub fn clean_line(&self, line: &str) -> CleanLineResult {
277 let mut out = line.to_string();
279
280 let mut bad_words = 0usize;
282 let mut bad_phrases = 0usize;
283 let mut detected_words: Vec<Box<str>> = Vec::with_capacity(5);
284 let mut detected_pats = Vec::with_capacity(5);
285
286 for word in self.split_line(line) {
293 let prepared = self.prepare_word(word.clone());
294 let info = self.check_word_impl(&prepared);
295 if !info.is_good {
296 bad_words += 1;
297 out = out.replacen(&word, self.data.beep, 1);
298 detected_words.push(Box::from(word.as_str()));
299 if let Some(p) = info.accuse.get(0) {
300 detected_pats.push(p.clone());
301 }
302 }
303 }
304
305 for &pat in self.data.bad_semi_phrases.iter() {
311 if let Some(new_out) = self.replace_all_cached(pat, &out, self.data.beep) {
312 bad_phrases += 1;
313 detected_pats.push(Box::from(pat));
314 out = new_out;
315 }
316 }
317
318 for &pat in self.data.bad_phrases.iter() {
320 if let Some(new_out) = self.replace_all_cached(pat, &out, self.data.beep) {
321 bad_phrases += 1;
322 detected_pats.push(Box::from(pat));
323 out = new_out;
324 }
325 }
326
327 CleanLineResult {
328 line: out,
329 bad_words_count: bad_words,
330 bad_phrases_count: bad_phrases,
331 detected_bad_words: detected_words,
332 detected_patterns: detected_pats,
333 }
334 }
335
336 pub fn clean_html_line(&self, line: &str) -> CleanHtmlResult {
340 use crate::html::{tokenize_html, TokType, Token};
341
342 let tokens = tokenize_html(line);
343
344 let mut current_word = String::new(); let mut current_tagged = String::new(); let mut tagged_list: Vec<&Token> = Vec::new(); let mut out = String::new();
349 let mut bad_count = 0usize;
350
351 let beep_html = self.data.beep_html; fn get_remained_tokens(tagged: &[&Token]) -> (String, String) {
356 let mut pre = String::new();
357 let mut post = String::new();
358
359 for t in tagged {
360 match t.kind {
361 TokType::TagOpen | TokType::TagSelf => {
362 pre.push_str(&t.value);
364 }
365 TokType::TagClose => {
366 post.push_str(&t.value);
368 }
369 _ => {}
370 }
371 }
372 (pre, post)
373 }
374
375 let process_spacer = |cw: &mut String,
379 ctw: &mut String,
380 twl: &mut Vec<&Token>,
381 r: &mut String,
382 bwc: &mut usize,
383 tok: Option<&Token>| {
384 if !cw.is_empty() {
385 if !self.is_word_good(cw) {
387 let (pre, post) = get_remained_tokens(twl);
388 *r += ⪯
389 *r += beep_html;
390 *r += &post;
391 *bwc += 1;
392 } else {
393 *r += ctw;
395 }
396 }
397 twl.clear();
399 cw.clear();
400 ctw.clear();
401
402 if let Some(t) = tok {
404 *r += &t.value;
405 }
406 };
407
408 for tok in &tokens {
410 match tok.kind {
411 TokType::TagOpen | TokType::TagClose | TokType::TagSelf => {
412 tagged_list.push(tok);
414 current_tagged.push_str(&tok.value);
415 }
416 TokType::Word => {
417 if !self.is_word_good(¤t_word) {
420 process_spacer(
421 &mut current_word,
422 &mut current_tagged,
423 &mut tagged_list,
424 &mut out,
425 &mut bad_count,
426 Some(tok),
427 );
428 } else {
429 tagged_list.push(tok);
430 current_tagged.push_str(&tok.value);
431 current_word.push_str(&tok.value);
432 }
433 }
434 TokType::Space | TokType::Spacer => {
435 process_spacer(
437 &mut current_word,
438 &mut current_tagged,
439 &mut tagged_list,
440 &mut out,
441 &mut bad_count,
442 Some(tok),
443 );
444 }
445 }
446 }
447
448 if !current_word.is_empty() || !current_tagged.is_empty() {
450 process_spacer(
451 &mut current_word,
452 &mut current_tagged,
453 &mut tagged_list,
454 &mut out,
455 &mut bad_count,
456 None,
457 );
458 }
459
460 CleanHtmlResult { line: out, bad_words_count: bad_count }
461 }
462}