1use std::collections::HashMap;
2use std::sync::{LazyLock, RwLock};
3
4use regex::Regex;
5use tera::{Filter, Kwargs, State, StringInput, TeraResult, Test, Value};
6
7static STRIPTAGS_RE: LazyLock<Regex> =
8 LazyLock::new(|| Regex::new(r"(<!--.*?-->|<[^>]*>)").unwrap());
9
10static SPACELESS_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r">\s+<").unwrap());
11
12pub fn striptags(val: &str, _: Kwargs, _: &State) -> String {
21 STRIPTAGS_RE.replace_all(val, "").into_owned()
22}
23
24pub fn spaceless(val: StringInput, _: Kwargs, _: &State) -> Value {
34 val.inherit_safety(SPACELESS_RE.replace_all(val.as_str(), "><").into_owned())
37}
38
39fn get_or_create_regex(cache: &RwLock<HashMap<String, Regex>>, pattern: &str) -> TeraResult<Regex> {
40 if let Some(r) = cache.read().unwrap().get(pattern) {
41 return Ok(r.clone());
42 }
43
44 let mut cache = cache.write().unwrap();
45
46 let regex = match Regex::new(pattern) {
47 Ok(regex) => regex,
48 Err(e) => return Err(tera::Error::message(format!("Invalid regex: {e}"))),
49 };
50
51 cache.insert(String::from(pattern), regex.clone());
52 Ok(regex)
53}
54
55#[derive(Debug, Default)]
62pub struct Matching {
63 cache: RwLock<HashMap<String, Regex>>,
64}
65
66impl Test<&str, TeraResult<bool>> for Matching {
67 fn call(&self, val: &str, kwargs: Kwargs, _: &State) -> TeraResult<bool> {
68 let pat = kwargs.must_get::<&str>("pat")?;
69 let regex = get_or_create_regex(&self.cache, pat)?;
70 Ok(regex.is_match(val))
71 }
72}
73
74#[derive(Debug, Default)]
82pub struct RegexReplace {
83 cache: RwLock<HashMap<String, Regex>>,
84}
85
86impl Filter<&str, TeraResult<String>> for RegexReplace {
87 fn call(&self, val: &str, kwargs: Kwargs, _: &State) -> TeraResult<String> {
88 let pattern = kwargs.must_get::<&str>("pattern")?;
89 let rep = kwargs.must_get::<&str>("rep")?;
90 let regex = get_or_create_regex(&self.cache, pattern)?;
91 Ok(regex.replace_all(val, rep).into_owned())
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use std::sync::Arc;
99 use tera::value::Map;
100 use tera::{ArgFromValue, Context, StringInput};
101
102 #[test]
103 fn test_striptags() {
104 let tests = vec![
105 (
106 r"<b>Joel</b> <button>is</button> a <span>slug</span>",
107 "Joel is a slug",
108 ),
109 (
110 r#"<p>just a small \n <a href="x"> example</a> link</p>\n<p>to a webpage</p><!-- <p>and some commented stuff</p> -->"#,
111 r#"just a small \n example link\nto a webpage"#,
112 ),
113 (
114 r"<p>See: 'é is an apostrophe followed by e acute</p>",
115 r"See: 'é is an apostrophe followed by e acute",
116 ),
117 (r"<adf>a", "a"),
118 (r"</adf>a", "a"),
119 (r"<asdf><asdf>e", "e"),
120 (r"hi, <f x", "hi, <f x"),
121 ("234<235, right?", "234<235, right?"),
122 ("a4<a5 right?", "a4<a5 right?"),
123 ("b7>b2!", "b7>b2!"),
124 ("</fe", "</fe"),
125 ("<x>b<y>", "b"),
126 (r#"a<p a >b</p>c"#, "abc"),
127 (r#"d<a:b c:d>e</p>f"#, "def"),
128 (
129 r#"<strong>foo</strong><a href="http://example.com">bar</a>"#,
130 "foobar",
131 ),
132 ("a & b", "a & b"),
133 ];
134 for (input, expected) in tests {
135 let ctx = Context::new();
136 let state = State::new(&ctx);
137 let res = striptags(input, Kwargs::default(), &state);
138 assert_eq!(expected, res);
139 }
140 }
141
142 #[test]
143 fn test_spaceless() {
144 let tests = vec![
145 ("<p>\n<a>test</a>\r\n </p>", "<p><a>test</a></p>"),
146 ("<p>\n<a> </a>\r\n </p>", "<p><a></a></p>"),
147 ("<p> </p>", "<p></p>"),
148 ("<p> <a>", "<p><a>"),
149 ("<p> test</p>", "<p> test</p>"),
150 ("<p>\r\n</p>", "<p></p>"),
151 ];
152 for (input, expected) in tests {
153 let ctx = Context::new();
154 let state = State::new(&ctx);
155 let val = Value::from(input);
156 let res = spaceless(
157 StringInput::from_value(&val).unwrap(),
158 Kwargs::default(),
159 &state,
160 );
161 assert_eq!(expected, res.as_str().unwrap());
162 }
163 }
164
165 #[test]
166 fn test_matching() {
167 let inputs = vec![
168 ("abc", "b", true),
169 ("abc", "^b$", false),
170 ("Hello, World!", r"(?i)(hello\W\sworld\W)", true),
171 ("The date was 2018-06-28", r"\d{4}-\d{2}-\d{2}$", true),
172 ];
173
174 for (input, pat, expected) in inputs {
175 let matching = Matching::default();
176 let mut map = Map::new();
177 map.insert("pat".into(), pat.into());
178 let kwargs = Kwargs::new(Arc::new(map));
179 let ctx = Context::new();
180 let res = matching.call(input, kwargs, &State::new(&ctx)).unwrap();
181 assert_eq!(expected, res);
182 }
183 }
184
185 #[test]
186 fn test_regex_replace() {
187 let regex_replace = RegexReplace::default();
188 let ctx = Context::new();
189 let state = State::new(&ctx);
190
191 let mut map = Map::new();
193 map.insert(
194 "pattern".into(),
195 r"(?P<last>[^,\s]+),\s+(?P<first>\S+)".into(),
196 );
197 map.insert("rep".into(), "$first $last".into());
198 let kwargs = Kwargs::new(Arc::new(map));
199 let result = regex_replace
200 .call("Springsteen, Bruce", kwargs, &state)
201 .unwrap();
202 assert_eq!(result, "Bruce Springsteen");
203
204 let mut map = Map::new();
206 map.insert("pattern".into(), r"\d+".into());
207 map.insert("rep".into(), "X".into());
208 let kwargs = Kwargs::new(Arc::new(map));
209 let result = regex_replace.call("abc123def456", kwargs, &state).unwrap();
210 assert_eq!(result, "abcXdefX");
211
212 let mut map = Map::new();
214 map.insert("pattern".into(), r"zzz".into());
215 map.insert("rep".into(), "X".into());
216 let kwargs = Kwargs::new(Arc::new(map));
217 let result = regex_replace.call("hello world", kwargs, &state).unwrap();
218 assert_eq!(result, "hello world");
219 }
220
221 #[test]
222 fn test_regex_replace_invalid_pattern() {
223 let regex_replace = RegexReplace::default();
224 let ctx = Context::new();
225 let state = State::new(&ctx);
226
227 let mut map = Map::new();
228 map.insert("pattern".into(), r"[invalid".into());
229 map.insert("rep".into(), "X".into());
230 let kwargs = Kwargs::new(Arc::new(map));
231 let result = regex_replace.call("test", kwargs, &state);
232 assert!(result.is_err());
233 }
234
235 #[test]
236 fn test_register() {
237 let mut tera = tera::Tera::default();
238 tera.register_filter("striptags", striptags);
239 tera.register_filter("spaceless", spaceless);
240 tera.register_filter("regex_replace", RegexReplace::default());
241 tera.register_test("matching", Matching::default());
242 }
243}