Skip to main content

tera_contrib/
format.rs

1use tera::value::ValueKind;
2use tera::{Error, Kwargs, State, TeraResult, Value};
3
4/// Putting a huge number for padding could trigger a huge allocation.
5const MAX_SPEC_NUM: usize = 100;
6
7/// Formats a value using Rust's std::fmt format specifiers.
8///
9/// Supports width, alignment, precision, sign, and zero-padding.
10/// Does NOT support radix specifiers (x, X, b, o) - only Display formatting.
11///
12/// # Example
13/// ```text
14/// {{ 3.14159 | format(spec=".2") }} -> "3.14"
15/// {{ 42 | format(spec="05") }} -> "00042"
16/// {{ "hi" | format(spec=">5") }} -> "   hi"
17/// {{ 42 | format(spec="+") }} -> "+42"
18/// ```
19pub fn format(val: Value, kwargs: Kwargs, _: &State) -> TeraResult<String> {
20    let spec = kwargs.must_get::<&str>("spec")?;
21    if spec.contains('$') {
22        return Err(Error::message(format!(
23            "format spec `{spec}` cannot reference arguments with `$`"
24        )));
25    }
26    for digits in spec.split(|c: char| !c.is_ascii_digit()) {
27        if !digits.is_empty() && !digits.parse::<usize>().is_ok_and(|n| n <= MAX_SPEC_NUM) {
28            return Err(Error::message(format!(
29                "format spec `{spec}` contains a number bigger than the maximum allowed ({MAX_SPEC_NUM})"
30            )));
31        }
32    }
33    let fmt_str = format!("{{:{}}}", spec);
34
35    match val.kind() {
36        ValueKind::String => {
37            let s = val.as_str().unwrap();
38            formatx::formatx!(&fmt_str, s)
39                .map_err(|e| Error::message(format!("format error: {}", e)))
40        }
41        ValueKind::I64 | ValueKind::I128 | ValueKind::U64 => {
42            let n = val.as_i128().unwrap();
43            formatx::formatx!(&fmt_str, n)
44                .map_err(|e| Error::message(format!("format error: {}", e)))
45        }
46        ValueKind::U128 => {
47            let n = val.as_u128().unwrap();
48            formatx::formatx!(&fmt_str, n)
49                .map_err(|e| Error::message(format!("format error: {}", e)))
50        }
51        ValueKind::F64 => {
52            let n = val.as_number().unwrap();
53            let f = n.as_float();
54            formatx::formatx!(&fmt_str, f)
55                .map_err(|e| Error::message(format!("format error: {}", e)))
56        }
57        ValueKind::Bool => {
58            let b = val.as_bool().unwrap();
59            formatx::formatx!(&fmt_str, b)
60                .map_err(|e| Error::message(format!("format error: {}", e)))
61        }
62        _ => Err(Error::message(format!(
63            "Cannot format value of type {} with format filter",
64            val.name()
65        ))),
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use tera::{Context, Tera};
73
74    fn render(template: &str) -> String {
75        let mut tera = Tera::default();
76        tera.register_filter("format", format);
77        tera.add_raw_template("test", template).unwrap();
78        tera.render("test", &Context::new()).unwrap()
79    }
80
81    #[test]
82    fn test_float_precision() {
83        assert_eq!(render("{{ 3.14159 | format(spec='.2') }}"), "3.14");
84        assert_eq!(render("{{ 3.14159 | format(spec='.4') }}"), "3.1416");
85    }
86
87    #[test]
88    fn test_integer_padding() {
89        assert_eq!(render("{{ 42 | format(spec='05') }}"), "00042");
90        assert_eq!(render("{{ 42 | format(spec='10') }}"), "        42");
91    }
92
93    #[test]
94    fn test_string_alignment() {
95        assert_eq!(render("{{ 'hi' | format(spec='>5') }}"), "   hi");
96        assert_eq!(render("{{ 'hi' | format(spec='<5') }}"), "hi   ");
97        assert_eq!(render("{{ 'hi' | format(spec='^5') }}"), " hi  ");
98    }
99
100    #[test]
101    fn test_sign() {
102        assert_eq!(render("{{ 42 | format(spec='+') }}"), "+42");
103        assert_eq!(render("{{ -42 | format(spec='+') }}"), "-42");
104    }
105
106    #[test]
107    fn test_bool() {
108        assert_eq!(render("{{ true | format(spec='>8') }}"), "    true");
109    }
110
111    #[test]
112    fn test_combined() {
113        assert_eq!(render("{{ 42 | format(spec='>+10') }}"), "       +42");
114        assert_eq!(render("{{ 3.14159 | format(spec='>10.2') }}"), "      3.14");
115    }
116
117    #[test]
118    fn test_huge_width_rejected() {
119        let mut tera = Tera::default();
120        tera.register_filter("format", format);
121        for spec in ["4000000000", ".4000000000", ">99999999999999999999"] {
122            tera.add_raw_template("test", &format!("{{{{ 1 | format(spec='{spec}') }}}}"))
123                .unwrap();
124            let err = tera.render("test", &Context::new()).unwrap_err();
125            assert!(err.to_string().contains("maximum allowed"), "{err}");
126        }
127    }
128
129    #[test]
130    fn test_dollar_reference_rejected() {
131        let mut tera = Tera::default();
132        tera.register_filter("format", format);
133        // `0$` would use the value (50000) as the width
134        for spec in ["0$", "0$.2", ">0$"] {
135            tera.add_raw_template("test", &format!("{{{{ 50000 | format(spec='{spec}') }}}}"))
136                .unwrap();
137            let err = tera.render("test", &Context::new()).unwrap_err();
138            assert!(err.to_string().contains('$'), "{err}");
139        }
140    }
141}