askama/filters/
mod.rs

1//! Module for built-in filter functions
2//!
3//! Contains all the built-in filter functions for use in templates.
4//! You can define your own filters, as well.
5//! For more information, read the [book](https://djc.github.io/askama/filters.html).
6#![allow(clippy::trivially_copy_pass_by_ref)]
7
8use std::fmt::{self, Write};
9
10#[cfg(feature = "serde-json")]
11mod json;
12#[cfg(feature = "serde-json")]
13pub use self::json::json;
14
15#[cfg(feature = "serde-yaml")]
16mod yaml;
17#[cfg(feature = "serde-yaml")]
18pub use self::yaml::yaml;
19
20#[allow(unused_imports)]
21use crate::error::Error::Fmt;
22use askama_escape::{Escaper, MarkupDisplay};
23#[cfg(feature = "humansize")]
24use dep_humansize::{format_size_i, ToF64, DECIMAL};
25#[cfg(feature = "num-traits")]
26use dep_num_traits::{cast::NumCast, Signed};
27#[cfg(feature = "percent-encoding")]
28use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
29
30use super::Result;
31
32#[cfg(feature = "percent-encoding")]
33// Urlencode char encoding set. Only the characters in the unreserved set don't
34// have any special purpose in any part of a URI and can be safely left
35// unencoded as specified in https://tools.ietf.org/html/rfc3986.html#section-2.3
36const URLENCODE_STRICT_SET: &AsciiSet = &NON_ALPHANUMERIC
37    .remove(b'_')
38    .remove(b'.')
39    .remove(b'-')
40    .remove(b'~');
41
42#[cfg(feature = "percent-encoding")]
43// Same as URLENCODE_STRICT_SET, but preserves forward slashes for encoding paths
44const URLENCODE_SET: &AsciiSet = &URLENCODE_STRICT_SET.remove(b'/');
45
46/// Marks a string (or other `Display` type) as safe
47///
48/// Use this is you want to allow markup in an expression, or if you know
49/// that the expression's contents don't need to be escaped.
50///
51/// Askama will automatically insert the first (`Escaper`) argument,
52/// so this filter only takes a single argument of any type that implements
53/// `Display`.
54pub fn safe<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>>
55where
56    E: Escaper,
57    T: fmt::Display,
58{
59    Ok(MarkupDisplay::new_safe(v, e))
60}
61
62/// Escapes strings according to the escape mode.
63///
64/// Askama will automatically insert the first (`Escaper`) argument,
65/// so this filter only takes a single argument of any type that implements
66/// `Display`.
67///
68/// It is possible to optionally specify an escaper other than the default for
69/// the template's extension, like `{{ val|escape("txt") }}`.
70pub fn escape<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>>
71where
72    E: Escaper,
73    T: fmt::Display,
74{
75    Ok(MarkupDisplay::new_unsafe(v, e))
76}
77
78#[cfg(feature = "humansize")]
79/// Returns adequate string representation (in KB, ..) of number of bytes
80pub fn filesizeformat(b: &(impl ToF64 + Copy)) -> Result<String> {
81    Ok(format_size_i(*b, DECIMAL))
82}
83
84#[cfg(feature = "percent-encoding")]
85/// Percent-encodes the argument for safe use in URI; does not encode `/`.
86///
87/// This should be safe for all parts of URI (paths segments, query keys, query
88/// values). In the rare case that the server can't deal with forward slashes in
89/// the query string, use [`urlencode_strict`], which encodes them as well.
90///
91/// Encodes all characters except ASCII letters, digits, and `_.-~/`. In other
92/// words, encodes all characters which are not in the unreserved set,
93/// as specified by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.3),
94/// with the exception of `/`.
95///
96/// ```none,ignore
97/// <a href="/metro{{ "/stations/Château d'Eau"|urlencode }}">Station</a>
98/// <a href="/page?text={{ "look, unicode/emojis ✨"|urlencode }}">Page</a>
99/// ```
100///
101/// To encode `/` as well, see [`urlencode_strict`](./fn.urlencode_strict.html).
102///
103/// [`urlencode_strict`]: ./fn.urlencode_strict.html
104pub fn urlencode<T: fmt::Display>(s: T) -> Result<String> {
105    let s = s.to_string();
106    Ok(utf8_percent_encode(&s, URLENCODE_SET).to_string())
107}
108
109#[cfg(feature = "percent-encoding")]
110/// Percent-encodes the argument for safe use in URI; encodes `/`.
111///
112/// Use this filter for encoding query keys and values in the rare case that
113/// the server can't process them unencoded.
114///
115/// Encodes all characters except ASCII letters, digits, and `_.-~`. In other
116/// words, encodes all characters which are not in the unreserved set,
117/// as specified by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.3).
118///
119/// ```none,ignore
120/// <a href="/page?text={{ "look, unicode/emojis ✨"|urlencode_strict }}">Page</a>
121/// ```
122///
123/// If you want to preserve `/`, see [`urlencode`](./fn.urlencode.html).
124pub fn urlencode_strict<T: fmt::Display>(s: T) -> Result<String> {
125    let s = s.to_string();
126    Ok(utf8_percent_encode(&s, URLENCODE_STRICT_SET).to_string())
127}
128
129/// Formats arguments according to the specified format
130///
131/// The *second* argument to this filter must be a string literal (as in normal
132/// Rust). The two arguments are passed through to the `format!()`
133/// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by
134/// the Askama code generator, but the order is swapped to support filter
135/// composition.
136///
137/// ```ignore
138/// {{ value | fmt("{:?}") }}
139/// ```
140///
141/// Compare with [format](./fn.format.html).
142pub fn fmt() {}
143
144/// Formats arguments according to the specified format
145///
146/// The first argument to this filter must be a string literal (as in normal
147/// Rust). All arguments are passed through to the `format!()`
148/// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by
149/// the Askama code generator.
150///
151/// ```ignore
152/// {{ "{:?}{:?}" | format(value, other_value) }}
153/// ```
154///
155/// Compare with [fmt](./fn.fmt.html).
156pub fn format() {}
157
158/// Replaces line breaks in plain text with appropriate HTML
159///
160/// A single newline becomes an HTML line break `<br>` and a new line
161/// followed by a blank line becomes a paragraph break `<p>`.
162pub fn linebreaks<T: fmt::Display>(s: T) -> Result<String> {
163    let s = s.to_string();
164    let linebroken = s.replace("\n\n", "</p><p>").replace('\n', "<br/>");
165
166    Ok(format!("<p>{linebroken}</p>"))
167}
168
169/// Converts all newlines in a piece of plain text to HTML line breaks
170pub fn linebreaksbr<T: fmt::Display>(s: T) -> Result<String> {
171    let s = s.to_string();
172    Ok(s.replace('\n', "<br/>"))
173}
174
175/// Replaces only paragraph breaks in plain text with appropriate HTML
176///
177/// A new line followed by a blank line becomes a paragraph break `<p>`.
178/// Paragraph tags only wrap content; empty paragraphs are removed.
179/// No `<br/>` tags are added.
180pub fn paragraphbreaks<T: fmt::Display>(s: T) -> Result<String> {
181    let s = s.to_string();
182    let linebroken = s.replace("\n\n", "</p><p>").replace("<p></p>", "");
183
184    Ok(format!("<p>{linebroken}</p>"))
185}
186
187/// Converts to lowercase
188pub fn lower<T: fmt::Display>(s: T) -> Result<String> {
189    let s = s.to_string();
190    Ok(s.to_lowercase())
191}
192
193/// Alias for the `lower()` filter
194pub fn lowercase<T: fmt::Display>(s: T) -> Result<String> {
195    lower(s)
196}
197
198/// Converts to uppercase
199pub fn upper<T: fmt::Display>(s: T) -> Result<String> {
200    let s = s.to_string();
201    Ok(s.to_uppercase())
202}
203
204/// Alias for the `upper()` filter
205pub fn uppercase<T: fmt::Display>(s: T) -> Result<String> {
206    upper(s)
207}
208
209/// Strip leading and trailing whitespace
210pub fn trim<T: fmt::Display>(s: T) -> Result<String> {
211    let s = s.to_string();
212    Ok(s.trim().to_owned())
213}
214
215/// Limit string length, appends '...' if truncated
216pub fn truncate<T: fmt::Display>(s: T, len: usize) -> Result<String> {
217    let mut s = s.to_string();
218    if s.len() > len {
219        let mut real_len = len;
220        while !s.is_char_boundary(real_len) {
221            real_len += 1;
222        }
223        s.truncate(real_len);
224        s.push_str("...");
225    }
226    Ok(s)
227}
228
229/// Indent lines with `width` spaces
230pub fn indent<T: fmt::Display>(s: T, width: usize) -> Result<String> {
231    let s = s.to_string();
232
233    let mut indented = String::new();
234
235    for (i, c) in s.char_indices() {
236        indented.push(c);
237
238        if c == '\n' && i < s.len() - 1 {
239            for _ in 0..width {
240                indented.push(' ');
241            }
242        }
243    }
244
245    Ok(indented)
246}
247
248#[cfg(feature = "num-traits")]
249/// Casts number to f64
250pub fn into_f64<T>(number: T) -> Result<f64>
251where
252    T: NumCast,
253{
254    number.to_f64().ok_or(Fmt(fmt::Error))
255}
256
257#[cfg(feature = "num-traits")]
258/// Casts number to isize
259pub fn into_isize<T>(number: T) -> Result<isize>
260where
261    T: NumCast,
262{
263    number.to_isize().ok_or(Fmt(fmt::Error))
264}
265
266/// Joins iterable into a string separated by provided argument
267pub fn join<T, I, S>(input: I, separator: S) -> Result<String>
268where
269    T: fmt::Display,
270    I: Iterator<Item = T>,
271    S: AsRef<str>,
272{
273    let separator: &str = separator.as_ref();
274
275    let mut rv = String::new();
276
277    for (num, item) in input.enumerate() {
278        if num > 0 {
279            rv.push_str(separator);
280        }
281
282        write!(rv, "{item}")?;
283    }
284
285    Ok(rv)
286}
287
288#[cfg(feature = "num-traits")]
289/// Absolute value
290pub fn abs<T>(number: T) -> Result<T>
291where
292    T: Signed,
293{
294    Ok(number.abs())
295}
296
297/// Capitalize a value. The first character will be uppercase, all others lowercase.
298pub fn capitalize<T: fmt::Display>(s: T) -> Result<String> {
299    let s = s.to_string();
300    match s.chars().next() {
301        Some(c) => {
302            let mut replacement: String = c.to_uppercase().collect();
303            replacement.push_str(&s[c.len_utf8()..].to_lowercase());
304            Ok(replacement)
305        }
306        _ => Ok(s),
307    }
308}
309
310/// Centers the value in a field of a given width
311pub fn center(src: &dyn fmt::Display, dst_len: usize) -> Result<String> {
312    let src = src.to_string();
313    let len = src.len();
314
315    if dst_len <= len {
316        Ok(src)
317    } else {
318        let diff = dst_len - len;
319        let mid = diff / 2;
320        let r = diff % 2;
321        let mut buf = String::with_capacity(dst_len);
322
323        for _ in 0..mid {
324            buf.push(' ');
325        }
326
327        buf.push_str(&src);
328
329        for _ in 0..mid + r {
330            buf.push(' ');
331        }
332
333        Ok(buf)
334    }
335}
336
337/// Count the words in that string
338pub fn wordcount<T: fmt::Display>(s: T) -> Result<usize> {
339    let s = s.to_string();
340
341    Ok(s.split_whitespace().count())
342}
343
344#[cfg(feature = "markdown")]
345pub fn markdown<E, S>(
346    e: E,
347    s: S,
348    options: Option<&comrak::ComrakOptions>,
349) -> Result<MarkupDisplay<E, String>>
350where
351    E: Escaper,
352    S: AsRef<str>,
353{
354    use comrak::{
355        markdown_to_html, ComrakExtensionOptions, ComrakOptions, ComrakParseOptions,
356        ComrakRenderOptions, ListStyleType,
357    };
358
359    const DEFAULT_OPTIONS: ComrakOptions = ComrakOptions {
360        extension: ComrakExtensionOptions {
361            strikethrough: true,
362            tagfilter: true,
363            table: true,
364            autolink: true,
365            // default:
366            tasklist: false,
367            superscript: false,
368            header_ids: None,
369            footnotes: false,
370            description_lists: false,
371            front_matter_delimiter: None,
372        },
373        parse: ComrakParseOptions {
374            // default:
375            smart: false,
376            default_info_string: None,
377            relaxed_tasklist_matching: false,
378        },
379        render: ComrakRenderOptions {
380            escape: true,
381            // default:
382            hardbreaks: false,
383            github_pre_lang: false,
384            full_info_string: false,
385            width: 0,
386            unsafe_: false,
387            list_style: ListStyleType::Dash,
388            sourcepos: false,
389        },
390    };
391
392    let s = markdown_to_html(s.as_ref(), options.unwrap_or(&DEFAULT_OPTIONS));
393    Ok(MarkupDisplay::new_safe(s, e))
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    #[cfg(feature = "num-traits")]
400    use std::f64::INFINITY;
401
402    #[cfg(feature = "humansize")]
403    #[test]
404    fn test_filesizeformat() {
405        assert_eq!(filesizeformat(&0).unwrap(), "0 B");
406        assert_eq!(filesizeformat(&999u64).unwrap(), "999 B");
407        assert_eq!(filesizeformat(&1000i32).unwrap(), "1 kB");
408        assert_eq!(filesizeformat(&1023).unwrap(), "1.02 kB");
409        assert_eq!(filesizeformat(&1024usize).unwrap(), "1.02 kB");
410    }
411
412    #[cfg(feature = "percent-encoding")]
413    #[test]
414    fn test_urlencoding() {
415        // Unreserved (https://tools.ietf.org/html/rfc3986.html#section-2.3)
416        // alpha / digit
417        assert_eq!(urlencode("AZaz09").unwrap(), "AZaz09");
418        assert_eq!(urlencode_strict("AZaz09").unwrap(), "AZaz09");
419        // other
420        assert_eq!(urlencode("_.-~").unwrap(), "_.-~");
421        assert_eq!(urlencode_strict("_.-~").unwrap(), "_.-~");
422
423        // Reserved (https://tools.ietf.org/html/rfc3986.html#section-2.2)
424        // gen-delims
425        assert_eq!(urlencode(":/?#[]@").unwrap(), "%3A/%3F%23%5B%5D%40");
426        assert_eq!(
427            urlencode_strict(":/?#[]@").unwrap(),
428            "%3A%2F%3F%23%5B%5D%40"
429        );
430        // sub-delims
431        assert_eq!(
432            urlencode("!$&'()*+,;=").unwrap(),
433            "%21%24%26%27%28%29%2A%2B%2C%3B%3D"
434        );
435        assert_eq!(
436            urlencode_strict("!$&'()*+,;=").unwrap(),
437            "%21%24%26%27%28%29%2A%2B%2C%3B%3D"
438        );
439
440        // Other
441        assert_eq!(
442            urlencode("žŠďŤňĚáÉóŮ").unwrap(),
443            "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE"
444        );
445        assert_eq!(
446            urlencode_strict("žŠďŤňĚáÉóŮ").unwrap(),
447            "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE"
448        );
449
450        // Ferris
451        assert_eq!(urlencode("🦀").unwrap(), "%F0%9F%A6%80");
452        assert_eq!(urlencode_strict("🦀").unwrap(), "%F0%9F%A6%80");
453    }
454
455    #[test]
456    fn test_linebreaks() {
457        assert_eq!(
458            linebreaks("Foo\nBar Baz").unwrap(),
459            "<p>Foo<br/>Bar Baz</p>"
460        );
461        assert_eq!(
462            linebreaks("Foo\nBar\n\nBaz").unwrap(),
463            "<p>Foo<br/>Bar</p><p>Baz</p>"
464        );
465    }
466
467    #[test]
468    fn test_linebreaksbr() {
469        assert_eq!(linebreaksbr("Foo\nBar").unwrap(), "Foo<br/>Bar");
470        assert_eq!(
471            linebreaksbr("Foo\nBar\n\nBaz").unwrap(),
472            "Foo<br/>Bar<br/><br/>Baz"
473        );
474    }
475
476    #[test]
477    fn test_paragraphbreaks() {
478        assert_eq!(
479            paragraphbreaks("Foo\nBar Baz").unwrap(),
480            "<p>Foo\nBar Baz</p>"
481        );
482        assert_eq!(
483            paragraphbreaks("Foo\nBar\n\nBaz").unwrap(),
484            "<p>Foo\nBar</p><p>Baz</p>"
485        );
486        assert_eq!(
487            paragraphbreaks("Foo\n\n\n\n\nBar\n\nBaz").unwrap(),
488            "<p>Foo</p><p>\nBar</p><p>Baz</p>"
489        );
490    }
491
492    #[test]
493    fn test_lower() {
494        assert_eq!(lower("Foo").unwrap(), "foo");
495        assert_eq!(lower("FOO").unwrap(), "foo");
496        assert_eq!(lower("FooBar").unwrap(), "foobar");
497        assert_eq!(lower("foo").unwrap(), "foo");
498    }
499
500    #[test]
501    fn test_upper() {
502        assert_eq!(upper("Foo").unwrap(), "FOO");
503        assert_eq!(upper("FOO").unwrap(), "FOO");
504        assert_eq!(upper("FooBar").unwrap(), "FOOBAR");
505        assert_eq!(upper("foo").unwrap(), "FOO");
506    }
507
508    #[test]
509    fn test_trim() {
510        assert_eq!(trim(" Hello\tworld\t").unwrap(), "Hello\tworld");
511    }
512
513    #[test]
514    fn test_truncate() {
515        assert_eq!(truncate("hello", 2).unwrap(), "he...");
516        let a = String::from("您好");
517        assert_eq!(a.len(), 6);
518        assert_eq!(String::from("您").len(), 3);
519        assert_eq!(truncate("您好", 1).unwrap(), "您...");
520        assert_eq!(truncate("您好", 2).unwrap(), "您...");
521        assert_eq!(truncate("您好", 3).unwrap(), "您...");
522        assert_eq!(truncate("您好", 4).unwrap(), "您好...");
523        assert_eq!(truncate("您好", 6).unwrap(), "您好");
524        assert_eq!(truncate("您好", 7).unwrap(), "您好");
525        let s = String::from("🤚a🤚");
526        assert_eq!(s.len(), 9);
527        assert_eq!(String::from("🤚").len(), 4);
528        assert_eq!(truncate("🤚a🤚", 1).unwrap(), "🤚...");
529        assert_eq!(truncate("🤚a🤚", 2).unwrap(), "🤚...");
530        assert_eq!(truncate("🤚a🤚", 3).unwrap(), "🤚...");
531        assert_eq!(truncate("🤚a🤚", 4).unwrap(), "🤚...");
532        assert_eq!(truncate("🤚a🤚", 5).unwrap(), "🤚a...");
533        assert_eq!(truncate("🤚a🤚", 6).unwrap(), "🤚a🤚...");
534        assert_eq!(truncate("🤚a🤚", 9).unwrap(), "🤚a🤚");
535        assert_eq!(truncate("🤚a🤚", 10).unwrap(), "🤚a🤚");
536    }
537
538    #[test]
539    fn test_indent() {
540        assert_eq!(indent("hello", 2).unwrap(), "hello");
541        assert_eq!(indent("hello\n", 2).unwrap(), "hello\n");
542        assert_eq!(indent("hello\nfoo", 2).unwrap(), "hello\n  foo");
543        assert_eq!(
544            indent("hello\nfoo\n bar", 4).unwrap(),
545            "hello\n    foo\n     bar"
546        );
547    }
548
549    #[cfg(feature = "num-traits")]
550    #[test]
551    #[allow(clippy::float_cmp)]
552    fn test_into_f64() {
553        assert_eq!(into_f64(1).unwrap(), 1.0_f64);
554        assert_eq!(into_f64(1.9).unwrap(), 1.9_f64);
555        assert_eq!(into_f64(-1.9).unwrap(), -1.9_f64);
556        assert_eq!(into_f64(INFINITY as f32).unwrap(), INFINITY);
557        assert_eq!(into_f64(-INFINITY as f32).unwrap(), -INFINITY);
558    }
559
560    #[cfg(feature = "num-traits")]
561    #[test]
562    fn test_into_isize() {
563        assert_eq!(into_isize(1).unwrap(), 1_isize);
564        assert_eq!(into_isize(1.9).unwrap(), 1_isize);
565        assert_eq!(into_isize(-1.9).unwrap(), -1_isize);
566        assert_eq!(into_isize(1.5_f64).unwrap(), 1_isize);
567        assert_eq!(into_isize(-1.5_f64).unwrap(), -1_isize);
568        match into_isize(INFINITY) {
569            Err(Fmt(fmt::Error)) => {}
570            _ => panic!("Should return error of type Err(Fmt(fmt::Error))"),
571        };
572    }
573
574    #[allow(clippy::needless_borrow)]
575    #[test]
576    fn test_join() {
577        assert_eq!(
578            join((&["hello", "world"]).iter(), ", ").unwrap(),
579            "hello, world"
580        );
581        assert_eq!(join((&["hello"]).iter(), ", ").unwrap(), "hello");
582
583        let empty: &[&str] = &[];
584        assert_eq!(join(empty.iter(), ", ").unwrap(), "");
585
586        let input: Vec<String> = vec!["foo".into(), "bar".into(), "bazz".into()];
587        assert_eq!(join(input.iter(), ":").unwrap(), "foo:bar:bazz");
588
589        let input: &[String] = &["foo".into(), "bar".into()];
590        assert_eq!(join(input.iter(), ":").unwrap(), "foo:bar");
591
592        let real: String = "blah".into();
593        let input: Vec<&str> = vec![&real];
594        assert_eq!(join(input.iter(), ";").unwrap(), "blah");
595
596        assert_eq!(
597            join((&&&&&["foo", "bar"]).iter(), ", ").unwrap(),
598            "foo, bar"
599        );
600    }
601
602    #[cfg(feature = "num-traits")]
603    #[test]
604    #[allow(clippy::float_cmp)]
605    fn test_abs() {
606        assert_eq!(abs(1).unwrap(), 1);
607        assert_eq!(abs(-1).unwrap(), 1);
608        assert_eq!(abs(1.0).unwrap(), 1.0);
609        assert_eq!(abs(-1.0).unwrap(), 1.0);
610        assert_eq!(abs(1.0_f64).unwrap(), 1.0_f64);
611        assert_eq!(abs(-1.0_f64).unwrap(), 1.0_f64);
612    }
613
614    #[test]
615    fn test_capitalize() {
616        assert_eq!(capitalize("foo").unwrap(), "Foo".to_string());
617        assert_eq!(capitalize("f").unwrap(), "F".to_string());
618        assert_eq!(capitalize("fO").unwrap(), "Fo".to_string());
619        assert_eq!(capitalize("").unwrap(), "".to_string());
620        assert_eq!(capitalize("FoO").unwrap(), "Foo".to_string());
621        assert_eq!(capitalize("foO BAR").unwrap(), "Foo bar".to_string());
622        assert_eq!(capitalize("äØÄÅÖ").unwrap(), "Äøäåö".to_string());
623        assert_eq!(capitalize("ß").unwrap(), "SS".to_string());
624        assert_eq!(capitalize("ßß").unwrap(), "SSß".to_string());
625    }
626
627    #[test]
628    fn test_center() {
629        assert_eq!(center(&"f", 3).unwrap(), " f ".to_string());
630        assert_eq!(center(&"f", 4).unwrap(), " f  ".to_string());
631        assert_eq!(center(&"foo", 1).unwrap(), "foo".to_string());
632        assert_eq!(center(&"foo bar", 8).unwrap(), "foo bar ".to_string());
633    }
634
635    #[test]
636    fn test_wordcount() {
637        assert_eq!(wordcount("").unwrap(), 0);
638        assert_eq!(wordcount(" \n\t").unwrap(), 0);
639        assert_eq!(wordcount("foo").unwrap(), 1);
640        assert_eq!(wordcount("foo bar").unwrap(), 2);
641    }
642}