decancer/
string.rs

1use crate::{util::merge_ranges, Matcher};
2#[cfg(feature = "serde")]
3use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
4use std::{
5  fmt::{self, Debug, Display, Formatter},
6  ops::{Deref, Range},
7};
8
9/// A small wrapper around the [`String`] data type for comparison purposes.
10///
11/// This is used because imperfections from translations can happen, thus this is used to provide comparison functions that are not as strict and can detect similar-looking characters (e.g: `i` and `l`)
12#[derive(Clone, Eq, Hash)]
13pub struct CuredString(pub(crate) String);
14
15impl CuredString {
16  /// Iterates throughout this string and yields every similar-looking match.
17  ///
18  /// If you plan on using this method with an array of strings, use [`find_multiple`][CuredString::find_multiple].
19  ///
20  /// This comparison is case-insensitive.
21  ///
22  /// ```rust
23  /// let cured = decancer::cure!("wow hello wow heellllo!").unwrap();
24  /// let mut matcher = cured.find("hello");
25  ///
26  /// assert_eq!(matcher.next(), Some(4..9));
27  /// assert_eq!(matcher.next(), Some(14..22));
28  /// assert_eq!(matcher.next(), None);
29  /// ```
30  #[inline(always)]
31  pub fn find<'a, 'b>(&'a self, other: &'b str) -> Matcher<'a, 'b> {
32    Matcher::new(self, other)
33  }
34
35  /// Iterates throughout this string and returns a [`Vec`] of every similar-looking match. Unlike [`find`][CuredString::find], this method also takes note of overlapping matches and merges them together.
36  ///
37  /// This comparison is case-insensitive.
38  ///
39  /// ```rust
40  /// let cured = decancer::cure!("hꡩ𝔏┕⊕𝚑ᅠΎ⫕ᣲ𑀜").unwrap();
41  /// let matches = cured.find_multiple(["hello", "oh yeah"]);
42  ///
43  /// assert_eq!(matches, [0..11]);
44  /// ```
45  ///
46  /// Usage with the [`censor`](https://docs.rs/censor) crate:
47  ///
48  /// ```rust
49  /// let censor = censor::Standard + censor::Sex;
50  ///
51  /// let cured = decancer::cure!("𝑺ꡘ꡶イ↥⢗ㄒ❘⋶ᔚ").unwrap();
52  /// let matches = cured.find_multiple(censor.set());
53  ///
54  /// assert_eq!(matches, [0..10]);  
55  /// ```
56  pub fn find_multiple<S, O>(&self, other: O) -> Vec<Range<usize>>
57  where
58    S: AsRef<str>,
59    O: IntoIterator<Item = S>,
60  {
61    let other = other.into_iter();
62    let mut ranges = Vec::with_capacity(other.size_hint().0);
63
64    for o in other {
65      ranges.extend(self.find(o.as_ref()));
66    }
67
68    merge_ranges(&mut ranges);
69    ranges
70  }
71
72  fn censor_inner<I>(&mut self, original: &str, matches: I, with: char)
73  where
74    I: IntoIterator<Item = Range<usize>>,
75  {
76    let mut with_str = String::new();
77    let mut char_diff = 0isize;
78
79    for mat in matches {
80      let cap = original[mat.clone()].chars().count() * with.len_utf8();
81
82      with_str.reserve_exact(cap);
83
84      for _ in (with_str.len()..cap).step_by(with.len_utf8()) {
85        with_str.push(with);
86      }
87
88      self.0.replace_range(
89        (mat.start as isize + char_diff) as usize..(mat.end as isize + char_diff) as _,
90        &with_str[..cap],
91      );
92
93      char_diff += cap as isize - mat.len() as isize;
94    }
95  }
96
97  /// Censors every match of a string with a repetition of a character in-place.
98  ///
99  /// If you plan on using this method with an array of strings, use [`censor_multiple`][CuredString::censor_multiple].
100  ///
101  /// This comparison is case-insensitive.
102  ///
103  /// ```rust
104  /// let mut cured = decancer::cure!("wow heellllo wow hello wow!").unwrap();
105  /// cured.censor("hello", '*');
106  ///
107  /// assert_eq!(cured, "wow ******** wow ***** wow!");
108  /// ```
109  pub fn censor(&mut self, other: &str, with: char) {
110    let original = self.clone();
111
112    self.censor_inner(&original, original.find(other), with);
113  }
114
115  /// Censors every matches from an array of strings with a repetition of a character in-place.
116  ///
117  /// This comparison is case-insensitive.
118  ///
119  /// ```rust
120  /// let mut cured = decancer::cure!("ꀡৎレレ⌽ⴙᅠ𝓎ȩ㆟ҥ").unwrap();
121  /// cured.censor_multiple(["hello", "oh yeah"], '*');
122  ///
123  /// assert_eq!(cured, "***********");
124  /// ```
125  ///
126  /// Usage with the [`censor`](https://docs.rs/censor) crate:
127  ///
128  /// ```rust
129  /// let censor = censor::Standard + censor::Sex;
130  ///
131  /// let mut cured = decancer::cure!("𝑺ꡘ꡶イ↥⢗ㄒ❘⋶ᔚ").unwrap();
132  /// cured.censor_multiple(censor.set(), '*');
133  ///
134  /// assert_eq!(cured, "**********");
135  /// ```
136  pub fn censor_multiple<S, O>(&mut self, other: O, with: char)
137  where
138    S: AsRef<str>,
139    O: IntoIterator<Item = S>,
140  {
141    let original = self.clone();
142
143    self.censor_inner(&original, original.find_multiple(other), with);
144  }
145
146  fn replace_inner<I>(&mut self, matches: I, with: &str)
147  where
148    I: IntoIterator<Item = Range<usize>>,
149  {
150    let mut char_diff = 0isize;
151
152    for mat in matches {
153      self.0.replace_range(
154        (mat.start as isize + char_diff) as usize..(mat.end as isize + char_diff) as _,
155        with,
156      );
157
158      char_diff += with.len() as isize - mat.len() as isize;
159    }
160  }
161
162  /// Replaces every match of a string with another string in-place.
163  ///
164  /// If you plan on using this method with an array of strings, use [`replace_multiple`][CuredString::replace_multiple].
165  ///
166  /// This comparison is case-insensitive.
167  ///
168  /// ```rust
169  /// let mut cured = decancer::cure!("wow hello wow heellllo!").unwrap();
170  /// cured.replace("hello", "world");
171  ///
172  /// assert_eq!(cured, "wow world wow world!");
173  /// ```
174  #[inline(always)]
175  pub fn replace(&mut self, other: &str, with: &str) {
176    self.replace_inner(self.clone().find(other), with);
177  }
178
179  /// Replaces every matches from an array of strings with another string in-place.
180  ///
181  /// This comparison is case-insensitive.
182  ///
183  /// ```rust
184  /// let mut cured = decancer::cure!("ꀡৎレレ⌽ⴙᅠ𝓎ȩ㆟ҥ").unwrap();
185  /// cured.replace_multiple(["hello", "oh yeah"], "world");
186  ///
187  /// assert_eq!(cured, "world");
188  /// ```
189  ///
190  /// Usage with the [`censor`](https://docs.rs/censor) crate:
191  ///
192  /// ```rust
193  /// let censor = censor::Standard + censor::Sex;
194  ///
195  /// let mut cured = decancer::cure!("𝑺ꡘ꡶イ↥⢗ㄒ❘⋶ᔚ").unwrap();
196  /// cured.replace_multiple(censor.set(), "no :)");
197  ///
198  /// assert_eq!(cured, "no :)");
199  /// ```
200  #[inline(always)]
201  pub fn replace_multiple<S, O>(&mut self, other: O, with: &str)
202  where
203    S: AsRef<str>,
204    O: IntoIterator<Item = S>,
205  {
206    self.replace_inner(self.clone().find_multiple(other), with);
207  }
208
209  /// Checks if this cured string similarly starts with another string.
210  ///
211  /// This comparison is case-insensitive.
212  pub fn starts_with(&self, other: &str) -> bool {
213    let mut iter = self.find(other);
214    let Some(mat) = iter.next() else {
215      return false;
216    };
217
218    mat.start == 0
219  }
220
221  /// Checks if this cured string similarly ends with another string.
222  ///
223  /// This comparison is case-insensitive.
224  pub fn ends_with(&self, other: &str) -> bool {
225    let Some(last) = self.find(other).last() else {
226      return false;
227    };
228
229    last.end == self.len()
230  }
231
232  /// Checks if this cured string similarly contains another string.
233  ///
234  /// This comparison is case-insensitive.
235  pub fn contains(&self, other: &str) -> bool {
236    let mut iter = self.find(other);
237
238    iter.next().is_some()
239  }
240}
241
242/// Coerces this cured string to a [`String`].
243///
244/// **NOTE:** It's highly **NOT** recommended to use Rust's comparison methods after calling this, and since the string output is laid out in memory the same way as it were to be displayed graphically, displaying it **may not display correctly** since some right-to-left characters are reversed.  
245impl From<CuredString> for String {
246  #[inline(always)]
247  fn from(val: CuredString) -> Self {
248    val.0
249  }
250}
251
252impl AsRef<str> for CuredString {
253  #[inline(always)]
254  fn as_ref(&self) -> &str {
255    &self.0
256  }
257}
258
259/// Checks if this cured string is similar with another string.
260///
261/// This comparison is case-insensitive.
262impl<S> PartialEq<S> for CuredString
263where
264  S: AsRef<str> + ?Sized,
265{
266  #[inline(always)]
267  fn eq(&self, other: &S) -> bool {
268    Matcher::is_equal(self, other.as_ref())
269  }
270}
271
272impl Debug for CuredString {
273  #[inline(always)]
274  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
275    Debug::fmt(&self.0, f)
276  }
277}
278
279impl Display for CuredString {
280  #[inline(always)]
281  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
282    Display::fmt(&self.0, f)
283  }
284}
285
286impl Deref for CuredString {
287  type Target = String;
288
289  #[inline(always)]
290  fn deref(&self) -> &Self::Target {
291    &self.0
292  }
293}
294
295#[cfg(feature = "serde")]
296#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
297impl Serialize for CuredString {
298  #[inline(always)]
299  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
300  where
301    S: Serializer,
302  {
303    serializer.serialize_str(self)
304  }
305}
306
307#[cfg(feature = "serde")]
308#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
309impl<'de> Deserialize<'de> for CuredString {
310  #[inline(always)]
311  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
312  where
313    D: Deserializer<'de>,
314  {
315    Deserialize::deserialize(deserializer)
316      .and_then(|s: &str| crate::cure!(s).map_err(de::Error::custom))
317  }
318}