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 Censor {
16 pub fn new(lang: CensorLang) -> Result<Self, CensorError> {
17 match lang {
18 CensorLang::Ru => {
19 let lang = Arc::new(crate::lang::ru::Ru {});
20 Censor::from(lang)
21 },
22 CensorLang::En => {
23 let lang = Arc::new(crate::lang::en::En {});
24 Censor::from(lang)
25 },
26 }
27 }
28
29 pub fn from<L>(lang: Arc<L>) -> Result<Self, CensorError>
30 where
31 L: LangProvider + Send + Sync + 'static,
32 {
33 let data = lang.data();
34 Ok(Self {
35 lang,
36 data,
37 re_cache: Lazy::new(|| Arc::new(RwLock::new(HashMap::with_capacity(100))))
38 })
39 }
40
41 fn is_match_cached(&self, pat: &str, text: &str) -> bool {
42 {
44 let cache = self.re_cache.read();
45 if let Some(r) = cache.get(pat) {
46 return r.is_match(text).unwrap_or(false)
47 }
48 }
49
50 let r = fancy_regex::Regex::new(pat)
52 .map_err(|e| CensorError::RegexCompilationFailed(e.to_string())).unwrap();
53 let res = r.is_match(text).unwrap_or(false);
54 {
55 let mut cache = self.re_cache.write();
56 cache.insert(pat.to_string(), r);
57 }
58 res
59 }
60
61 fn compile_and_cache_pattern(&self, pat: &str, cache: &mut RwLockWriteGuard<HashMap<String, fancy_regex::Regex>>) {
71 let r = fancy_regex::Regex::new(pat)
72 .map_err(|e| CensorError::RegexCompilationFailed(e.to_string())).unwrap();
73 cache.insert(pat.to_string(), r);
74 }
75
76 pub fn precompile_all_patterns(&self) {
77 self.precompile_foul_data();
78 self.precompile_foul_core();
79 self.precompile_bad_phrases();
80 self.precompile_bad_semi_phrases();
81 self.precompile_excludes_core();
82 self.precompile_excludes_data();
83 }
84
85 pub fn precompile_foul_data(&self) {
86 let mut cache = self.re_cache.write();
87
88 for (_, pats) in self.data.foul_data {
89 for &pat in pats {
90 self.compile_and_cache_pattern(pat, &mut cache);
91 }
92 }
93 }
94
95 pub fn precompile_foul_core(&self) {
96 let mut cache = self.re_cache.write();
97
98 for (pat, _) in self.data.foul_core {
99 self.compile_and_cache_pattern(pat, &mut cache);
100 }
101 }
102
103 pub fn precompile_bad_phrases(&self) {
104 let mut cache = self.re_cache.write();
105
106 for &pat in self.data.bad_phrases {
107 self.compile_and_cache_pattern(pat, &mut cache);
108 }
109 }
110
111 pub fn precompile_bad_semi_phrases(&self) {
112 let mut cache = self.re_cache.write();
113
114 for &pat in self.data.bad_semi_phrases {
115 self.compile_and_cache_pattern(pat, &mut cache);
116 }
117 }
118
119 pub fn precompile_excludes_core(&self) {
120 let mut cache = self.re_cache.write();
121
122 for (pat, _) in self.data.excludes_core {
123 self.compile_and_cache_pattern(pat, &mut cache);
124 }
125 }
126
127 pub fn precompile_excludes_data(&self) {
128 let mut cache = self.re_cache.write();
129
130 for (_, pats) in self.data.excludes_data {
131 for &pat in pats {
132 self.compile_and_cache_pattern(pat, &mut cache);
133 }
134 }
135 }
136
137 fn replace_all_cached(&self, pat: &str, text: &str, repl: &str) -> Option<String> {
138 if !self.is_match_cached(pat, text) {
140 return None;
141 }
142
143 let cache = self.re_cache.read();
145 let compiled = cache.get(pat).unwrap();
146
147 let replaced = compiled.replace_all(text, repl).into_owned();
149 if replaced == text { None } else { Some(replaced) }
150 }
151
152 fn split_line(&self, s: &str) -> Vec<String> {
153 self.lang.split_line(s)
154 }
155
156 fn prepare_word(&self, mut w: String) -> String {
157 if !is_pi_or_e_word(&w) {
158 w = PAT_PUNCT3.replace_all(&w, "").into_owned();
160 }
161 let mut w = w.to_lowercase();
162
163 for (pat, rep) in NORMALIZATION_PATTERNS.iter() {
165 w = pat.replace_all(&w, *rep).into_owned();
166 }
167
168 w = crate::lang::common::translate_similar_chars(&w, self.data.trans_tab);
170
171 remove_duplicates(&w)
173 }
174
175 pub fn is_word_good(&self, raw: &str) -> bool {
176 let w = self.prepare_word(raw.to_string());
177 self.check_word_impl_fast(&w)
178 }
179
180 fn check_word_impl(&self, prepared: &String) -> WordInfo {
181 let mut info = WordInfo::new(Box::from(prepared.as_str()));
182
183 let fl_str = String::from(info.word.chars().next().map(|c| c.to_string()).unwrap_or_default());
185
186 if let Some(pats) = self.data.foul_data.get(fl_str.as_str()) {
188 for &pat in pats {
189 if self.is_match_cached(pat, &info.word) {
190 info.is_good = false;
191 info.accuse.push(Box::from(pat)); break;
193 }
194 }
195 }
196
197 if info.is_good {
199 for (&_key, &pat) in self.data.foul_core.iter() {
200 if self.is_match_cached(pat, prepared) {
201 info.is_good = false;
202 info.accuse.push(Box::from(pat));
203 break;
204 }
205 }
206 }
207
208 if info.is_good {
210 for &pat in self.data.bad_semi_phrases.iter() {
211 if self.is_match_cached(pat, prepared) {
212 info.is_good = false;
213 info.accuse.push(Box::from(pat));
214 break;
215 }
216 }
217 }
218
219 if !info.is_good {
221 for (&_key, &pat) in self.data.excludes_core.iter() {
223 if self.is_match_cached(pat, prepared) {
224 info.is_good = true;
225 info.excuse.push(Box::from(pat));
226 break;
227 }
228 }
229 if !info.is_good {
231 if let Some(pats) = self.data.excludes_data.get(fl_str.as_str()) {
232 for &pat in pats {
233 if self.is_match_cached(pat, prepared) {
234 info.is_good = true;
235 info.excuse.push(Box::from(pat));
236 break;
237 }
238 }
239 }
240 }
241 }
242
243 info
244 }
245
246 fn check_word_impl_fast(&self, prepared: &str) -> bool {
247 let fl_str = String::from(prepared.chars().next().map(|c| c.to_string()).unwrap_or_default());
249
250 if let Some(pats) = self.data.foul_data.get(fl_str.as_str()) {
252 for &pat in pats {
253 if self.is_match_cached(pat, prepared) {
254 return false;
255 }
256 }
257 }
258
259 for (&_key, &pat) in self.data.foul_core.iter() {
261 if self.is_match_cached(pat, prepared) {
262 return false;
263 }
264 }
265
266 for &pat in self.data.bad_semi_phrases.iter() {
268 if self.is_match_cached(pat, prepared) {
269 return false;
270 }
271 }
272
273 for (&_key, &pat) in self.data.excludes_core.iter() {
276 if self.is_match_cached(pat, prepared) {
277 return true;
278 }
279 }
280 if let Some(pats) = self.data.excludes_data.get(fl_str.as_str()) {
282 for &pat in pats {
283 if self.is_match_cached(pat, prepared) {
284 return true;
285 }
286 }
287 }
288
289 false }
291
292 pub fn clean_line(&self, line: &str) -> CleanLineResult {
294 let mut out = line.to_string();
296
297 let mut bad_words = 0usize;
299 let mut bad_phrases = 0usize;
300 let mut detected_words: Vec<Box<str>> = Vec::with_capacity(5);
301 let mut detected_pats = Vec::with_capacity(5);
302
303 for word in self.split_line(line) {
310 let prepared = self.prepare_word(word.clone());
311 let info = self.check_word_impl(&prepared);
312 if !info.is_good {
313 bad_words += 1;
314 out = out.replacen(&word, self.data.beep, 1);
315 detected_words.push(Box::from(word.as_str()));
316 if let Some(p) = info.accuse.get(0) {
317 detected_pats.push(p.clone());
318 }
319 }
320 }
321
322 for &pat in self.data.bad_semi_phrases.iter() {
328 if let Some(new_out) = self.replace_all_cached(pat, &out, self.data.beep) {
329 bad_phrases += 1;
330 detected_pats.push(Box::from(pat));
331 out = new_out;
332 }
333 }
334
335 for &pat in self.data.bad_phrases.iter() {
337 if let Some(new_out) = self.replace_all_cached(pat, &out, self.data.beep) {
338 bad_phrases += 1;
339 detected_pats.push(Box::from(pat));
340 out = new_out;
341 }
342 }
343
344 CleanLineResult {
345 line: out,
346 bad_words_count: bad_words,
347 bad_phrases_count: bad_phrases,
348 detected_bad_words: detected_words,
349 detected_patterns: detected_pats,
350 }
351 }
352
353 pub fn clean_html_line(&self, line: &str) -> CleanHtmlResult {
357 use crate::html::{tokenize_html, TokType, Token};
358
359 let tokens = tokenize_html(line);
360
361 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();
366 let mut bad_count = 0usize;
367
368 let beep_html = self.data.beep_html; fn get_remained_tokens(tagged: &[&Token]) -> (String, String) {
373 let mut pre = String::new();
374 let mut post = String::new();
375
376 for t in tagged {
377 match t.kind {
378 TokType::TagOpen | TokType::TagSelf => {
379 pre.push_str(&t.value);
381 }
382 TokType::TagClose => {
383 post.push_str(&t.value);
385 }
386 _ => {}
387 }
388 }
389 (pre, post)
390 }
391
392 let process_spacer = |cw: &mut String,
396 ctw: &mut String,
397 twl: &mut Vec<&Token>,
398 r: &mut String,
399 bwc: &mut usize,
400 tok: Option<&Token>| {
401 if !cw.is_empty() {
402 if !self.is_word_good(cw) {
404 let (pre, post) = get_remained_tokens(twl);
405 *r += ⪯
406 *r += beep_html;
407 *r += &post;
408 *bwc += 1;
409 } else {
410 *r += ctw;
412 }
413 }
414 twl.clear();
416 cw.clear();
417 ctw.clear();
418
419 if let Some(t) = tok {
421 *r += &t.value;
422 }
423 };
424
425 for tok in &tokens {
427 match tok.kind {
428 TokType::TagOpen | TokType::TagClose | TokType::TagSelf => {
429 tagged_list.push(tok);
431 current_tagged.push_str(&tok.value);
432 }
433 TokType::Word => {
434 if !self.is_word_good(¤t_word) {
437 process_spacer(
438 &mut current_word,
439 &mut current_tagged,
440 &mut tagged_list,
441 &mut out,
442 &mut bad_count,
443 Some(tok),
444 );
445 } else {
446 tagged_list.push(tok);
447 current_tagged.push_str(&tok.value);
448 current_word.push_str(&tok.value);
449 }
450 }
451 TokType::Space | TokType::Spacer => {
452 process_spacer(
454 &mut current_word,
455 &mut current_tagged,
456 &mut tagged_list,
457 &mut out,
458 &mut bad_count,
459 Some(tok),
460 );
461 }
462 }
463 }
464
465 if !current_word.is_empty() || !current_tagged.is_empty() {
467 process_spacer(
468 &mut current_word,
469 &mut current_tagged,
470 &mut tagged_list,
471 &mut out,
472 &mut bad_count,
473 None,
474 );
475 }
476
477 CleanHtmlResult { line: out, bad_words_count: bad_count }
478 }
479}