1use tera::value::ValueKind;
2use tera::{Error, Kwargs, State, TeraResult, Value};
3
4const MAX_SPEC_NUM: usize = 100;
6
7pub 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 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}