tera/builtins/filters/
string.rs

1/// Filters operating on string
2use std::collections::HashMap;
3
4use lazy_static::lazy_static;
5use regex::{Captures, Regex};
6use serde_json::value::{to_value, Value};
7use unic_segment::GraphemeIndices;
8
9#[cfg(feature = "urlencode")]
10use percent_encoding::{percent_encode, AsciiSet, NON_ALPHANUMERIC};
11
12use crate::errors::{Error, Result};
13use crate::utils;
14
15/// https://url.spec.whatwg.org/#fragment-percent-encode-set
16#[cfg(feature = "urlencode")]
17const FRAGMENT_ENCODE_SET: &AsciiSet =
18    &percent_encoding::CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
19
20/// https://url.spec.whatwg.org/#path-percent-encode-set
21#[cfg(feature = "urlencode")]
22const PATH_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'#').add(b'?').add(b'{').add(b'}');
23
24/// https://url.spec.whatwg.org/#userinfo-percent-encode-set
25#[cfg(feature = "urlencode")]
26const USERINFO_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET
27    .add(b'/')
28    .add(b':')
29    .add(b';')
30    .add(b'=')
31    .add(b'@')
32    .add(b'[')
33    .add(b'\\')
34    .add(b']')
35    .add(b'^')
36    .add(b'|');
37
38/// Same as Python quote
39/// https://github.com/python/cpython/blob/da27d9b9dc44913ffee8f28d9638985eaaa03755/Lib/urllib/parse.py#L787
40/// with `/` not escaped
41#[cfg(feature = "urlencode")]
42const PYTHON_ENCODE_SET: &AsciiSet = &USERINFO_ENCODE_SET
43    .remove(b'/')
44    .add(b':')
45    .add(b'?')
46    .add(b'#')
47    .add(b'[')
48    .add(b']')
49    .add(b'@')
50    .add(b'!')
51    .add(b'$')
52    .add(b'&')
53    .add(b'\'')
54    .add(b'(')
55    .add(b')')
56    .add(b'*')
57    .add(b'+')
58    .add(b',')
59    .add(b';')
60    .add(b'=');
61
62lazy_static! {
63    static ref STRIPTAGS_RE: Regex = Regex::new(r"(<!--.*?-->|<[^>]*>)").unwrap();
64    static ref WORDS_RE: Regex = Regex::new(r"\b(?P<first>[\w'])(?P<rest>[\w']*)\b").unwrap();
65    static ref SPACELESS_RE: Regex = Regex::new(r">\s+<").unwrap();
66}
67
68/// Convert a value to uppercase.
69pub fn upper(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
70    let s = try_get_value!("upper", "value", String, value);
71
72    Ok(to_value(s.to_uppercase()).unwrap())
73}
74
75/// Convert a value to lowercase.
76pub fn lower(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
77    let s = try_get_value!("lower", "value", String, value);
78
79    Ok(to_value(s.to_lowercase()).unwrap())
80}
81
82/// Strip leading and trailing whitespace.
83pub fn trim(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
84    let s = try_get_value!("trim", "value", String, value);
85
86    Ok(to_value(s.trim()).unwrap())
87}
88
89/// Strip leading whitespace.
90pub fn trim_start(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
91    let s = try_get_value!("trim_start", "value", String, value);
92
93    Ok(to_value(s.trim_start()).unwrap())
94}
95
96/// Strip trailing whitespace.
97pub fn trim_end(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
98    let s = try_get_value!("trim_end", "value", String, value);
99
100    Ok(to_value(s.trim_end()).unwrap())
101}
102
103/// Strip leading characters that match the given pattern.
104pub fn trim_start_matches(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
105    let s = try_get_value!("trim_start_matches", "value", String, value);
106
107    let pat = match args.get("pat") {
108        Some(pat) => {
109            let p = try_get_value!("trim_start_matches", "pat", String, pat);
110            // When reading from a file, it will escape `\n` to `\\n` for example so we need
111            // to replace double escape. In practice it might cause issues if someone wants to split
112            // by `\\n` for real but that seems pretty unlikely
113            p.replace("\\n", "\n").replace("\\t", "\t")
114        }
115        None => return Err(Error::msg("Filter `trim_start_matches` expected an arg called `pat`")),
116    };
117
118    Ok(to_value(s.trim_start_matches(&pat)).unwrap())
119}
120
121/// Strip trailing characters that match the given pattern.
122pub fn trim_end_matches(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
123    let s = try_get_value!("trim_end_matches", "value", String, value);
124
125    let pat = match args.get("pat") {
126        Some(pat) => {
127            let p = try_get_value!("trim_end_matches", "pat", String, pat);
128            // When reading from a file, it will escape `\n` to `\\n` for example so we need
129            // to replace double escape. In practice it might cause issues if someone wants to split
130            // by `\\n` for real but that seems pretty unlikely
131            p.replace("\\n", "\n").replace("\\t", "\t")
132        }
133        None => return Err(Error::msg("Filter `trim_end_matches` expected an arg called `pat`")),
134    };
135
136    Ok(to_value(s.trim_end_matches(&pat)).unwrap())
137}
138
139/// Truncates a string to the indicated length.
140///
141/// # Arguments
142///
143/// * `value`   - The string that needs to be truncated.
144/// * `args`    - A set of key/value arguments that can take the following
145///   keys.
146/// * `length`  - The length at which the string needs to be truncated. If
147///   the length is larger than the length of the string, the string is
148///   returned untouched. The default value is 255.
149/// * `end`     - The ellipsis string to be used if the given string is
150///   truncated. The default value is "…".
151///
152/// # Remarks
153///
154/// The return value of this function might be longer than `length`: the `end`
155/// string is *added* after the truncation occurs.
156///
157pub fn truncate(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
158    let s = try_get_value!("truncate", "value", String, value);
159    let length = match args.get("length") {
160        Some(l) => try_get_value!("truncate", "length", usize, l),
161        None => 255,
162    };
163    let end = match args.get("end") {
164        Some(l) => try_get_value!("truncate", "end", String, l),
165        None => "…".to_string(),
166    };
167
168    let graphemes = GraphemeIndices::new(&s).collect::<Vec<(usize, &str)>>();
169
170    // Nothing to truncate?
171    if length >= graphemes.len() {
172        return Ok(to_value(&s).unwrap());
173    }
174
175    let result = s[..graphemes[length].0].to_string() + &end;
176    Ok(to_value(result).unwrap())
177}
178
179/// Gets the number of words in a string.
180pub fn wordcount(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
181    let s = try_get_value!("wordcount", "value", String, value);
182
183    Ok(to_value(s.split_whitespace().count()).unwrap())
184}
185
186/// Replaces given `from` substring with `to` string.
187pub fn replace(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
188    let s = try_get_value!("replace", "value", String, value);
189
190    let from = match args.get("from") {
191        Some(val) => try_get_value!("replace", "from", String, val),
192        None => return Err(Error::msg("Filter `replace` expected an arg called `from`")),
193    };
194
195    let to = match args.get("to") {
196        Some(val) => try_get_value!("replace", "to", String, val),
197        None => return Err(Error::msg("Filter `replace` expected an arg called `to`")),
198    };
199
200    Ok(to_value(s.replace(&from, &to)).unwrap())
201}
202
203/// First letter of the string is uppercase rest is lowercase
204pub fn capitalize(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
205    let s = try_get_value!("capitalize", "value", String, value);
206    let mut chars = s.chars();
207    match chars.next() {
208        None => Ok(to_value("").unwrap()),
209        Some(f) => {
210            let res = f.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase();
211            Ok(to_value(res).unwrap())
212        }
213    }
214}
215
216/// Percent-encodes reserved URI characters
217#[cfg(feature = "urlencode")]
218pub fn urlencode(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
219    let s = try_get_value!("urlencode", "value", String, value);
220    let encoded = percent_encode(s.as_bytes(), PYTHON_ENCODE_SET).to_string();
221    Ok(Value::String(encoded))
222}
223
224/// Percent-encodes all non-alphanumeric characters
225#[cfg(feature = "urlencode")]
226pub fn urlencode_strict(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
227    let s = try_get_value!("urlencode_strict", "value", String, value);
228    let encoded = percent_encode(s.as_bytes(), NON_ALPHANUMERIC).to_string();
229    Ok(Value::String(encoded))
230}
231
232/// Escapes quote characters
233pub fn addslashes(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
234    let s = try_get_value!("addslashes", "value", String, value);
235    Ok(to_value(s.replace('\\', "\\\\").replace('\"', "\\\"").replace('\'', "\\\'")).unwrap())
236}
237
238/// Transform a string into a slug
239#[cfg(feature = "builtins")]
240pub fn slugify(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
241    let s = try_get_value!("slugify", "value", String, value);
242    Ok(to_value(slug::slugify(s)).unwrap())
243}
244
245/// Capitalizes each word in the string
246pub fn title(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
247    let s = try_get_value!("title", "value", String, value);
248
249    Ok(to_value(WORDS_RE.replace_all(&s, |caps: &Captures| {
250        let first = caps["first"].to_uppercase();
251        let rest = caps["rest"].to_lowercase();
252        format!("{}{}", first, rest)
253    }))
254    .unwrap())
255}
256
257/// Convert line breaks (`\n` or `\r\n`) to HTML linebreaks (`<br>`).
258///
259/// Example: The input "Hello\nWorld" turns into "Hello<br>World".
260pub fn linebreaksbr(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
261    let s = try_get_value!("linebreaksbr", "value", String, value);
262    Ok(to_value(s.replace("\r\n", "<br>").replace('\n', "<br>")).unwrap())
263}
264
265/// Indents a string by the specified width.
266///
267/// # Arguments
268///
269/// * `value`   - The string to indent.
270/// * `args`    - A set of key/value arguments that can take the following
271///   keys.
272/// * `prefix`  - The prefix used for indentation. The default value is 4 spaces.
273/// * `first`  - True indents the first line.  The default is false.
274/// * `blank`  - True indents blank lines.  The default is false.
275///
276pub fn indent(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
277    let s = try_get_value!("indent", "value", String, value);
278
279    let prefix = match args.get("prefix") {
280        Some(p) => try_get_value!("indent", "prefix", String, p),
281        None => "    ".to_string(),
282    };
283    let first = match args.get("first") {
284        Some(f) => try_get_value!("indent", "first", bool, f),
285        None => false,
286    };
287    let blank = match args.get("blank") {
288        Some(b) => try_get_value!("indent", "blank", bool, b),
289        None => false,
290    };
291
292    // Attempt to pre-allocate enough space to prevent additional allocations/copies
293    let mut out = String::with_capacity(
294        s.len() + (prefix.len() * (s.chars().filter(|&c| c == '\n').count() + 1)),
295    );
296    let mut first_pass = true;
297
298    for line in s.lines() {
299        if first_pass {
300            if first {
301                out.push_str(&prefix);
302            }
303            first_pass = false;
304        } else {
305            out.push('\n');
306            if blank || !line.trim_start().is_empty() {
307                out.push_str(&prefix);
308            }
309        }
310        out.push_str(line);
311    }
312
313    Ok(to_value(&out).unwrap())
314}
315
316/// Removes html tags from string
317pub fn striptags(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
318    let s = try_get_value!("striptags", "value", String, value);
319    Ok(to_value(STRIPTAGS_RE.replace_all(&s, "")).unwrap())
320}
321
322/// Removes spaces between html tags from string
323pub fn spaceless(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
324    let s = try_get_value!("spaceless", "value", String, value);
325    Ok(to_value(SPACELESS_RE.replace_all(&s, "><")).unwrap())
326}
327
328/// Returns the given text with all special HTML characters encoded
329pub fn escape_html(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
330    let s = try_get_value!("escape_html", "value", String, value);
331    Ok(Value::String(utils::escape_html(&s)))
332}
333
334/// Returns the given text with all special XML characters encoded
335/// Very similar to `escape_html`, just a few characters less are encoded
336pub fn escape_xml(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
337    let s = try_get_value!("escape_html", "value", String, value);
338
339    let mut output = String::with_capacity(s.len() * 2);
340    for c in s.chars() {
341        match c {
342            '&' => output.push_str("&amp;"),
343            '<' => output.push_str("&lt;"),
344            '>' => output.push_str("&gt;"),
345            '"' => output.push_str("&quot;"),
346            '\'' => output.push_str("&apos;"),
347            _ => output.push(c),
348        }
349    }
350    Ok(Value::String(output))
351}
352
353/// Split the given string by the given pattern.
354pub fn split(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
355    let s = try_get_value!("split", "value", String, value);
356
357    let pat = match args.get("pat") {
358        Some(pat) => {
359            let p = try_get_value!("split", "pat", String, pat);
360            // When reading from a file, it will escape `\n` to `\\n` for example so we need
361            // to replace double escape. In practice it might cause issues if someone wants to split
362            // by `\\n` for real but that seems pretty unlikely
363            p.replace("\\n", "\n").replace("\\t", "\t")
364        }
365        None => return Err(Error::msg("Filter `split` expected an arg called `pat`")),
366    };
367
368    Ok(to_value(s.split(&pat).collect::<Vec<_>>()).unwrap())
369}
370
371/// Convert the value to a signed integer number
372pub fn int(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
373    let default = match args.get("default") {
374        Some(d) => try_get_value!("int", "default", i64, d),
375        None => 0,
376    };
377    let base = match args.get("base") {
378        Some(b) => try_get_value!("int", "base", u32, b),
379        None => 10,
380    };
381
382    let v = match value {
383        Value::String(s) => {
384            let s = s.trim();
385            let s = match base {
386                2 => s.trim_start_matches("0b"),
387                8 => s.trim_start_matches("0o"),
388                16 => s.trim_start_matches("0x"),
389                _ => s,
390            };
391
392            match i64::from_str_radix(s, base) {
393                Ok(v) => v,
394                Err(_) => {
395                    if s.contains('.') {
396                        match s.parse::<f64>() {
397                            Ok(f) => f as i64,
398                            Err(_) => default,
399                        }
400                    } else {
401                        default
402                    }
403                }
404            }
405        }
406        Value::Number(n) => match n.as_f64() {
407            Some(f) => f as i64,
408            None => match n.as_i64() {
409                Some(i) => i,
410                None => default,
411            },
412        },
413        _ => return Err(Error::msg("Filter `int` received an unexpected type")),
414    };
415
416    Ok(to_value(v).unwrap())
417}
418
419/// Convert the value to a floating point number
420pub fn float(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
421    let default = match args.get("default") {
422        Some(d) => try_get_value!("float", "default", f64, d),
423        None => 0.0,
424    };
425
426    let v = match value {
427        Value::String(s) => {
428            let s = s.trim();
429            s.parse::<f64>().unwrap_or(default)
430        }
431        Value::Number(n) => match n.as_f64() {
432            Some(f) => f,
433            None => match n.as_i64() {
434                Some(i) => i as f64,
435                None => default,
436            },
437        },
438        _ => return Err(Error::msg("Filter `float` received an unexpected type")),
439    };
440
441    Ok(to_value(v).unwrap())
442}
443
444#[cfg(test)]
445mod tests {
446    use std::collections::HashMap;
447
448    use serde_json::value::to_value;
449
450    use super::*;
451
452    #[test]
453    fn test_upper() {
454        let result = upper(&to_value("hello").unwrap(), &HashMap::new());
455        assert!(result.is_ok());
456        assert_eq!(result.unwrap(), to_value("HELLO").unwrap());
457    }
458
459    #[test]
460    fn test_upper_error() {
461        let result = upper(&to_value(50).unwrap(), &HashMap::new());
462        assert!(result.is_err());
463        assert_eq!(
464            result.err().unwrap().to_string(),
465            "Filter `upper` was called on an incorrect value: got `50` but expected a String"
466        );
467    }
468
469    #[test]
470    fn test_trim() {
471        let result = trim(&to_value("  hello  ").unwrap(), &HashMap::new());
472        assert!(result.is_ok());
473        assert_eq!(result.unwrap(), to_value("hello").unwrap());
474    }
475
476    #[test]
477    fn test_trim_start() {
478        let result = trim_start(&to_value("  hello  ").unwrap(), &HashMap::new());
479        assert!(result.is_ok());
480        assert_eq!(result.unwrap(), to_value("hello  ").unwrap());
481    }
482
483    #[test]
484    fn test_trim_end() {
485        let result = trim_end(&to_value("  hello  ").unwrap(), &HashMap::new());
486        assert!(result.is_ok());
487        assert_eq!(result.unwrap(), to_value("  hello").unwrap());
488    }
489
490    #[test]
491    fn test_trim_start_matches() {
492        let tests: Vec<(_, _, _)> = vec![
493            ("/a/b/cde/", "/", "a/b/cde/"),
494            ("\nhello\nworld\n", "\n", "hello\nworld\n"),
495            (", hello, world, ", ", ", "hello, world, "),
496        ];
497        for (input, pat, expected) in tests {
498            let mut args = HashMap::new();
499            args.insert("pat".to_string(), to_value(pat).unwrap());
500            let result = trim_start_matches(&to_value(input).unwrap(), &args);
501            assert!(result.is_ok());
502            assert_eq!(result.unwrap(), to_value(expected).unwrap());
503        }
504    }
505
506    #[test]
507    fn test_trim_end_matches() {
508        let tests: Vec<(_, _, _)> = vec![
509            ("/a/b/cde/", "/", "/a/b/cde"),
510            ("\nhello\nworld\n", "\n", "\nhello\nworld"),
511            (", hello, world, ", ", ", ", hello, world"),
512        ];
513        for (input, pat, expected) in tests {
514            let mut args = HashMap::new();
515            args.insert("pat".to_string(), to_value(pat).unwrap());
516            let result = trim_end_matches(&to_value(input).unwrap(), &args);
517            assert!(result.is_ok());
518            assert_eq!(result.unwrap(), to_value(expected).unwrap());
519        }
520    }
521
522    #[test]
523    fn test_truncate_smaller_than_length() {
524        let mut args = HashMap::new();
525        args.insert("length".to_string(), to_value(255).unwrap());
526        let result = truncate(&to_value("hello").unwrap(), &args);
527        assert!(result.is_ok());
528        assert_eq!(result.unwrap(), to_value("hello").unwrap());
529    }
530
531    #[test]
532    fn test_truncate_when_required() {
533        let mut args = HashMap::new();
534        args.insert("length".to_string(), to_value(2).unwrap());
535        let result = truncate(&to_value("日本語").unwrap(), &args);
536        assert!(result.is_ok());
537        assert_eq!(result.unwrap(), to_value("日本…").unwrap());
538    }
539
540    #[test]
541    fn test_truncate_custom_end() {
542        let mut args = HashMap::new();
543        args.insert("length".to_string(), to_value(2).unwrap());
544        args.insert("end".to_string(), to_value("").unwrap());
545        let result = truncate(&to_value("日本語").unwrap(), &args);
546        assert!(result.is_ok());
547        assert_eq!(result.unwrap(), to_value("日本").unwrap());
548    }
549
550    #[test]
551    fn test_truncate_multichar_grapheme() {
552        let mut args = HashMap::new();
553        args.insert("length".to_string(), to_value(5).unwrap());
554        args.insert("end".to_string(), to_value("…").unwrap());
555        let result = truncate(&to_value("👨‍👩‍👧‍👦 family").unwrap(), &args);
556        assert!(result.is_ok());
557        assert_eq!(result.unwrap(), to_value("👨‍👩‍👧‍👦 fam…").unwrap());
558    }
559
560    #[test]
561    fn test_lower() {
562        let result = lower(&to_value("HELLO").unwrap(), &HashMap::new());
563        assert!(result.is_ok());
564        assert_eq!(result.unwrap(), to_value("hello").unwrap());
565    }
566
567    #[test]
568    fn test_wordcount() {
569        let result = wordcount(&to_value("Joel is a slug").unwrap(), &HashMap::new());
570        assert!(result.is_ok());
571        assert_eq!(result.unwrap(), to_value(4).unwrap());
572    }
573
574    #[test]
575    fn test_replace() {
576        let mut args = HashMap::new();
577        args.insert("from".to_string(), to_value("Hello").unwrap());
578        args.insert("to".to_string(), to_value("Goodbye").unwrap());
579        let result = replace(&to_value("Hello world!").unwrap(), &args);
580        assert!(result.is_ok());
581        assert_eq!(result.unwrap(), to_value("Goodbye world!").unwrap());
582    }
583
584    // https://github.com/Keats/tera/issues/435
585    #[test]
586    fn test_replace_newline() {
587        let mut args = HashMap::new();
588        args.insert("from".to_string(), to_value("\n").unwrap());
589        args.insert("to".to_string(), to_value("<br>").unwrap());
590        let result = replace(&to_value("Animal Alphabets\nB is for Bee-Eater").unwrap(), &args);
591        assert!(result.is_ok());
592        assert_eq!(result.unwrap(), to_value("Animal Alphabets<br>B is for Bee-Eater").unwrap());
593    }
594
595    #[test]
596    fn test_replace_missing_arg() {
597        let mut args = HashMap::new();
598        args.insert("from".to_string(), to_value("Hello").unwrap());
599        let result = replace(&to_value("Hello world!").unwrap(), &args);
600        assert!(result.is_err());
601        assert_eq!(
602            result.err().unwrap().to_string(),
603            "Filter `replace` expected an arg called `to`"
604        );
605    }
606
607    #[test]
608    fn test_capitalize() {
609        let tests = vec![("CAPITAL IZE", "Capital ize"), ("capital ize", "Capital ize")];
610        for (input, expected) in tests {
611            let result = capitalize(&to_value(input).unwrap(), &HashMap::new());
612            assert!(result.is_ok());
613            assert_eq!(result.unwrap(), to_value(expected).unwrap());
614        }
615    }
616
617    #[test]
618    fn test_addslashes() {
619        let tests = vec![
620            (r#"I'm so happy"#, r#"I\'m so happy"#),
621            (r#"Let "me" help you"#, r#"Let \"me\" help you"#),
622            (r#"<a>'"#, r#"<a>\'"#),
623            (
624                r#""double quotes" and \'single quotes\'"#,
625                r#"\"double quotes\" and \\\'single quotes\\\'"#,
626            ),
627            (r#"\ : backslashes too"#, r#"\\ : backslashes too"#),
628        ];
629        for (input, expected) in tests {
630            let result = addslashes(&to_value(input).unwrap(), &HashMap::new());
631            assert!(result.is_ok());
632            assert_eq!(result.unwrap(), to_value(expected).unwrap());
633        }
634    }
635
636    #[cfg(feature = "builtins")]
637    #[test]
638    fn test_slugify() {
639        // slug crate already has tests for general slugification so we just
640        // check our function works
641        let tests =
642            vec![(r#"Hello world"#, r#"hello-world"#), (r#"Hello 世界"#, r#"hello-shi-jie"#)];
643        for (input, expected) in tests {
644            let result = slugify(&to_value(input).unwrap(), &HashMap::new());
645            assert!(result.is_ok());
646            assert_eq!(result.unwrap(), to_value(expected).unwrap());
647        }
648    }
649
650    #[cfg(feature = "urlencode")]
651    #[test]
652    fn test_urlencode() {
653        let tests = vec![
654            (
655                r#"https://www.example.org/foo?a=b&c=d"#,
656                r#"https%3A//www.example.org/foo%3Fa%3Db%26c%3Dd"#,
657            ),
658            (
659                r#"https://www.example.org/apples-&-oranges/"#,
660                r#"https%3A//www.example.org/apples-%26-oranges/"#,
661            ),
662            (r#"https://www.example.org/"#, r#"https%3A//www.example.org/"#),
663            (r#"/test&"/me?/"#, r#"/test%26%22/me%3F/"#),
664            (r#"escape/slash"#, r#"escape/slash"#),
665        ];
666        for (input, expected) in tests {
667            let args = HashMap::new();
668            let result = urlencode(&to_value(input).unwrap(), &args);
669            assert!(result.is_ok());
670            assert_eq!(result.unwrap(), to_value(expected).unwrap());
671        }
672    }
673
674    #[cfg(feature = "urlencode")]
675    #[test]
676    fn test_urlencode_strict() {
677        let tests = vec![
678            (
679                r#"https://www.example.org/foo?a=b&c=d"#,
680                r#"https%3A%2F%2Fwww%2Eexample%2Eorg%2Ffoo%3Fa%3Db%26c%3Dd"#,
681            ),
682            (
683                r#"https://www.example.org/apples-&-oranges/"#,
684                r#"https%3A%2F%2Fwww%2Eexample%2Eorg%2Fapples%2D%26%2Doranges%2F"#,
685            ),
686            (r#"https://www.example.org/"#, r#"https%3A%2F%2Fwww%2Eexample%2Eorg%2F"#),
687            (r#"/test&"/me?/"#, r#"%2Ftest%26%22%2Fme%3F%2F"#),
688            (r#"escape/slash"#, r#"escape%2Fslash"#),
689        ];
690        for (input, expected) in tests {
691            let args = HashMap::new();
692            let result = urlencode_strict(&to_value(input).unwrap(), &args);
693            assert!(result.is_ok());
694            assert_eq!(result.unwrap(), to_value(expected).unwrap());
695        }
696    }
697
698    #[test]
699    fn test_title() {
700        let tests = vec![
701            ("foo bar", "Foo Bar"),
702            ("foo\tbar", "Foo\tBar"),
703            ("foo  bar", "Foo  Bar"),
704            ("f bar f", "F Bar F"),
705            ("foo-bar", "Foo-Bar"),
706            ("FOO\tBAR", "Foo\tBar"),
707            ("foo (bar)", "Foo (Bar)"),
708            ("foo (bar) ", "Foo (Bar) "),
709            ("foo {bar}", "Foo {Bar}"),
710            ("foo [bar]", "Foo [Bar]"),
711            ("foo <bar>", "Foo <Bar>"),
712            ("  foo  bar", "  Foo  Bar"),
713            ("\tfoo\tbar\t", "\tFoo\tBar\t"),
714            ("foo bar ", "Foo Bar "),
715            ("foo bar\t", "Foo Bar\t"),
716            ("foo's bar", "Foo's Bar"),
717        ];
718        for (input, expected) in tests {
719            let result = title(&to_value(input).unwrap(), &HashMap::new());
720            assert!(result.is_ok());
721            assert_eq!(result.unwrap(), to_value(expected).unwrap());
722        }
723    }
724
725    #[test]
726    fn test_indent_defaults() {
727        let args = HashMap::new();
728        let result = indent(&to_value("one\n\ntwo\nthree").unwrap(), &args);
729        assert!(result.is_ok());
730        assert_eq!(result.unwrap(), to_value("one\n\n    two\n    three").unwrap());
731    }
732
733    #[test]
734    fn test_indent_args() {
735        let mut args = HashMap::new();
736        args.insert("first".to_string(), to_value(true).unwrap());
737        args.insert("prefix".to_string(), to_value(" ").unwrap());
738        args.insert("blank".to_string(), to_value(true).unwrap());
739        let result = indent(&to_value("one\n\ntwo\nthree").unwrap(), &args);
740        assert!(result.is_ok());
741        assert_eq!(result.unwrap(), to_value(" one\n \n two\n three").unwrap());
742    }
743
744    #[test]
745    fn test_striptags() {
746        let tests = vec![
747            (r"<b>Joel</b> <button>is</button> a <span>slug</span>", "Joel is a slug"),
748            (
749                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> -->"#,
750                r#"just a small   \n  example link\nto a webpage"#,
751            ),
752            (
753                r"<p>See: &#39;&eacute; is an apostrophe followed by e acute</p>",
754                r"See: &#39;&eacute; is an apostrophe followed by e acute",
755            ),
756            (r"<adf>a", "a"),
757            (r"</adf>a", "a"),
758            (r"<asdf><asdf>e", "e"),
759            (r"hi, <f x", "hi, <f x"),
760            ("234<235, right?", "234<235, right?"),
761            ("a4<a5 right?", "a4<a5 right?"),
762            ("b7>b2!", "b7>b2!"),
763            ("</fe", "</fe"),
764            ("<x>b<y>", "b"),
765            (r#"a<p a >b</p>c"#, "abc"),
766            (r#"d<a:b c:d>e</p>f"#, "def"),
767            (r#"<strong>foo</strong><a href="http://example.com">bar</a>"#, "foobar"),
768        ];
769        for (input, expected) in tests {
770            let result = striptags(&to_value(input).unwrap(), &HashMap::new());
771            assert!(result.is_ok());
772            assert_eq!(result.unwrap(), to_value(expected).unwrap());
773        }
774    }
775
776    #[test]
777    fn test_spaceless() {
778        let tests = vec![
779            ("<p>\n<a>test</a>\r\n </p>", "<p><a>test</a></p>"),
780            ("<p>\n<a> </a>\r\n </p>", "<p><a></a></p>"),
781            ("<p> </p>", "<p></p>"),
782            ("<p> <a>", "<p><a>"),
783            ("<p> test</p>", "<p> test</p>"),
784            ("<p>\r\n</p>", "<p></p>"),
785        ];
786        for (input, expected) in tests {
787            let result = spaceless(&to_value(input).unwrap(), &HashMap::new());
788            assert!(result.is_ok());
789            assert_eq!(result.unwrap(), to_value(expected).unwrap());
790        }
791    }
792
793    #[test]
794    fn test_split() {
795        let tests: Vec<(_, _, &[&str])> = vec![
796            ("a/b/cde", "/", &["a", "b", "cde"]),
797            ("hello\nworld", "\n", &["hello", "world"]),
798            ("hello, world", ", ", &["hello", "world"]),
799        ];
800        for (input, pat, expected) in tests {
801            let mut args = HashMap::new();
802            args.insert("pat".to_string(), to_value(pat).unwrap());
803            let result = split(&to_value(input).unwrap(), &args).unwrap();
804            let result = result.as_array().unwrap();
805            assert_eq!(result.len(), expected.len());
806            for (result, expected) in result.iter().zip(expected.iter()) {
807                assert_eq!(result, expected);
808            }
809        }
810    }
811
812    #[test]
813    fn test_xml_escape() {
814        let tests = vec![
815            (r"hey-&-ho", "hey-&amp;-ho"),
816            (r"hey-'-ho", "hey-&apos;-ho"),
817            (r"hey-&'-ho", "hey-&amp;&apos;-ho"),
818            (r#"hey-&'"-ho"#, "hey-&amp;&apos;&quot;-ho"),
819            (r#"hey-&'"<-ho"#, "hey-&amp;&apos;&quot;&lt;-ho"),
820            (r#"hey-&'"<>-ho"#, "hey-&amp;&apos;&quot;&lt;&gt;-ho"),
821        ];
822        for (input, expected) in tests {
823            let result = escape_xml(&to_value(input).unwrap(), &HashMap::new());
824            assert!(result.is_ok());
825            assert_eq!(result.unwrap(), to_value(expected).unwrap());
826        }
827    }
828
829    #[test]
830    fn test_int_decimal_strings() {
831        let tests: Vec<(&str, i64)> = vec![
832            ("0", 0),
833            ("-5", -5),
834            ("9223372036854775807", i64::max_value()),
835            ("0b1010", 0),
836            ("1.23", 1),
837        ];
838        for (input, expected) in tests {
839            let args = HashMap::new();
840            let result = int(&to_value(input).unwrap(), &args);
841
842            assert!(result.is_ok());
843            assert_eq!(result.unwrap(), to_value(expected).unwrap());
844        }
845    }
846
847    #[test]
848    fn test_int_others() {
849        let mut args = HashMap::new();
850
851        let result = int(&to_value(1.23).unwrap(), &args);
852        assert!(result.is_ok());
853        assert_eq!(result.unwrap(), to_value(1).unwrap());
854
855        let result = int(&to_value(-5).unwrap(), &args);
856        assert!(result.is_ok());
857        assert_eq!(result.unwrap(), to_value(-5).unwrap());
858
859        args.insert("default".to_string(), to_value(5).unwrap());
860        args.insert("base".to_string(), to_value(2).unwrap());
861        let tests: Vec<(&str, i64)> =
862            vec![("0", 0), ("-3", 5), ("1010", 10), ("0b1010", 10), ("0xF00", 5)];
863        for (input, expected) in tests {
864            let result = int(&to_value(input).unwrap(), &args);
865            assert!(result.is_ok());
866            assert_eq!(result.unwrap(), to_value(expected).unwrap());
867        }
868
869        args.insert("default".to_string(), to_value(-4).unwrap());
870        args.insert("base".to_string(), to_value(8).unwrap());
871        let tests: Vec<(&str, i64)> =
872            vec![("21", 17), ("-3", -3), ("9OO", -4), ("0o567", 375), ("0b101", -4)];
873        for (input, expected) in tests {
874            let result = int(&to_value(input).unwrap(), &args);
875            assert!(result.is_ok());
876            assert_eq!(result.unwrap(), to_value(expected).unwrap());
877        }
878
879        args.insert("default".to_string(), to_value(0).unwrap());
880        args.insert("base".to_string(), to_value(16).unwrap());
881        let tests: Vec<(&str, i64)> = vec![("1011", 4113), ("0xC3", 195)];
882        for (input, expected) in tests {
883            let result = int(&to_value(input).unwrap(), &args);
884            assert!(result.is_ok());
885            assert_eq!(result.unwrap(), to_value(expected).unwrap());
886        }
887
888        args.insert("default".to_string(), to_value(0).unwrap());
889        args.insert("base".to_string(), to_value(5).unwrap());
890        let tests: Vec<(&str, i64)> = vec![("4321", 586), ("-100", -25), ("0b100", 0)];
891        for (input, expected) in tests {
892            let result = int(&to_value(input).unwrap(), &args);
893            assert!(result.is_ok());
894            assert_eq!(result.unwrap(), to_value(expected).unwrap());
895        }
896    }
897
898    #[test]
899    fn test_float() {
900        let mut args = HashMap::new();
901
902        let tests: Vec<(&str, f64)> = vec![("0", 0.0), ("-5.3", -5.3)];
903        for (input, expected) in tests {
904            let result = float(&to_value(input).unwrap(), &args);
905
906            assert!(result.is_ok());
907            assert_eq!(result.unwrap(), to_value(expected).unwrap());
908        }
909
910        args.insert("default".to_string(), to_value(3.18).unwrap());
911        let result = float(&to_value("bad_val").unwrap(), &args);
912        assert!(result.is_ok());
913        assert_eq!(result.unwrap(), to_value(3.18).unwrap());
914
915        let result = float(&to_value(1.23).unwrap(), &args);
916        assert!(result.is_ok());
917        assert_eq!(result.unwrap(), to_value(1.23).unwrap());
918    }
919
920    #[test]
921    fn test_linebreaksbr() {
922        let args = HashMap::new();
923        let tests: Vec<(&str, &str)> = vec![
924            ("hello world", "hello world"),
925            ("hello\nworld", "hello<br>world"),
926            ("hello\r\nworld", "hello<br>world"),
927            ("hello\n\rworld", "hello<br>\rworld"),
928            ("hello\r\n\nworld", "hello<br><br>world"),
929            ("hello<br>world\n", "hello<br>world<br>"),
930        ];
931        for (input, expected) in tests {
932            let result = linebreaksbr(&to_value(input).unwrap(), &args);
933            assert!(result.is_ok());
934            assert_eq!(result.unwrap(), to_value(expected).unwrap());
935        }
936    }
937}