1use crate::value::{string_char_count, VmValue};
13
14use super::error::TemplateError;
15use super::render::{display_value, truthy};
16
17type FilterBody = fn(&VmValue, &[VmValue]) -> Result<VmValue, String>;
21
22pub struct Filter {
25 pub name: &'static str,
27 pub params: &'static [&'static str],
30 pub required: usize,
32 pub summary: &'static str,
34 body: FilterBody,
35}
36
37impl Filter {
38 pub fn signature(&self) -> String {
41 if self.params.is_empty() {
42 return self.name.to_string();
43 }
44 let mut out = format!("{}: ", self.name);
45 for (index, param) in self.params.iter().enumerate() {
46 if index == self.required {
50 out.push('[');
51 }
52 if index > 0 {
53 out.push_str(", ");
54 }
55 out.push_str(param);
56 }
57 if self.required < self.params.len() {
58 out.push(']');
59 }
60 out
61 }
62}
63
64pub static FILTERS: &[Filter] = &[
66 Filter {
67 name: "upper",
68 params: &[],
69 required: 0,
70 summary: "Uppercase the value.",
71 body: |v, _| Ok(str_value(display_value(v).to_uppercase())),
72 },
73 Filter {
74 name: "lower",
75 params: &[],
76 required: 0,
77 summary: "Lowercase the value.",
78 body: |v, _| Ok(str_value(display_value(v).to_lowercase())),
79 },
80 Filter {
81 name: "trim",
82 params: &[],
83 required: 0,
84 summary: "Strip leading and trailing whitespace.",
85 body: |v, _| Ok(str_value(display_value(v).trim().to_string())),
86 },
87 Filter {
88 name: "capitalize",
89 params: &[],
90 required: 0,
91 summary: "Uppercase the first character and lowercase the rest.",
92 body: |v, _| Ok(str_value(capitalize(&display_value(v)))),
93 },
94 Filter {
95 name: "title",
96 params: &[],
97 required: 0,
98 summary: "Uppercase the first character of every word.",
99 body: |v, _| Ok(str_value(title_case(&display_value(v)))),
100 },
101 Filter {
102 name: "length",
103 params: &[],
104 required: 0,
105 summary: "Number of characters, items, or entries.",
106 body: |v, _| length(v),
107 },
108 Filter {
109 name: "first",
110 params: &[],
111 required: 0,
112 summary: "First item of a list or set, or first character of a string.",
113 body: |v, _| Ok(first(v)),
114 },
115 Filter {
116 name: "last",
117 params: &[],
118 required: 0,
119 summary: "Last item of a list or set, or last character of a string.",
120 body: |v, _| Ok(last(v)),
121 },
122 Filter {
123 name: "reverse",
124 params: &[],
125 required: 0,
126 summary: "Reverse a list or string. Other values pass through unchanged.",
127 body: |v, _| Ok(reverse(v)),
128 },
129 Filter {
130 name: "join",
131 params: &["separator"],
132 required: 1,
133 summary: "Join a list or set into a string with the given separator.",
134 body: |v, args| join(v, &args[0]),
135 },
136 Filter {
137 name: "default",
138 params: &["fallback"],
139 required: 1,
140 summary: "Substitute the fallback when the value is falsy.",
141 body: |v, args| {
142 Ok(if truthy(v) {
143 v.clone()
144 } else {
145 args[0].clone()
146 })
147 },
148 },
149 Filter {
150 name: "json",
151 params: &["pretty"],
152 required: 0,
153 summary: "Serialize the value as JSON, optionally pretty-printed.",
154 body: |v, args| json(v, args.first().map(truthy).unwrap_or(false)),
155 },
156 Filter {
157 name: "indent",
158 params: &["width", "indent_first"],
159 required: 1,
160 summary: "Indent every line by `width` spaces, skipping the first line unless \
161 `indent_first` is true.",
162 body: |v, args| indent(v, &args[0], args.get(1).map(truthy).unwrap_or(false)),
163 },
164 Filter {
165 name: "lines",
166 params: &[],
167 required: 0,
168 summary: "Split the value into a list of lines.",
169 body: |v, _| Ok(lines(v)),
170 },
171 Filter {
172 name: "escape_md",
173 params: &[],
174 required: 0,
175 summary: "Backslash-escape Markdown punctuation.",
176 body: |v, _| Ok(str_value(escape_md(&display_value(v)))),
177 },
178 Filter {
179 name: "replace",
180 params: &["from", "to"],
181 required: 2,
182 summary: "Replace every occurrence of `from` with `to`.",
183 body: |v, args| {
184 let s = display_value(v);
185 let from = display_value(&args[0]);
186 let to = display_value(&args[1]);
187 Ok(str_value(s.replace(&from, &to)))
188 },
189 },
190];
191
192pub fn lookup(name: &str) -> Option<&'static Filter> {
194 FILTERS.iter().find(|filter| filter.name == name)
195}
196
197pub(super) fn apply_filter(
198 name: &str,
199 v: &VmValue,
200 args: &[VmValue],
201 line: usize,
202 col: usize,
203) -> Result<VmValue, TemplateError> {
204 let Some(filter) = lookup(name) else {
205 return Err(TemplateError::new(
206 line,
207 col,
208 format!("unknown filter `{name}`"),
209 ));
210 };
211 if args.len() < filter.required || args.len() > filter.params.len() {
212 return Err(TemplateError::new(
213 line,
214 col,
215 format!("filter `{name}` got wrong number of arguments"),
216 ));
217 }
218 (filter.body)(v, args).map_err(|message| TemplateError::new(line, col, message))
219}
220
221fn str_value(s: String) -> VmValue {
222 VmValue::String(arcstr::ArcStr::from(s))
223}
224
225fn capitalize(s: &str) -> String {
226 let mut out = String::with_capacity(s.len());
227 let mut chars = s.chars();
228 if let Some(c) = chars.next() {
229 out.extend(c.to_uppercase());
230 }
231 for c in chars {
232 out.extend(c.to_lowercase());
233 }
234 out
235}
236
237fn title_case(s: &str) -> String {
238 let mut out = String::with_capacity(s.len());
239 let mut at_start = true;
240 for c in s.chars() {
241 if c.is_whitespace() {
242 at_start = true;
243 out.push(c);
244 } else if at_start {
245 out.extend(c.to_uppercase());
246 at_start = false;
247 } else {
248 out.extend(c.to_lowercase());
249 }
250 }
251 out
252}
253
254fn length(v: &VmValue) -> Result<VmValue, String> {
255 let n: i64 = match v {
256 VmValue::String(s) => string_char_count(s) as i64,
257 VmValue::List(items) => items.len() as i64,
258 VmValue::Set(items) => items.len() as i64,
259 VmValue::Dict(d) => d.len() as i64,
260 VmValue::Range(r) => r.len(),
261 VmValue::Nil => 0,
262 other => return Err(format!("`length` not defined for {}", other.type_name())),
263 };
264 Ok(VmValue::Int(n))
265}
266
267fn first(v: &VmValue) -> VmValue {
268 match v {
269 VmValue::List(items) => items.first().cloned().unwrap_or(VmValue::Nil),
270 VmValue::Set(set) => set.items().first().cloned().unwrap_or(VmValue::Nil),
271 VmValue::String(s) => s
272 .chars()
273 .next()
274 .map(|c| str_value(c.to_string()))
275 .unwrap_or(VmValue::Nil),
276 _ => VmValue::Nil,
277 }
278}
279
280fn last(v: &VmValue) -> VmValue {
281 match v {
282 VmValue::List(items) => items.last().cloned().unwrap_or(VmValue::Nil),
283 VmValue::Set(set) => set.items().last().cloned().unwrap_or(VmValue::Nil),
284 VmValue::String(s) => s
285 .chars()
286 .last()
287 .map(|c| str_value(c.to_string()))
288 .unwrap_or(VmValue::Nil),
289 _ => VmValue::Nil,
290 }
291}
292
293fn reverse(v: &VmValue) -> VmValue {
294 match v {
295 VmValue::List(items) => {
296 let mut out: Vec<VmValue> = items.as_ref().clone();
297 out.reverse();
298 VmValue::List(std::sync::Arc::new(out))
299 }
300 VmValue::String(s) => str_value(s.chars().rev().collect::<String>()),
301 _ => v.clone(),
302 }
303}
304
305fn join(v: &VmValue, separator: &VmValue) -> Result<VmValue, String> {
306 let sep = display_value(separator);
307 let parts: Vec<String> = match v {
308 VmValue::List(items) => items.iter().map(display_value).collect(),
309 VmValue::Set(items) => items.iter().map(display_value).collect(),
310 VmValue::String(s) => return Ok(VmValue::String(s.clone())),
311 _ => return Err(format!("`join` requires a list (got {})", v.type_name())),
312 };
313 Ok(str_value(parts.join(&sep)))
314}
315
316fn json(v: &VmValue, pretty: bool) -> Result<VmValue, String> {
317 let jv = crate::llm::helpers::vm_value_to_json(v);
318 let s = if pretty {
319 serde_json::to_string_pretty(&jv)
320 } else {
321 serde_json::to_string(&jv)
322 }
323 .map_err(|e| format!("json serialization: {e}"))?;
324 Ok(str_value(s))
325}
326
327fn indent(v: &VmValue, width: &VmValue, indent_first: bool) -> Result<VmValue, String> {
328 let VmValue::Int(n) = width else {
329 return Err("`indent` requires an integer width".to_string());
330 };
331 let n = (*n).max(0) as usize;
332 let pad: String = " ".repeat(n);
333 let s = display_value(v);
334 let mut out = String::with_capacity(s.len() + n * 4);
335 for (i, line) in s.split('\n').enumerate() {
336 if i > 0 {
337 out.push('\n');
338 }
339 if !line.is_empty() && (i > 0 || indent_first) {
340 out.push_str(&pad);
341 }
342 out.push_str(line);
343 }
344 Ok(str_value(out))
345}
346
347fn lines(v: &VmValue) -> VmValue {
348 let s = display_value(v);
349 let list: Vec<VmValue> = s.split('\n').map(|p| str_value(p.to_string())).collect();
350 VmValue::List(std::sync::Arc::new(list))
351}
352
353fn escape_md(s: &str) -> String {
354 let mut out = String::with_capacity(s.len() + 8);
355 for c in s.chars() {
356 match c {
357 '\\' | '`' | '*' | '_' | '{' | '}' | '[' | ']' | '(' | ')' | '#' | '+' | '-' | '.'
358 | '!' | '|' | '<' | '>' => {
359 out.push('\\');
360 out.push(c);
361 }
362 _ => out.push(c),
363 }
364 }
365 out
366}
367
368#[cfg(test)]
369mod tests {
370 use super::{apply_filter, lookup, FILTERS};
371 use crate::value::VmValue;
372
373 #[test]
374 fn filter_names_are_unique() {
375 let mut names: Vec<&str> = FILTERS.iter().map(|filter| filter.name).collect();
376 let before = names.len();
377 names.sort_unstable();
378 names.dedup();
379 assert_eq!(before, names.len(), "duplicate filter name in the table");
380 }
381
382 #[test]
383 fn required_arguments_are_a_prefix_of_the_parameter_list() {
384 for filter in FILTERS {
385 assert!(
386 filter.required <= filter.params.len(),
387 "`{}` requires more arguments than it declares",
388 filter.name
389 );
390 }
391 }
392
393 #[test]
394 fn signatures_mark_optional_arguments() {
395 assert_eq!(lookup("upper").unwrap().signature(), "upper");
396 assert_eq!(lookup("join").unwrap().signature(), "join: separator");
397 assert_eq!(lookup("replace").unwrap().signature(), "replace: from, to");
398 assert_eq!(lookup("json").unwrap().signature(), "json: [pretty]");
399 assert_eq!(
400 lookup("indent").unwrap().signature(),
401 "indent: width[, indent_first]"
402 );
403 }
404
405 #[test]
406 fn unknown_names_are_not_filters() {
407 assert!(lookup("endif").is_none());
408 assert!(lookup("").is_none());
409 }
410
411 #[test]
416 fn dispatch_covers_exactly_the_declared_filters() {
417 for filter in FILTERS {
418 let args = vec![VmValue::Nil; filter.required];
419 if let Err(error) = apply_filter(filter.name, &VmValue::Nil, &args, 1, 1) {
422 assert!(
423 !error.kind.contains("unknown filter"),
424 "`{}` is declared but not dispatched",
425 filter.name
426 );
427 }
428 }
429
430 let error = apply_filter("uppercase", &VmValue::Nil, &[], 1, 1)
431 .expect_err("an undeclared name is not a filter");
432 assert!(error.kind.contains("unknown filter"), "got {}", error.kind);
433 }
434}