rustrict/
replacements.rs

1use crate::feature_cell::FeatureCell;
2use crate::Map;
3use arrayvec::ArrayString;
4use lazy_static::lazy_static;
5use std::collections::hash_map::Entry;
6use std::ops::Deref;
7
8lazy_static! {
9    pub(crate) static ref REPLACEMENTS: FeatureCell<Replacements> = FeatureCell::new(Replacements(
10        include_str!("replacements.csv")
11            .lines()
12            .filter(|line| !line.is_empty())
13            .map(|line| {
14                let comma = line.find(',').unwrap();
15                (
16                    line[..comma].chars().next().unwrap(),
17                    ArrayString::from(&line[comma + 1..]).unwrap(),
18                )
19            })
20            .collect()
21    ));
22}
23
24/// Set of possible interpretations for an input character.
25///
26/// For example, `A` can be replaced with `a` so the word `apple` matches `Apple`.
27#[derive(Clone, Debug)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub struct Replacements(Map<char, ArrayString<12>>);
30
31impl Default for Replacements {
32    fn default() -> Self {
33        REPLACEMENTS.deref().deref().clone()
34    }
35}
36
37impl Replacements {
38    /// Empty.
39    pub fn new() -> Self {
40        Self(Default::default())
41    }
42
43    /// Allows direct mutable access to the global default set of replacements.
44    ///
45    /// Prefer the safe API `Censor::with_replacements`.
46    ///
47    /// # Safety
48    ///
49    /// You must manually avoid concurrent access/censoring.
50    #[cfg(feature = "customize")]
51    #[cfg_attr(doc, doc(cfg(feature = "customize")))]
52    pub unsafe fn customize_default() -> &'static mut Self {
53        REPLACEMENTS.get_mut()
54    }
55
56    pub(crate) fn get(&self, src: char) -> Option<&ArrayString<12>> {
57        self.0.get(&src)
58    }
59
60    /// Adds a new replacement character.
61    ///
62    /// # Panics
63    ///
64    /// Panics if the total replacement characters exceed 12 bytes.
65    pub fn insert(&mut self, src: char, dst: char) {
66        let replacements = self.0.entry(src).or_default();
67        if !replacements.contains(dst) {
68            replacements.push(dst);
69        }
70    }
71
72    /// Removes a replacement character.
73    pub fn remove(&mut self, src: char, dst: char) {
74        if let Entry::Occupied(mut occupied) = self.0.entry(src) {
75            let mut filtered = ArrayString::default();
76            for c in occupied.get().chars() {
77                if c != dst {
78                    filtered.push(c);
79                }
80            }
81            if filtered.is_empty() {
82                occupied.remove();
83            } else {
84                occupied.insert(filtered);
85            }
86        }
87    }
88}