Skip to main content

plf_contrib/
format.rs

1use plf::value::ValueKind;
2use plf::{Error, Kwargs, State, TeraResult, Value};
3
4/// Formats a value using Rust's std::fmt format specifiers.
5///
6/// Supports width, alignment, precision, sign, and zero-padding.
7/// Does NOT support radix specifiers (x, X, b, o) - only Display formatting.
8///
9/// # Example
10/// ```text
11/// {{ 3.14159 | format(spec=".2") }} -> "3.14"
12/// {{ 42 | format(spec="05") }} -> "00042"
13/// {{ "hi" | format(spec=">5") }} -> "   hi"
14/// {{ 42 | format(spec="+") }} -> "+42"
15/// ```
16pub fn format(val: Value, kwargs: Kwargs, _: &State) -> TeraResult<String> {
17    let spec = kwargs.must_get::<&str>("spec")?;
18    let fmt_str = format!("{{:{}}}", spec);
19
20    match val.kind() {
21        ValueKind::String => {
22            let s = val.as_str().unwrap();
23            formatx::formatx!(&fmt_str, s)
24                .map_err(|e| Error::message(format!("format error: {}", e)))
25        }
26        ValueKind::I64 | ValueKind::I128 | ValueKind::U64 => {
27            let n = val.as_i128().unwrap();
28            formatx::formatx!(&fmt_str, n)
29                .map_err(|e| Error::message(format!("format error: {}", e)))
30        }
31        ValueKind::U128 => {
32            let n = val.as_u128().unwrap();
33            formatx::formatx!(&fmt_str, n)
34                .map_err(|e| Error::message(format!("format error: {}", e)))
35        }
36        ValueKind::F64 => {
37            let n = val.as_number().unwrap();
38            let f = n.as_float();
39            formatx::formatx!(&fmt_str, f)
40                .map_err(|e| Error::message(format!("format error: {}", e)))
41        }
42        ValueKind::Bool => {
43            let b = val.as_bool().unwrap();
44            formatx::formatx!(&fmt_str, b)
45                .map_err(|e| Error::message(format!("format error: {}", e)))
46        }
47        _ => Err(Error::message(format!(
48            "Cannot format value of type {} with format filter",
49            val.name()
50        ))),
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use plf::{Context, Tera};
58
59    fn render(template: &str) -> String {
60        let mut tera = Tera::default();
61        tera.register_filter("format", format);
62        tera.add_raw_template("test", template).unwrap();
63        tera.render("test", &Context::new()).unwrap()
64    }
65
66    #[test]
67    fn test_float_precision() {
68        assert_eq!(render("{{ 3.14159 | format(spec='.2') }}"), "3.14");
69        assert_eq!(render("{{ 3.14159 | format(spec='.4') }}"), "3.1416");
70    }
71
72    #[test]
73    fn test_integer_padding() {
74        assert_eq!(render("{{ 42 | format(spec='05') }}"), "00042");
75        assert_eq!(render("{{ 42 | format(spec='10') }}"), "        42");
76    }
77
78    #[test]
79    fn test_string_alignment() {
80        assert_eq!(render("{{ 'hi' | format(spec='>5') }}"), "   hi");
81        assert_eq!(render("{{ 'hi' | format(spec='<5') }}"), "hi   ");
82        assert_eq!(render("{{ 'hi' | format(spec='^5') }}"), " hi  ");
83    }
84
85    #[test]
86    fn test_sign() {
87        assert_eq!(render("{{ 42 | format(spec='+') }}"), "+42");
88        assert_eq!(render("{{ -42 | format(spec='+') }}"), "-42");
89    }
90
91    #[test]
92    fn test_bool() {
93        assert_eq!(render("{{ true | format(spec='>8') }}"), "    true");
94    }
95
96    #[test]
97    fn test_combined() {
98        assert_eq!(render("{{ 42 | format(spec='>+10') }}"), "       +42");
99        assert_eq!(render("{{ 3.14159 | format(spec='>10.2') }}"), "      3.14");
100    }
101}