1use crate::utils::{remove_first_n_chars, remove_last_n_chars};
2use lazy_static::lazy_static;
3use log::error;
4use regex::{Captures, Regex, Replacer};
5use std::cmp::Ordering;
6
7mod utils;
8
9lazy_static! {
10 static ref REGEX_REGEX: Regex = Regex::new(r"[\\b\$\^\[\]\+\*\.]").unwrap();
13}
14
15#[derive(Debug, Clone)]
16pub struct MaybeRegex {
17 data: TagWrapperData,
18 original: String,
19 pub is_negative: bool,
20 case_sensitive: bool,
21}
22
23impl PartialEq for MaybeRegex {
24 fn eq(&self, other: &Self) -> bool {
25 self.original == other.original && self.is_negative == other.is_negative
26 }
27}
28
29impl PartialOrd for MaybeRegex {
30 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
31 (&self.original, self.is_negative).partial_cmp(&(&other.original, other.is_negative))
32 }
33}
34
35#[derive(Debug, Clone)]
36pub enum TagWrapperData {
37 Raw(String),
38 Regex(Regex),
39}
40
41impl MaybeRegex {
42 pub fn new<S: AsRef<str>>(s: S) -> Self {
43 Self::from(s)
44 }
45
46 pub fn from<S: AsRef<str>>(s: S) -> Self {
47 let s = s.as_ref();
48 let (s, is_negative) = if s.starts_with("-") {
49 (remove_first_n_chars(s, 1), true)
50 } else if s.ends_with("-") {
51 (remove_last_n_chars(s, 1), true)
52 } else {
53 (s.into(), false)
54 };
55
56 match get_regex(&s) {
57 Some(regex) => Self {
58 data: TagWrapperData::Regex(regex),
59 original: s,
60 is_negative,
61 case_sensitive: false,
62 },
63 None => Self {
64 data: TagWrapperData::Raw(s.clone()),
65 original: s,
66 is_negative,
67 case_sensitive: false,
68 },
69 }
70 }
71
72 pub fn as_case_sensitive(mut self) -> Self {
73 self.case_sensitive = true;
74 self
75 }
76
77 pub fn is_regex(&self) -> bool {
78 match &self.data {
79 TagWrapperData::Raw(_) => false,
80 TagWrapperData::Regex(_) => true,
81 }
82 }
83
84 pub fn matches<S: AsRef<str>>(&self, haystack: S) -> bool {
85 let matches = self.is_contained_within(haystack);
86 if self.is_negative {
87 return !matches;
88 }
89 matches
90 }
91
92 pub fn is_contained_within<S: AsRef<str>>(&self, haystack: S) -> bool {
95 let haystack = if self.case_sensitive {
96 haystack.as_ref()
97 } else {
98 &haystack.as_ref().to_lowercase()
99 };
100
101 match &self.data {
102 TagWrapperData::Raw(value) => haystack.contains(value),
103 TagWrapperData::Regex(regex) => regex.is_match(haystack),
104 }
105 }
106
107 pub fn replace(&self, str: String, to_string: impl Fn(&str) -> String + 'static) -> String {
108 let mut output = str;
109 match &self.data {
110 TagWrapperData::Raw(value) => {
111 let replacement = to_string(value);
112 output = output.replace(value, &replacement);
113 }
114 TagWrapperData::Regex(regex) => {
115 let highlighter = Highlighter {
116 to_string_cb: Box::new(to_string),
117 };
118
119 output = output.replace("\n", "abcdefg");
121 output = regex.replace_all(&output, highlighter).to_string();
122 output = output.replace("abcdefg", "\n");
123 }
124 };
125 output
126 }
127
128 pub fn to_str(&self) -> &str {
129 self.original.as_str()
130 }
131
132 pub fn to_string(&self) -> String {
133 self.original.clone()
134 }
135
136 pub fn match_indices<S: AsRef<str>>(&self, other: S) -> Vec<(usize, usize)> {
137 let other = if self.case_sensitive {
138 other.as_ref()
139 } else {
140 &other.as_ref().to_lowercase()
141 };
142
143 match &self.data {
144 TagWrapperData::Raw(value) => other
145 .match_indices(value)
146 .map(|(index, _)| (index, value.len()))
147 .collect(),
148 TagWrapperData::Regex(regex) => regex
149 .find_iter(other)
150 .map(|some_match| (some_match.start(), some_match.len()))
151 .collect(),
152 }
153 }
154
155 pub fn matches_exactly<S: AsRef<str>>(&self, other: S) -> bool {
156 let other = if self.case_sensitive {
157 other.as_ref()
158 } else {
159 &other.as_ref().to_lowercase()
160 };
161
162 match &self.data {
163 TagWrapperData::Raw(value) => other == *value,
164 TagWrapperData::Regex(regex) => {
165 if let Some(found) = regex.find(other) {
166 return found.len() == other.len();
167 }
168 false
169 }
170 }
171 }
172
173 pub fn starts_with<S: AsRef<str>>(&self, s: S) -> bool {
174 let s = if self.case_sensitive {
175 s.as_ref()
176 } else {
177 &s.as_ref().to_lowercase()
178 };
179
180 match &self.data {
181 TagWrapperData::Raw(value) => value.starts_with(s),
182 TagWrapperData::Regex(regex) => {
183 if let Some(found) = regex.find(s) {
184 return found.start() == 0;
185 }
186 false
187 }
188 }
189 }
190}
191
192fn get_regex(s: &str) -> Option<Regex> {
193 if REGEX_REGEX.is_match(s) {
194 match Regex::new(s) {
195 Ok(regex) => {
196 return Some(regex);
197 }
198 Err(_e) => {
199 error!("Bad regex: {s}");
200 }
201 }
202 }
203 None
204}
205
206struct Highlighter {
207 to_string_cb: Box<dyn Fn(&str) -> String>,
208}
209
210impl Replacer for Highlighter {
211 fn replace_append(&mut self, caps: &Captures<'_>, dst: &mut String) {
212 let temp = caps.get(0).map_or("", |m| m.as_str()).to_string();
213 let rv = (*self.to_string_cb)(&temp);
214 dst.push_str(&rv);
215 }
216}
217
218#[cfg(test)]
219mod test {
220 use super::*;
221
222 #[test]
223 fn detects_regexes() {
224 assert!(MaybeRegex::new("This is a regex.*").is_regex());
225 assert!(MaybeRegex::new(".*This is a regex").is_regex());
226 assert!(MaybeRegex::new(".This is a regex").is_regex());
227 assert!(MaybeRegex::new("This is a regex [0-9]").is_regex());
228 }
229
230 #[test]
231 fn detects_non_regexes() {
232 assert!(!MaybeRegex::new("This is not a regex").is_regex());
233 assert!(!MaybeRegex::new("This is not a regex?").is_regex());
234 assert!(!MaybeRegex::new("This is not a regex [").is_regex());
235 assert!(!MaybeRegex::new("This is not a regex [0-9").is_regex());
236 }
237
238 #[test]
239 fn contains_works() {
240 assert!(!MaybeRegex::new("z").is_contained_within("Hello"));
241 assert!(!MaybeRegex::new("e$").is_contained_within("Hello"));
242
243 assert!(MaybeRegex::new("e").is_contained_within("Hello"));
244 assert!(MaybeRegex::new("o$").is_contained_within("Hello"));
245 }
246
247 #[test]
248 fn negative_works() {
249 assert!(MaybeRegex::new("-e").is_contained_within("Hello"));
250 assert!(!MaybeRegex::new("-e").matches("Hello"));
251
252 assert!(MaybeRegex::new("-o$").is_contained_within("Hello"));
253 assert!(!MaybeRegex::new("-o$").matches("Hello"));
254 }
255
256 #[test]
257 fn all_string_types_work() {
258 assert!(MaybeRegex::new("e").is_contained_within("Hello"));
259 assert!(MaybeRegex::new(String::from("e")).is_contained_within("Hello"));
260 assert!(MaybeRegex::new(&String::from("e")).is_contained_within("Hello"));
261 }
262}