1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
use crate::feature_cell::FeatureCell;
use crate::Map;
use arrayvec::ArrayString;
use lazy_static::lazy_static;
use std::collections::hash_map::Entry;
use std::ops::Deref;

lazy_static! {
    pub(crate) static ref REPLACEMENTS: FeatureCell<Replacements> = FeatureCell::new(Replacements(
        include_str!("replacements.csv")
            .lines()
            .filter(|line| !line.is_empty())
            .map(|line| {
                let comma = line.find(',').unwrap();
                (
                    line[..comma].chars().next().unwrap(),
                    ArrayString::from(&line[comma + 1..]).unwrap(),
                )
            })
            .collect()
    ));
}

/// Set of possible interpretations for an input character.
///
/// For example, `A` can be replaced with `a` so the word `apple` matches `Apple`.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Replacements(Map<char, ArrayString<12>>);

impl Default for Replacements {
    fn default() -> Self {
        REPLACEMENTS.deref().deref().clone()
    }
}

impl Replacements {
    /// Empty.
    pub fn new() -> Self {
        Self(Default::default())
    }

    /// Allows direct mutable access to the global default set of replacements.
    ///
    /// Prefer the safe API `Censor::with_replacements`.
    ///
    /// # Safety
    ///
    /// You must manually avoid concurrent access/censoring.
    #[cfg(feature = "customize")]
    #[cfg_attr(doc, doc(cfg(feature = "customize")))]
    pub unsafe fn customize_default() -> &'static mut Self {
        REPLACEMENTS.get_mut()
    }

    pub(crate) fn get(&self, src: char) -> Option<&ArrayString<12>> {
        self.0.get(&src)
    }

    /// Adds a new replacement character.
    ///
    /// # Panics
    ///
    /// Panics if the total replacement characters exceed 12 bytes.
    pub fn insert(&mut self, src: char, dst: char) {
        let replacements = self.0.entry(src).or_default();
        if !replacements.contains(dst) {
            replacements.push(dst);
        }
    }

    /// Removes a replacement character.
    pub fn remove(&mut self, src: char, dst: char) {
        if let Entry::Occupied(mut occupied) = self.0.entry(src) {
            let mut filtered = ArrayString::default();
            for c in occupied.get().chars() {
                if c != dst {
                    filtered.push(c);
                }
            }
            if filtered.is_empty() {
                occupied.remove();
            } else {
                occupied.insert(filtered);
            }
        }
    }
}