askama_shared 0.12.2

Shared code for Askama
Documentation
//! Module for built-in filter functions
//!
//! Contains all the built-in filter functions for use in templates.
//! You can define your own filters, as well.
//! For more information, read the [book](https://djc.github.io/askama/filters.html).
#![allow(clippy::trivially_copy_pass_by_ref)]

use std::fmt;

#[cfg(feature = "serde_json")]
mod json;
#[cfg(feature = "serde_json")]
pub use self::json::json;

#[cfg(feature = "serde_yaml")]
mod yaml;
#[cfg(feature = "serde_yaml")]
pub use self::yaml::yaml;

#[allow(unused_imports)]
use crate::error::Error::Fmt;
use askama_escape::{Escaper, MarkupDisplay};
#[cfg(feature = "humansize")]
use humansize::{file_size_opts, FileSize};
#[cfg(feature = "num-traits")]
use num_traits::{cast::NumCast, Signed};
#[cfg(feature = "percent-encoding")]
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};

use super::Result;

#[cfg(feature = "percent-encoding")]
// Urlencode char encoding set. Only the characters in the unreserved set don't
// have any special purpose in any part of a URI and can be safely left
// unencoded as specified in https://tools.ietf.org/html/rfc3986.html#section-2.3
const URLENCODE_STRICT_SET: &AsciiSet = &NON_ALPHANUMERIC
    .remove(b'_')
    .remove(b'.')
    .remove(b'-')
    .remove(b'~');

#[cfg(feature = "percent-encoding")]
// Same as URLENCODE_STRICT_SET, but preserves forward slashes for encoding paths
const URLENCODE_SET: &AsciiSet = &URLENCODE_STRICT_SET.remove(b'/');

// This is used by the code generator to decide whether a named filter is part of
// Askama or should refer to a local `filters` module. It should contain all the
// filters shipped with Askama, even the optional ones (since optional inclusion
// in the const vector based on features seems impossible right now).
pub const BUILT_IN_FILTERS: &[&str] = &[
    "abs",
    "capitalize",
    "center",
    "e",
    "escape",
    "filesizeformat",
    "fmt",
    "format",
    "indent",
    "into_f64",
    "into_isize",
    "join",
    "linebreaks",
    "linebreaksbr",
    "paragraphbreaks",
    "lower",
    "lowercase",
    "safe",
    "trim",
    "truncate",
    "upper",
    "uppercase",
    "urlencode",
    "urlencode_strict",
    "wordcount",
    // optional features, reserve the names anyway:
    "json",
    "markdown",
    "yaml",
];

/// Marks a string (or other `Display` type) as safe
///
/// Use this is you want to allow markup in an expression, or if you know
/// that the expression's contents don't need to be escaped.
///
/// Askama will automatically insert the first (`Escaper`) argument,
/// so this filter only takes a single argument of any type that implements
/// `Display`.
pub fn safe<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>>
where
    E: Escaper,
    T: fmt::Display,
{
    Ok(MarkupDisplay::new_safe(v, e))
}

/// Escapes `&`, `<` and `>` in strings
///
/// Askama will automatically insert the first (`Escaper`) argument,
/// so this filter only takes a single argument of any type that implements
/// `Display`.
pub fn escape<E, T>(e: E, v: T) -> Result<MarkupDisplay<E, T>>
where
    E: Escaper,
    T: fmt::Display,
{
    Ok(MarkupDisplay::new_unsafe(v, e))
}

#[cfg(feature = "humansize")]
/// Returns adequate string representation (in KB, ..) of number of bytes
pub fn filesizeformat<B: FileSize>(b: &B) -> Result<String> {
    b.file_size(file_size_opts::DECIMAL)
        .map_err(|_| Fmt(fmt::Error))
}

#[cfg(feature = "percent-encoding")]
/// Percent-encodes the argument for safe use in URI; does not encode `/`.
///
/// This should be safe for all parts of URI (paths segments, query keys, query
/// values). In the rare case that the server can't deal with forward slashes in
/// the query string, use [`urlencode_strict`], which encodes them as well.
///
/// Encodes all characters except ASCII letters, digits, and `_.-~/`. In other
/// words, encodes all characters which are not in the unreserved set,
/// as specified by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.3),
/// with the exception of `/`.
///
/// ```none,ignore
/// <a href="/metro{{ "/stations/Château d'Eau"|urlencode }}">Station</a>
/// <a href="/page?text={{ "look, unicode/emojis ✨"|urlencode }}">Page</a>
/// ```
///
/// To encode `/` as well, see [`urlencode_strict`](./fn.urlencode_strict.html).
///
/// [`urlencode_strict`]: ./fn.urlencode_strict.html
pub fn urlencode<T: fmt::Display>(s: T) -> Result<String> {
    let s = s.to_string();
    Ok(utf8_percent_encode(&s, URLENCODE_SET).to_string())
}

#[cfg(feature = "percent-encoding")]
/// Percent-encodes the argument for safe use in URI; encodes `/`.
///
/// Use this filter for encoding query keys and values in the rare case that
/// the server can't process them unencoded.
///
/// Encodes all characters except ASCII letters, digits, and `_.-~`. In other
/// words, encodes all characters which are not in the unreserved set,
/// as specified by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.3).
///
/// ```none,ignore
/// <a href="/page?text={{ "look, unicode/emojis ✨"|urlencode_strict }}">Page</a>
/// ```
///
/// If you want to preserve `/`, see [`urlencode`](./fn.urlencode.html).
pub fn urlencode_strict<T: fmt::Display>(s: T) -> Result<String> {
    let s = s.to_string();
    Ok(utf8_percent_encode(&s, URLENCODE_STRICT_SET).to_string())
}

/// Formats arguments according to the specified format
///
/// The *second* argument to this filter must be a string literal (as in normal
/// Rust). The two arguments are passed through to the `format!()`
/// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by
/// the Askama code generator, but the order is swapped to support filter
/// composition.
///
/// ```ignore
/// {{ value | fmt("{:?}") }}
/// ```
///
/// Compare with [format](./fn.format.html).
pub fn fmt() {}

/// Formats arguments according to the specified format
///
/// The first argument to this filter must be a string literal (as in normal
/// Rust). All arguments are passed through to the `format!()`
/// [macro](https://doc.rust-lang.org/stable/std/macro.format.html) by
/// the Askama code generator.
///
/// ```ignore
/// {{ "{:?}{:?}" | format(value, other_value) }}
/// ```
///
/// Compare with [fmt](./fn.fmt.html).
pub fn format() {}

/// Replaces line breaks in plain text with appropriate HTML
///
/// A single newline becomes an HTML line break `<br>` and a new line
/// followed by a blank line becomes a paragraph break `<p>`.
pub fn linebreaks<T: fmt::Display>(s: T) -> Result<String> {
    let s = s.to_string();
    let linebroken = s.replace("\n\n", "</p><p>").replace('\n', "<br/>");

    Ok(format!("<p>{}</p>", linebroken))
}

/// Converts all newlines in a piece of plain text to HTML line breaks
pub fn linebreaksbr<T: fmt::Display>(s: T) -> Result<String> {
    let s = s.to_string();
    Ok(s.replace('\n', "<br/>"))
}

/// Replaces only paragraph breaks in plain text with appropriate HTML
///
/// A new line followed by a blank line becomes a paragraph break `<p>`.
/// Paragraph tags only wrap content; empty paragraphs are removed.
/// No `<br/>` tags are added.
pub fn paragraphbreaks<T: fmt::Display>(s: T) -> Result<String> {
    let s = s.to_string();
    let linebroken = s.replace("\n\n", "</p><p>").replace("<p></p>", "");

    Ok(format!("<p>{}</p>", linebroken))
}

/// Converts to lowercase
pub fn lower<T: fmt::Display>(s: T) -> Result<String> {
    let s = s.to_string();
    Ok(s.to_lowercase())
}

/// Alias for the `lower()` filter
pub fn lowercase<T: fmt::Display>(s: T) -> Result<String> {
    lower(s)
}

/// Converts to uppercase
pub fn upper<T: fmt::Display>(s: T) -> Result<String> {
    let s = s.to_string();
    Ok(s.to_uppercase())
}

/// Alias for the `upper()` filter
pub fn uppercase<T: fmt::Display>(s: T) -> Result<String> {
    upper(s)
}

/// Strip leading and trailing whitespace
pub fn trim<T: fmt::Display>(s: T) -> Result<String> {
    let s = s.to_string();
    Ok(s.trim().to_owned())
}

/// Limit string length, appends '...' if truncated
pub fn truncate<T: fmt::Display>(s: T, len: usize) -> Result<String> {
    let mut s = s.to_string();
    if s.len() > len {
        let mut real_len = len;
        while !s.is_char_boundary(real_len) {
            real_len += 1;
        }
        s.truncate(real_len);
        s.push_str("...");
    }
    Ok(s)
}

/// Indent lines with `width` spaces
pub fn indent<T: fmt::Display>(s: T, width: usize) -> Result<String> {
    let s = s.to_string();

    let mut indented = String::new();

    for (i, c) in s.char_indices() {
        indented.push(c);

        if c == '\n' && i < s.len() - 1 {
            for _ in 0..width {
                indented.push(' ');
            }
        }
    }

    Ok(indented)
}

#[cfg(feature = "num-traits")]
/// Casts number to f64
pub fn into_f64<T>(number: T) -> Result<f64>
where
    T: NumCast,
{
    number.to_f64().ok_or(Fmt(fmt::Error))
}

#[cfg(feature = "num-traits")]
/// Casts number to isize
pub fn into_isize<T>(number: T) -> Result<isize>
where
    T: NumCast,
{
    number.to_isize().ok_or(Fmt(fmt::Error))
}

/// Joins iterable into a string separated by provided argument
pub fn join<T, I, S>(input: I, separator: S) -> Result<String>
where
    T: fmt::Display,
    I: Iterator<Item = T>,
    S: AsRef<str>,
{
    let separator: &str = separator.as_ref();

    let mut rv = String::new();

    for (num, item) in input.enumerate() {
        if num > 0 {
            rv.push_str(separator);
        }

        rv.push_str(&format!("{}", item));
    }

    Ok(rv)
}

#[cfg(feature = "num-traits")]
/// Absolute value
pub fn abs<T>(number: T) -> Result<T>
where
    T: Signed,
{
    Ok(number.abs())
}

/// Capitalize a value. The first character will be uppercase, all others lowercase.
pub fn capitalize<T: fmt::Display>(s: T) -> Result<String> {
    let mut s = s.to_string();

    match s.get_mut(0..1).map(|s| {
        s.make_ascii_uppercase();
        &*s
    }) {
        None => Ok(s),
        _ => {
            s.get_mut(1..).map(|s| {
                s.make_ascii_lowercase();
                &*s
            });
            Ok(s)
        }
    }
}

/// Centers the value in a field of a given width
pub fn center(src: &dyn fmt::Display, dst_len: usize) -> Result<String> {
    let src = src.to_string();
    let len = src.len();

    if dst_len <= len {
        Ok(src)
    } else {
        let diff = dst_len - len;
        let mid = diff / 2;
        let r = diff % 2;
        let mut buf = String::with_capacity(dst_len);

        for _ in 0..mid {
            buf.push(' ');
        }

        buf.push_str(&src);

        for _ in 0..mid + r {
            buf.push(' ');
        }

        Ok(buf)
    }
}

/// Count the words in that string
pub fn wordcount<T: fmt::Display>(s: T) -> Result<usize> {
    let s = s.to_string();

    Ok(s.split_whitespace().count())
}

#[cfg(feature = "markdown")]
pub fn markdown<E, S>(
    e: E,
    s: S,
    options: Option<&comrak::ComrakOptions>,
) -> Result<MarkupDisplay<E, String>>
where
    E: Escaper,
    S: AsRef<str>,
{
    use comrak::{
        markdown_to_html, ComrakExtensionOptions, ComrakOptions, ComrakParseOptions,
        ComrakRenderOptions,
    };

    const DEFAULT_OPTIONS: ComrakOptions = ComrakOptions {
        extension: ComrakExtensionOptions {
            strikethrough: true,
            tagfilter: true,
            table: true,
            autolink: true,
            // default:
            tasklist: false,
            superscript: false,
            header_ids: None,
            footnotes: false,
            description_lists: false,
            front_matter_delimiter: None,
        },
        parse: ComrakParseOptions {
            // default:
            smart: false,
            default_info_string: None,
        },
        render: ComrakRenderOptions {
            unsafe_: false,
            escape: true,
            // default:
            hardbreaks: false,
            github_pre_lang: false,
            width: 0,
        },
    };

    let s = markdown_to_html(s.as_ref(), options.unwrap_or(&DEFAULT_OPTIONS));
    Ok(MarkupDisplay::new_safe(s, e))
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(feature = "num-traits")]
    use std::f64::INFINITY;

    #[cfg(feature = "humansize")]
    #[test]
    fn test_filesizeformat() {
        assert_eq!(filesizeformat(&0).unwrap(), "0 B");
        assert_eq!(filesizeformat(&999u64).unwrap(), "999 B");
        assert_eq!(filesizeformat(&1000i32).unwrap(), "1 KB");
        assert_eq!(filesizeformat(&1023).unwrap(), "1.02 KB");
        assert_eq!(filesizeformat(&1024usize).unwrap(), "1.02 KB");
    }

    #[cfg(feature = "percent-encoding")]
    #[test]
    fn test_urlencoding() {
        // Unreserved (https://tools.ietf.org/html/rfc3986.html#section-2.3)
        // alpha / digit
        assert_eq!(urlencode(&"AZaz09").unwrap(), "AZaz09");
        assert_eq!(urlencode_strict(&"AZaz09").unwrap(), "AZaz09");
        // other
        assert_eq!(urlencode(&"_.-~").unwrap(), "_.-~");
        assert_eq!(urlencode_strict(&"_.-~").unwrap(), "_.-~");

        // Reserved (https://tools.ietf.org/html/rfc3986.html#section-2.2)
        // gen-delims
        assert_eq!(urlencode(&":/?#[]@").unwrap(), "%3A/%3F%23%5B%5D%40");
        assert_eq!(
            urlencode_strict(&":/?#[]@").unwrap(),
            "%3A%2F%3F%23%5B%5D%40"
        );
        // sub-delims
        assert_eq!(
            urlencode(&"!$&'()*+,;=").unwrap(),
            "%21%24%26%27%28%29%2A%2B%2C%3B%3D"
        );
        assert_eq!(
            urlencode_strict(&"!$&'()*+,;=").unwrap(),
            "%21%24%26%27%28%29%2A%2B%2C%3B%3D"
        );

        // Other
        assert_eq!(
            urlencode(&"žŠďŤňĚáÉóŮ").unwrap(),
            "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE"
        );
        assert_eq!(
            urlencode_strict(&"žŠďŤňĚáÉóŮ").unwrap(),
            "%C5%BE%C5%A0%C4%8F%C5%A4%C5%88%C4%9A%C3%A1%C3%89%C3%B3%C5%AE"
        );

        // Ferris
        assert_eq!(urlencode(&"🦀").unwrap(), "%F0%9F%A6%80");
        assert_eq!(urlencode_strict(&"🦀").unwrap(), "%F0%9F%A6%80");
    }

    #[test]
    fn test_linebreaks() {
        assert_eq!(
            linebreaks(&"Foo\nBar Baz").unwrap(),
            "<p>Foo<br/>Bar Baz</p>"
        );
        assert_eq!(
            linebreaks(&"Foo\nBar\n\nBaz").unwrap(),
            "<p>Foo<br/>Bar</p><p>Baz</p>"
        );
    }

    #[test]
    fn test_linebreaksbr() {
        assert_eq!(linebreaksbr(&"Foo\nBar").unwrap(), "Foo<br/>Bar");
        assert_eq!(
            linebreaksbr(&"Foo\nBar\n\nBaz").unwrap(),
            "Foo<br/>Bar<br/><br/>Baz"
        );
    }

    #[test]
    fn test_paragraphbreaks() {
        assert_eq!(
            paragraphbreaks(&"Foo\nBar Baz").unwrap(),
            "<p>Foo\nBar Baz</p>"
        );
        assert_eq!(
            paragraphbreaks(&"Foo\nBar\n\nBaz").unwrap(),
            "<p>Foo\nBar</p><p>Baz</p>"
        );
        assert_eq!(
            paragraphbreaks(&"Foo\n\n\n\n\nBar\n\nBaz").unwrap(),
            "<p>Foo</p><p>\nBar</p><p>Baz</p>"
        );
    }

    #[test]
    fn test_lower() {
        assert_eq!(lower(&"Foo").unwrap(), "foo");
        assert_eq!(lower(&"FOO").unwrap(), "foo");
        assert_eq!(lower(&"FooBar").unwrap(), "foobar");
        assert_eq!(lower(&"foo").unwrap(), "foo");
    }

    #[test]
    fn test_upper() {
        assert_eq!(upper(&"Foo").unwrap(), "FOO");
        assert_eq!(upper(&"FOO").unwrap(), "FOO");
        assert_eq!(upper(&"FooBar").unwrap(), "FOOBAR");
        assert_eq!(upper(&"foo").unwrap(), "FOO");
    }

    #[test]
    fn test_trim() {
        assert_eq!(trim(&" Hello\tworld\t").unwrap(), "Hello\tworld");
    }

    #[test]
    fn test_truncate() {
        assert_eq!(truncate(&"hello", 2).unwrap(), "he...");
        let a = String::from("您好");
        assert_eq!(a.len(), 6);
        assert_eq!(String::from("").len(), 3);
        assert_eq!(truncate(&"您好", 1).unwrap(), "您...");
        assert_eq!(truncate(&"您好", 2).unwrap(), "您...");
        assert_eq!(truncate(&"您好", 3).unwrap(), "您...");
        assert_eq!(truncate(&"您好", 4).unwrap(), "您好...");
        assert_eq!(truncate(&"您好", 6).unwrap(), "您好");
        assert_eq!(truncate(&"您好", 7).unwrap(), "您好");
        let s = String::from("🤚a🤚");
        assert_eq!(s.len(), 9);
        assert_eq!(String::from("🤚").len(), 4);
        assert_eq!(truncate(&"🤚a🤚", 1).unwrap(), "🤚...");
        assert_eq!(truncate(&"🤚a🤚", 2).unwrap(), "🤚...");
        assert_eq!(truncate(&"🤚a🤚", 3).unwrap(), "🤚...");
        assert_eq!(truncate(&"🤚a🤚", 4).unwrap(), "🤚...");
        assert_eq!(truncate(&"🤚a🤚", 5).unwrap(), "🤚a...");
        assert_eq!(truncate(&"🤚a🤚", 6).unwrap(), "🤚a🤚...");
        assert_eq!(truncate(&"🤚a🤚", 9).unwrap(), "🤚a🤚");
        assert_eq!(truncate(&"🤚a🤚", 10).unwrap(), "🤚a🤚");
    }

    #[test]
    fn test_indent() {
        assert_eq!(indent(&"hello", 2).unwrap(), "hello");
        assert_eq!(indent(&"hello\n", 2).unwrap(), "hello\n");
        assert_eq!(indent(&"hello\nfoo", 2).unwrap(), "hello\n  foo");
        assert_eq!(
            indent(&"hello\nfoo\n bar", 4).unwrap(),
            "hello\n    foo\n     bar"
        );
    }

    #[cfg(feature = "num-traits")]
    #[test]
    #[allow(clippy::float_cmp)]
    fn test_into_f64() {
        assert_eq!(into_f64(1).unwrap(), 1.0_f64);
        assert_eq!(into_f64(1.9).unwrap(), 1.9_f64);
        assert_eq!(into_f64(-1.9).unwrap(), -1.9_f64);
        assert_eq!(into_f64(INFINITY as f32).unwrap(), INFINITY);
        assert_eq!(into_f64(-INFINITY as f32).unwrap(), -INFINITY);
    }

    #[cfg(feature = "num-traits")]
    #[test]
    fn test_into_isize() {
        assert_eq!(into_isize(1).unwrap(), 1_isize);
        assert_eq!(into_isize(1.9).unwrap(), 1_isize);
        assert_eq!(into_isize(-1.9).unwrap(), -1_isize);
        assert_eq!(into_isize(1.5_f64).unwrap(), 1_isize);
        assert_eq!(into_isize(-1.5_f64).unwrap(), -1_isize);
        match into_isize(INFINITY) {
            Err(Fmt(fmt::Error)) => {}
            _ => panic!("Should return error of type Err(Fmt(fmt::Error))"),
        };
    }

    #[allow(clippy::needless_borrow)]
    #[test]
    fn test_join() {
        assert_eq!(
            join((&["hello", "world"]).iter(), ", ").unwrap(),
            "hello, world"
        );
        assert_eq!(join((&["hello"]).iter(), ", ").unwrap(), "hello");

        let empty: &[&str] = &[];
        assert_eq!(join(empty.iter(), ", ").unwrap(), "");

        let input: Vec<String> = vec!["foo".into(), "bar".into(), "bazz".into()];
        assert_eq!(join(input.iter(), ":").unwrap(), "foo:bar:bazz");

        let input: &[String] = &["foo".into(), "bar".into()];
        assert_eq!(join(input.iter(), ":").unwrap(), "foo:bar");

        let real: String = "blah".into();
        let input: Vec<&str> = vec![&real];
        assert_eq!(join(input.iter(), ";").unwrap(), "blah");

        assert_eq!(
            join((&&&&&["foo", "bar"]).iter(), ", ").unwrap(),
            "foo, bar"
        );
    }

    #[cfg(feature = "num-traits")]
    #[test]
    #[allow(clippy::float_cmp)]
    fn test_abs() {
        assert_eq!(abs(1).unwrap(), 1);
        assert_eq!(abs(-1).unwrap(), 1);
        assert_eq!(abs(1.0).unwrap(), 1.0);
        assert_eq!(abs(-1.0).unwrap(), 1.0);
        assert_eq!(abs(1.0_f64).unwrap(), 1.0_f64);
        assert_eq!(abs(-1.0_f64).unwrap(), 1.0_f64);
    }

    #[test]
    fn test_capitalize() {
        assert_eq!(capitalize(&"foo").unwrap(), "Foo".to_string());
        assert_eq!(capitalize(&"f").unwrap(), "F".to_string());
        assert_eq!(capitalize(&"fO").unwrap(), "Fo".to_string());
        assert_eq!(capitalize(&"").unwrap(), "".to_string());
        assert_eq!(capitalize(&"FoO").unwrap(), "Foo".to_string());
        assert_eq!(capitalize(&"foO BAR").unwrap(), "Foo bar".to_string());
    }

    #[test]
    fn test_center() {
        assert_eq!(center(&"f", 3).unwrap(), " f ".to_string());
        assert_eq!(center(&"f", 4).unwrap(), " f  ".to_string());
        assert_eq!(center(&"foo", 1).unwrap(), "foo".to_string());
        assert_eq!(center(&"foo bar", 8).unwrap(), "foo bar ".to_string());
    }

    #[test]
    fn test_wordcount() {
        assert_eq!(wordcount(&"").unwrap(), 0);
        assert_eq!(wordcount(&" \n\t").unwrap(), 0);
        assert_eq!(wordcount(&"foo").unwrap(), 1);
        assert_eq!(wordcount(&"foo bar").unwrap(), 2);
    }
}