1use regex::Regex;
2use std::borrow::Cow;
3use std::collections::HashMap;
4use std::sync::LazyLock;
5use tera::Value;
6
7use sha1::Digest as Sha1Digest;
8use sha2::Digest as Sha2Digest;
9use sha3::Digest as Sha3Digest;
10
11use crate::path_util::expand_tilde;
14
15fn value_to_string(v: &Value) -> Cow<'_, str> {
19 match v {
20 Value::String(s) => Cow::Borrowed(s.as_str()),
21 Value::Number(n) => Cow::Owned(n.to_string()),
22 Value::Bool(b) => Cow::Owned(b.to_string()),
23 Value::Null => Cow::Borrowed(""),
24 other => Cow::Owned(other.to_string()),
26 }
27}
28
29fn printf_default(v: &Value) -> String {
34 value_to_string(v).into_owned()
35}
36
37#[derive(Clone, Copy)]
39struct PrintfSpec {
40 minus: bool,
41 plus: bool,
42 space: bool,
43 zero: bool,
44 hash: bool,
45 width: Option<usize>,
46 precision: Option<usize>,
47 verb: char,
48}
49
50fn pad(spec: &PrintfSpec, body: String, numeric_sign_prefix: Option<(&str, &str)>) -> String {
54 let (sign, prefix, core) = match numeric_sign_prefix {
55 Some((sign, prefix)) => (sign, prefix, body.as_str()),
56 None => ("", "", body.as_str()),
57 };
58 let assembled = format!("{}{}{}", sign, prefix, core);
59 let Some(width) = spec.width else {
60 return assembled;
61 };
62 let len = assembled.chars().count();
63 if len >= width {
64 return assembled;
65 }
66 let padding = width - len;
67 if spec.minus {
68 format!("{}{}", assembled, " ".repeat(padding))
69 } else if spec.zero && numeric_sign_prefix.is_some() {
70 format!("{}{}{}{}", sign, prefix, "0".repeat(padding), core)
72 } else if spec.zero {
73 format!("{}{}", "0".repeat(padding), assembled)
74 } else {
75 format!("{}{}", " ".repeat(padding), assembled)
76 }
77}
78
79fn pad_int(spec: &PrintfSpec, body: String, sign: &str, prefix: &str) -> String {
86 if spec.precision.is_some() && spec.zero {
87 let no_zero = PrintfSpec {
88 zero: false,
89 ..*spec
90 };
91 pad(&no_zero, body, Some((sign, prefix)))
92 } else {
93 pad(spec, body, Some((sign, prefix)))
94 }
95}
96
97fn numeric_sign(negative: bool, plus: bool, space: bool) -> &'static str {
100 if negative {
101 "-"
102 } else if plus {
103 "+"
104 } else if space {
105 " "
106 } else {
107 ""
108 }
109}
110
111fn int_precision(digits: &str, precision: Option<usize>) -> String {
119 match precision {
120 Some(0) if digits == "0" => String::new(),
121 Some(p) if digits.len() < p => format!("{}{}", "0".repeat(p - digits.len()), digits),
122 _ => digits.to_string(),
123 }
124}
125
126fn go_exponent(s: &str, uppercase: bool) -> String {
134 let letter = if uppercase { 'E' } else { 'e' };
135 let Some(pos) = s.find(['e', 'E']) else {
136 return s.to_string();
137 };
138 let (mantissa, exp_part) = s.split_at(pos);
139 let exp_str = &exp_part[1..];
141 let (sign, digits) = match exp_str.strip_prefix('-') {
142 Some(rest) => ('-', rest),
143 None => ('+', exp_str.strip_prefix('+').unwrap_or(exp_str)),
144 };
145 let padded = if digits.len() < 2 {
147 format!("{:0>2}", digits)
148 } else {
149 digits.to_string()
150 };
151 format!("{}{}{}{}", mantissa, letter, sign, padded)
152}
153
154fn trim_fraction_zeros(s: &str) -> &str {
157 if !s.contains('.') {
158 return s;
159 }
160 let trimmed = s.trim_end_matches('0');
161 trimmed.strip_suffix('.').unwrap_or(trimmed)
162}
163
164fn format_g(mag: f64, precision: Option<usize>, uppercase: bool) -> String {
172 let sci = format!("{:e}", mag);
175 let exp: i32 = sci
176 .split(['e', 'E'])
177 .nth(1)
178 .and_then(|e| e.parse().ok())
179 .unwrap_or(0);
180 let eprec = precision.map(|p| p as i32).unwrap_or(6).max(1);
181
182 if exp < -4 || exp >= eprec {
183 let body = match precision {
186 Some(p) => format!("{:.*e}", p.saturating_sub(1), mag),
187 None => sci.clone(),
188 };
189 let normalized = go_exponent(&body, uppercase);
190 if precision.is_some()
192 && let Some(epos) = normalized.find(['e', 'E'])
193 {
194 let (mantissa, exp_part) = normalized.split_at(epos);
195 return format!("{}{}", trim_fraction_zeros(mantissa), exp_part);
196 }
197 normalized
198 } else {
199 let body = match precision {
204 Some(p) => {
206 let frac = (p as i32 - (exp + 1)).max(0) as usize;
207 format!("{:.*}", frac, mag)
208 }
209 None => format!("{}", mag),
210 };
211 trim_fraction_zeros(&body).to_string()
212 }
213}
214
215const PRINTF_FIELD_MAX: usize = 100_000;
218
219fn format_verb(spec: &PrintfSpec, value: Option<&Value>) -> Result<String, tera::Error> {
225 let val = || -> Result<&Value, tera::Error> {
226 value
227 .ok_or_else(|| tera::Error::msg(format!("printf: missing argument for %{}", spec.verb)))
228 };
229 match spec.verb {
230 's' => {
231 let mut s = printf_default(val()?);
232 if let Some(prec) = spec.precision {
233 s = s.chars().take(prec).collect();
234 }
235 Ok(pad(spec, s, None))
236 }
237 'v' => Ok(pad(spec, printf_default(val()?), None)),
238 't' => {
239 let b = val()?
240 .as_bool()
241 .ok_or_else(|| tera::Error::msg("printf: %t expects a boolean argument"))?;
242 Ok(pad(spec, b.to_string(), None))
243 }
244 'q' => {
245 let s = printf_default(val()?);
246 Ok(pad(spec, format!("{:?}", s), None))
247 }
248 'c' => {
249 let v = val()?;
250 let code = v
251 .as_u64()
252 .ok_or_else(|| tera::Error::msg("printf: %c expects a non-negative integer"))?;
253 let ch = u32::try_from(code)
254 .ok()
255 .and_then(char::from_u32)
256 .ok_or_else(|| {
257 tera::Error::msg(format!("printf: %c: {} is not a valid code point", code))
258 })?;
259 Ok(pad(spec, ch.to_string(), None))
260 }
261 'd' => {
262 let n = val()?
263 .as_i64()
264 .ok_or_else(|| tera::Error::msg("printf: %d expects an integer argument"))?;
265 let sign = numeric_sign(n < 0, spec.plus, spec.space);
266 Ok(pad_int(
267 spec,
268 int_precision(&n.unsigned_abs().to_string(), spec.precision),
269 sign,
270 "",
271 ))
272 }
273 'b' | 'o' | 'x' | 'X' => {
274 let n = val()?.as_i64().ok_or_else(|| {
275 tera::Error::msg(format!(
276 "printf: %{} expects an integer argument",
277 spec.verb
278 ))
279 })?;
280 let mag = n.unsigned_abs();
281 let digits = match spec.verb {
282 'b' => format!("{:b}", mag),
283 'o' => format!("{:o}", mag),
284 'x' => format!("{:x}", mag),
285 'X' => format!("{:X}", mag),
286 _ => unreachable!(),
287 };
288 let body = int_precision(&digits, spec.precision);
289 let sign = numeric_sign(n < 0, spec.plus, spec.space);
290 let prefix = if spec.hash {
291 match spec.verb {
292 'b' => "0b",
293 'o' => "0",
294 'x' => "0x",
295 'X' => "0X",
296 _ => "",
297 }
298 } else {
299 ""
300 };
301 Ok(pad_int(spec, body, sign, prefix))
302 }
303 'f' | 'e' | 'E' | 'g' | 'G' => {
304 let f = val()?.as_f64().ok_or_else(|| {
305 tera::Error::msg(format!("printf: %{} expects a numeric argument", spec.verb))
306 })?;
307 let prec = spec.precision.unwrap_or(6);
308 let mag = f.abs();
309 let body = match spec.verb {
310 'f' => format!("{:.*}", prec, mag),
311 'e' | 'E' => go_exponent(&format!("{:.*e}", prec, mag), spec.verb == 'E'),
315 'g' | 'G' => format_g(mag, spec.precision, spec.verb == 'G'),
318 _ => unreachable!(),
319 };
320 let sign = numeric_sign(f.is_sign_negative() && f != 0.0, spec.plus, spec.space);
321 Ok(pad(spec, body, Some((sign, ""))))
322 }
323 other => Err(tera::Error::msg(format!(
324 "printf: unsupported verb %{} (supported: s d v x X o b c q f e E g G t %%)",
325 other
326 ))),
327 }
328}
329
330fn sprintf(format: &str, args: &[Value]) -> Result<String, tera::Error> {
337 let mut out = String::new();
338 let mut arg_idx = 0usize;
339 let mut chars = format.chars().peekable();
340
341 while let Some(c) = chars.next() {
342 if c != '%' {
343 out.push(c);
344 continue;
345 }
346 if chars.peek() == Some(&'%') {
348 chars.next();
349 out.push('%');
350 continue;
351 }
352
353 let mut spec = PrintfSpec {
354 minus: false,
355 plus: false,
356 space: false,
357 zero: false,
358 hash: false,
359 width: None,
360 precision: None,
361 verb: ' ',
362 };
363
364 while let Some(&f) = chars.peek() {
366 match f {
367 '-' => spec.minus = true,
368 '+' => spec.plus = true,
369 ' ' => spec.space = true,
370 '0' => spec.zero = true,
371 '#' => spec.hash = true,
372 _ => break,
373 }
374 chars.next();
375 }
376
377 let mut width_digits = String::new();
379 while let Some(&d) = chars.peek() {
380 if d.is_ascii_digit() {
381 width_digits.push(d);
382 chars.next();
383 } else {
384 break;
385 }
386 }
387 if !width_digits.is_empty() {
388 let w = width_digits.parse::<usize>().unwrap_or(usize::MAX);
390 if w > PRINTF_FIELD_MAX {
391 return Err(tera::Error::msg(format!(
392 "printf width {} exceeds maximum {}",
393 width_digits, PRINTF_FIELD_MAX
394 )));
395 }
396 spec.width = Some(w);
397 }
398
399 if chars.peek() == Some(&'.') {
401 chars.next();
402 let mut prec_digits = String::new();
403 while let Some(&d) = chars.peek() {
404 if d.is_ascii_digit() {
405 prec_digits.push(d);
406 chars.next();
407 } else {
408 break;
409 }
410 }
411 let p = if prec_digits.is_empty() {
413 0
414 } else {
415 prec_digits.parse::<usize>().unwrap_or(usize::MAX)
416 };
417 if p > PRINTF_FIELD_MAX {
418 return Err(tera::Error::msg(format!(
419 "printf precision {} exceeds maximum {}",
420 prec_digits, PRINTF_FIELD_MAX
421 )));
422 }
423 spec.precision = Some(p);
424 }
425
426 let verb = chars
427 .next()
428 .ok_or_else(|| tera::Error::msg("printf: format string ends with a dangling '%'"))?;
429 spec.verb = verb;
430
431 let value = args.get(arg_idx);
432 out.push_str(&format_verb(&spec, value)?);
433 arg_idx += 1;
435 }
436
437 Ok(out)
438}
439
440pub(super) fn translate_go_time_format(fmt: &str) -> Cow<'_, str> {
447 if fmt.contains('%') {
449 return Cow::Borrowed(fmt);
450 }
451
452 const GO_MARKERS: &[&str] = &[
455 "2006", "06", "January", "Jan", "01", "Monday", "Mon", "02", "15", "03", "04", "05", "PM",
456 "pm", "-0700", "Z0700", "MST",
457 ];
458 let has_go_patterns = GO_MARKERS.iter().any(|p| fmt.contains(p));
459 if !has_go_patterns {
460 return Cow::Borrowed(fmt);
461 }
462
463 let mut result = fmt.to_string();
467
468 let replacements: &[(&str, &str)] = &[
469 ("January", "%B"), ("Monday", "%A"), ("-0700", "%z"), ("Z0700", "%z"), ("2006", "%Y"), ("Jan", "%b"), ("Mon", "%a"), ("MST", "%Z"), ("PM", "%p"), ("pm", "%P"), ("15", "%H"), ("06", "%y"), ("05", "%S"), ("04", "%M"), ("03", "%I"), ("02", "%d"), ("01", "%m"), ];
488
489 for (go_pat, chrono_pat) in replacements {
490 result = result.replace(go_pat, chrono_pat);
491 }
492
493 Cow::Owned(result)
494}
495
496enum VersionPart {
497 Major,
498 Minor,
499 Patch,
500}
501
502fn increment_version(v: &str, part: VersionPart) -> Result<String, tera::Error> {
510 let stripped = v.strip_prefix('v').unwrap_or(v);
511 let parts: Vec<&str> = stripped.splitn(3, '.').collect();
512 let invalid = || {
513 tera::Error::msg(format!(
514 "incpatch/incminor/incmajor: '{}' is not a valid semver version (expected MAJOR.MINOR.PATCH)",
515 v
516 ))
517 };
518 if parts.len() < 3 {
519 return Err(invalid());
520 }
521 let major: u64 = parts
522 .first()
523 .and_then(|s| s.parse().ok())
524 .ok_or_else(invalid)?;
525 let minor: u64 = parts
526 .get(1)
527 .and_then(|s| s.parse().ok())
528 .ok_or_else(invalid)?;
529 let patch: u64 = parts
530 .get(2)
531 .and_then(|s| {
532 s.split('-').next().and_then(|n| n.parse().ok())
534 })
535 .ok_or_else(invalid)?;
536 let prefix = if v.starts_with('v') { "v" } else { "" };
537 Ok(match part {
538 VersionPart::Major => format!("{}{}.0.0", prefix, major + 1),
539 VersionPart::Minor => format!("{}{}.{}.0", prefix, major, minor + 1),
540 VersionPart::Patch => format!("{}{}.{}.{}", prefix, major, minor, patch + 1),
541 })
542}
543
544pub fn ruby_escape_str(s: &str) -> String {
554 s.replace('\\', "\\\\").replace('"', "\\\"")
555}
556
557pub(super) fn register_ruby_escape(tera: &mut tera::Tera) {
568 tera.register_filter(
569 "ruby_escape",
570 |value: &Value, _: &HashMap<String, Value>| {
571 let s = tera::try_get_value!("ruby_escape", "value", String, value);
572 Ok(Value::String(ruby_escape_str(&s)))
573 },
574 );
575}
576
577pub(super) static BASE_TERA: LazyLock<tera::Tera> = LazyLock::new(|| {
580 let mut tera = tera::Tera::default();
581 register_ruby_escape(&mut tera);
582
583 tera.register_filter("tolower", |value: &Value, _: &HashMap<String, Value>| {
585 let s = tera::try_get_value!("tolower", "value", String, value);
586 Ok(Value::String(s.to_lowercase()))
587 });
588 tera.register_filter("toupper", |value: &Value, _: &HashMap<String, Value>| {
589 let s = tera::try_get_value!("toupper", "value", String, value);
590 Ok(Value::String(s.to_uppercase()))
591 });
592
593 tera.register_filter(
595 "trimprefix",
596 |value: &Value, args: &HashMap<String, Value>| {
597 let s = tera::try_get_value!("trimprefix", "value", String, value);
598 let prefix = args
599 .get("prefix")
600 .and_then(|v| v.as_str())
601 .ok_or_else(|| tera::Error::msg("trimprefix requires a `prefix` argument"))?;
602 let result = s.strip_prefix(prefix).unwrap_or(&s);
603 Ok(Value::String(result.to_string()))
604 },
605 );
606
607 tera.register_filter(
609 "trimsuffix",
610 |value: &Value, args: &HashMap<String, Value>| {
611 let s = tera::try_get_value!("trimsuffix", "value", String, value);
612 let suffix = args
613 .get("suffix")
614 .and_then(|v| v.as_str())
615 .ok_or_else(|| tera::Error::msg("trimsuffix requires a `suffix` argument"))?;
616 let result = s.strip_suffix(suffix).unwrap_or(&s);
617 Ok(Value::String(result.to_string()))
618 },
619 );
620
621 tera.register_function(
627 "envOrDefault",
628 |args: &HashMap<String, Value>| -> tera::Result<Value> {
629 let name = args
630 .get("name")
631 .and_then(|v| v.as_str())
632 .ok_or_else(|| tera::Error::msg("envOrDefault requires `name` argument"))?;
633 let default = args.get("default").and_then(|v| v.as_str()).unwrap_or("");
634 let value = std::env::var(name).unwrap_or_else(|_| default.to_string());
635 Ok(Value::String(value))
636 },
637 );
638 tera.register_function(
639 "isEnvSet",
640 |args: &HashMap<String, Value>| -> tera::Result<Value> {
641 let name = args
642 .get("name")
643 .and_then(|v| v.as_str())
644 .ok_or_else(|| tera::Error::msg("isEnvSet requires `name` argument"))?;
645 let is_set = std::env::var(name).map(|v| !v.is_empty()).unwrap_or(false);
646 Ok(Value::Bool(is_set))
647 },
648 );
649
650 tera.register_function(
654 "incpatch",
655 |args: &HashMap<String, Value>| -> tera::Result<Value> {
656 let v = args
657 .get("v")
658 .and_then(|v| v.as_str())
659 .ok_or_else(|| tera::Error::msg("incpatch requires `v` argument"))?;
660 Ok(Value::String(increment_version(v, VersionPart::Patch)?))
661 },
662 );
663
664 tera.register_function(
666 "incminor",
667 |args: &HashMap<String, Value>| -> tera::Result<Value> {
668 let v = args
669 .get("v")
670 .and_then(|v| v.as_str())
671 .ok_or_else(|| tera::Error::msg("incminor requires `v` argument"))?;
672 Ok(Value::String(increment_version(v, VersionPart::Minor)?))
673 },
674 );
675
676 tera.register_function(
678 "incmajor",
679 |args: &HashMap<String, Value>| -> tera::Result<Value> {
680 let v = args
681 .get("v")
682 .and_then(|v| v.as_str())
683 .ok_or_else(|| tera::Error::msg("incmajor requires `v` argument"))?;
684 Ok(Value::String(increment_version(v, VersionPart::Major)?))
685 },
686 );
687
688 macro_rules! register_hash_fn {
691 ($tera:expr, $name:expr, $hash_fn:expr) => {
692 $tera.register_function(
693 $name,
694 |args: &HashMap<String, Value>| -> tera::Result<Value> {
695 let s = args.get("s").and_then(|v| v.as_str()).ok_or_else(|| {
696 tera::Error::msg(format!("{} requires `s` argument", $name))
697 })?;
698 let bytes = std::fs::read(s).map_err(|e| {
700 tera::Error::msg(format!("{}: failed to read file '{}': {}", $name, s, e))
701 })?;
702 Ok(Value::String($hash_fn(&bytes)))
703 },
704 );
705 };
706 }
707
708 register_hash_fn!(tera, "sha1", |b: &[u8]| {
709 let mut h = sha1::Sha1::new();
710 Sha1Digest::update(&mut h, b);
711 crate::hashing::hex_lower(&Sha1Digest::finalize(h))
712 });
713 register_hash_fn!(tera, "sha224", |b: &[u8]| {
714 let mut h = sha2::Sha224::new();
715 Sha2Digest::update(&mut h, b);
716 crate::hashing::hex_lower(&Sha2Digest::finalize(h))
717 });
718 register_hash_fn!(tera, "sha256", |b: &[u8]| {
719 let mut h = sha2::Sha256::new();
720 Sha2Digest::update(&mut h, b);
721 crate::hashing::hex_lower(&Sha2Digest::finalize(h))
722 });
723 register_hash_fn!(tera, "sha384", |b: &[u8]| {
724 let mut h = sha2::Sha384::new();
725 Sha2Digest::update(&mut h, b);
726 crate::hashing::hex_lower(&Sha2Digest::finalize(h))
727 });
728 register_hash_fn!(tera, "sha512", |b: &[u8]| {
729 let mut h = sha2::Sha512::new();
730 Sha2Digest::update(&mut h, b);
731 crate::hashing::hex_lower(&Sha2Digest::finalize(h))
732 });
733 register_hash_fn!(tera, "sha3_224", |b: &[u8]| {
734 let mut h = sha3::Sha3_224::new();
735 Sha3Digest::update(&mut h, b);
736 crate::hashing::hex_lower(&Sha3Digest::finalize(h))
737 });
738 register_hash_fn!(tera, "sha3_256", |b: &[u8]| {
739 let mut h = sha3::Sha3_256::new();
740 Sha3Digest::update(&mut h, b);
741 crate::hashing::hex_lower(&Sha3Digest::finalize(h))
742 });
743 register_hash_fn!(tera, "sha3_384", |b: &[u8]| {
744 let mut h = sha3::Sha3_384::new();
745 Sha3Digest::update(&mut h, b);
746 crate::hashing::hex_lower(&Sha3Digest::finalize(h))
747 });
748 register_hash_fn!(tera, "sha3_512", |b: &[u8]| {
749 let mut h = sha3::Sha3_512::new();
750 Sha3Digest::update(&mut h, b);
751 crate::hashing::hex_lower(&Sha3Digest::finalize(h))
752 });
753 register_hash_fn!(tera, "blake2b", |b: &[u8]| {
754 let mut h = blake2::Blake2b512::new();
755 blake2::Digest::update(&mut h, b);
756 crate::hashing::hex_lower(&blake2::Digest::finalize(h))
757 });
758 register_hash_fn!(tera, "blake2s", |b: &[u8]| {
759 let mut h = blake2::Blake2s256::new();
760 blake2::Digest::update(&mut h, b);
761 crate::hashing::hex_lower(&blake2::Digest::finalize(h))
762 });
763 register_hash_fn!(tera, "blake3", |b: &[u8]| {
764 crate::hashing::hex_lower(blake3::hash(b).as_bytes())
765 });
766 register_hash_fn!(tera, "md5", |b: &[u8]| {
767 let mut h = md5::Md5::new();
768 md5::Digest::update(&mut h, b);
769 crate::hashing::hex_lower(&md5::Digest::finalize(h))
770 });
771 register_hash_fn!(tera, "crc32", |b: &[u8]| {
772 format!("{:08x}", crc32fast::hash(b))
773 });
774
775 tera.register_function(
781 "readFile",
782 |args: &HashMap<String, Value>| -> tera::Result<Value> {
783 let path = args
784 .get("path")
785 .and_then(|v| v.as_str())
786 .ok_or_else(|| tera::Error::msg("readFile requires `path` argument"))?;
787 let resolved = expand_tilde(path);
788 let content = std::fs::read_to_string(resolved.as_ref()).unwrap_or_default();
789 Ok(Value::String(content.trim().to_string()))
790 },
791 );
792
793 tera.register_function(
796 "mustReadFile",
797 |args: &HashMap<String, Value>| -> tera::Result<Value> {
798 let path = args
799 .get("path")
800 .and_then(|v| v.as_str())
801 .ok_or_else(|| tera::Error::msg("mustReadFile requires `path` argument"))?;
802 let resolved = expand_tilde(path);
803 let content = std::fs::read_to_string(resolved.as_ref())
804 .map_err(|e| tera::Error::msg(format!("mustReadFile: {}: {}", resolved, e)))?;
805 Ok(Value::String(content.trim().to_string()))
806 },
807 );
808
809 tera.register_function(
818 "time",
819 |args: &HashMap<String, Value>| -> tera::Result<Value> {
820 let fmt = args
821 .get("format")
822 .and_then(|v| v.as_str())
823 .unwrap_or("%Y-%m-%dT%H:%M:%SZ");
824 let chrono_fmt = translate_go_time_format(fmt);
825 let now = crate::sde::resolve_now();
826 Ok(Value::String(now.format(&chrono_fmt).to_string()))
827 },
828 );
829
830 tera.register_filter("dir", |value: &Value, _: &HashMap<String, Value>| {
834 let s = tera::try_get_value!("dir", "value", String, value);
835 let p = std::path::Path::new(&s);
836 Ok(Value::String(
837 p.parent()
838 .map(|p| p.to_string_lossy().to_string())
839 .unwrap_or_default(),
840 ))
841 });
842
843 tera.register_filter("base", |value: &Value, _: &HashMap<String, Value>| {
845 let s = tera::try_get_value!("base", "value", String, value);
846 let p = std::path::Path::new(&s);
847 Ok(Value::String(
848 p.file_name()
849 .map(|n| n.to_string_lossy().to_string())
850 .unwrap_or_default(),
851 ))
852 });
853
854 tera.register_filter("abs", |value: &Value, _: &HashMap<String, Value>| {
856 let s = tera::try_get_value!("abs", "value", String, value);
857 let p = std::path::Path::new(&s);
858 if p.is_absolute() {
859 Ok(Value::String(s))
860 } else {
861 let abs = std::env::current_dir()
862 .map(|cwd| cwd.join(p).to_string_lossy().to_string())
863 .unwrap_or(s);
864 Ok(Value::String(abs))
865 }
866 });
867
868 tera.register_filter(
870 "urlPathEscape",
871 |value: &Value, _: &HashMap<String, Value>| {
872 let s = tera::try_get_value!("urlPathEscape", "value", String, value);
873 let encoded: String = s
876 .bytes()
877 .map(|b| {
878 if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.' || b == b'~'
879 {
880 (b as char).to_string()
881 } else {
882 format!("%{:02X}", b)
883 }
884 })
885 .collect();
886 Ok(Value::String(encoded))
887 },
888 );
889
890 tera.register_filter("mdv2escape", |value: &Value, _: &HashMap<String, Value>| {
892 let s = tera::try_get_value!("mdv2escape", "value", String, value);
893 let escaped = s
894 .chars()
895 .map(|c| {
896 if "_*[]()~`>#+-=|{}.!".contains(c) {
897 format!("\\{}", c)
898 } else {
899 c.to_string()
900 }
901 })
902 .collect::<String>();
903 Ok(Value::String(escaped))
904 });
905
906 tera.register_function(
910 "contains",
911 |args: &HashMap<String, Value>| -> tera::Result<Value> {
912 let s = args
913 .get("s")
914 .and_then(|v| v.as_str())
915 .ok_or_else(|| tera::Error::msg("contains requires `s` argument"))?;
916 let substr = args
917 .get("substr")
918 .and_then(|v| v.as_str())
919 .ok_or_else(|| tera::Error::msg("contains requires `substr` argument"))?;
920 Ok(Value::Bool(s.contains(substr)))
921 },
922 );
923
924 tera.register_function(
930 "list",
931 |args: &HashMap<String, Value>| -> tera::Result<Value> {
932 let items = args
933 .get("items")
934 .and_then(|v| v.as_array())
935 .ok_or_else(|| tera::Error::msg("list requires `items` argument"))?;
936 Ok(Value::Array(items.clone()))
937 },
938 );
939
940 tera.register_function(
943 "map",
944 |args: &HashMap<String, Value>| -> tera::Result<Value> {
945 let pairs = args
946 .get("pairs")
947 .and_then(|v| v.as_array())
948 .ok_or_else(|| tera::Error::msg("map requires `pairs` argument"))?;
949 if pairs.len() % 2 != 0 {
950 return Err(tera::Error::msg(
951 "map requires an even number of arguments (key-value pairs)",
952 ));
953 }
954 let mut result = tera::Map::new();
955 for chunk in pairs.chunks(2) {
956 let key = chunk[0].as_str().unwrap_or("").to_string();
957 result.insert(key, chunk[1].clone());
958 }
959 Ok(Value::Object(result))
960 },
961 );
962
963 let in_fn = |args: &HashMap<String, Value>| -> tera::Result<Value> {
968 let items = args
969 .get("items")
970 .and_then(|v| v.as_array())
971 .ok_or_else(|| tera::Error::msg("in requires `items` argument (must be an array)"))?;
972 let value = args
973 .get("value")
974 .ok_or_else(|| tera::Error::msg("in requires `value` argument"))?;
975 let needle = value_to_string(value);
977 let found = items.iter().any(|item| value_to_string(item) == needle);
978 Ok(Value::Bool(found))
979 };
980 tera.register_function("in", in_fn);
981 tera.register_function("contains_any", in_fn);
984
985 tera.register_function(
993 "reReplaceAll",
994 |args: &HashMap<String, Value>| -> tera::Result<Value> {
995 let pattern = args
996 .get("pattern")
997 .and_then(|v| v.as_str())
998 .ok_or_else(|| tera::Error::msg("reReplaceAll requires `pattern` argument"))?;
999 let input = args
1000 .get("input")
1001 .and_then(|v| v.as_str())
1002 .ok_or_else(|| tera::Error::msg("reReplaceAll requires `input` argument"))?;
1003 let replacement = args
1004 .get("replacement")
1005 .and_then(|v| v.as_str())
1006 .ok_or_else(|| tera::Error::msg("reReplaceAll requires `replacement` argument"))?;
1007 let re = Regex::new(pattern).map_err(|e| {
1008 tera::Error::msg(format!("reReplaceAll: invalid regex '{}': {}", pattern, e))
1009 })?;
1010 Ok(Value::String(
1011 re.replace_all(input, replacement).to_string(),
1012 ))
1013 },
1014 );
1015
1016 tera.register_filter(
1020 "reReplaceAll",
1021 |value: &Value, args: &HashMap<String, Value>| {
1022 let input = tera::try_get_value!("reReplaceAll", "value", String, value);
1023 let pattern = args
1024 .get("pattern")
1025 .and_then(|v| v.as_str())
1026 .ok_or_else(|| {
1027 tera::Error::msg("reReplaceAll filter requires `pattern` argument")
1028 })?;
1029 let replacement = args
1030 .get("replacement")
1031 .and_then(|v| v.as_str())
1032 .ok_or_else(|| {
1033 tera::Error::msg("reReplaceAll filter requires `replacement` argument")
1034 })?;
1035 let re = Regex::new(pattern).map_err(|e| {
1036 tera::Error::msg(format!("reReplaceAll: invalid regex '{}': {}", pattern, e))
1037 })?;
1038 Ok(Value::String(
1039 re.replace_all(&input, replacement).to_string(),
1040 ))
1041 },
1042 );
1043
1044 tera.register_function(
1047 "englishJoin",
1048 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1049 let items = args
1050 .get("items")
1051 .and_then(|v| v.as_array())
1052 .ok_or_else(|| tera::Error::msg("englishJoin requires `items` argument"))?;
1053 let oxford = args.get("oxford").and_then(|v| v.as_bool()).unwrap_or(true);
1054 let strs: Vec<String> = items
1055 .iter()
1056 .map(|v| v.as_str().unwrap_or("").to_string())
1057 .filter(|s| !s.trim().is_empty())
1058 .collect();
1059 let result = match strs.len() {
1060 0 => String::new(),
1061 1 => strs[0].clone(),
1062 2 => format!("{} and {}", strs[0], strs[1]),
1063 _ => {
1064 let Some((last, rest)) = strs.split_last() else {
1067 return Ok(Value::String(String::new()));
1068 };
1069 if oxford {
1070 format!("{}, and {}", rest.join(", "), last)
1071 } else {
1072 format!("{} and {}", rest.join(", "), last)
1073 }
1074 }
1075 };
1076 Ok(Value::String(result))
1077 },
1078 );
1079
1080 tera.register_filter(
1082 "englishJoin",
1083 |value: &Value, args: &HashMap<String, Value>| {
1084 let items = value
1085 .as_array()
1086 .ok_or_else(|| tera::Error::msg("englishJoin filter expects an array"))?;
1087 let oxford = args.get("oxford").and_then(|v| v.as_bool()).unwrap_or(true);
1088 let strs: Vec<String> = items
1089 .iter()
1090 .map(|v| v.as_str().unwrap_or("").to_string())
1091 .filter(|s| !s.trim().is_empty())
1092 .collect();
1093 let result = match strs.len() {
1094 0 => String::new(),
1095 1 => strs[0].clone(),
1096 2 => format!("{} and {}", strs[0], strs[1]),
1097 _ => {
1098 let Some((last, rest)) = strs.split_last() else {
1101 return Ok(Value::String(String::new()));
1102 };
1103 if oxford {
1104 format!("{}, and {}", rest.join(", "), last)
1105 } else {
1106 format!("{} and {}", rest.join(", "), last)
1107 }
1108 }
1109 };
1110 Ok(Value::String(result))
1111 },
1112 );
1113
1114 tera.register_filter("filter", |value: &Value, args: &HashMap<String, Value>| {
1116 let pattern = args
1117 .get("regexp")
1118 .and_then(|v| v.as_str())
1119 .ok_or_else(|| tera::Error::msg("filter requires `regexp` argument"))?;
1120 let re = regex::Regex::new(pattern)
1121 .map_err(|e| tera::Error::msg(format!("invalid regex '{}': {}", pattern, e)))?;
1122 let input = value.as_str().unwrap_or("");
1123 let result: Vec<&str> = input.lines().filter(|line| re.is_match(line)).collect();
1124 Ok(Value::String(result.join("\n")))
1125 });
1126
1127 tera.register_filter(
1129 "reverseFilter",
1130 |value: &Value, args: &HashMap<String, Value>| {
1131 let pattern = args
1132 .get("regexp")
1133 .and_then(|v| v.as_str())
1134 .ok_or_else(|| tera::Error::msg("reverseFilter requires `regexp` argument"))?;
1135 let re = regex::Regex::new(pattern)
1136 .map_err(|e| tera::Error::msg(format!("invalid regex '{}': {}", pattern, e)))?;
1137 let input = value.as_str().unwrap_or("");
1138 let result: Vec<&str> = input.lines().filter(|line| !re.is_match(line)).collect();
1139 Ok(Value::String(result.join("\n")))
1140 },
1141 );
1142
1143 tera.register_function(
1149 "filter",
1150 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1151 let items_val = args
1152 .get("items")
1153 .ok_or_else(|| tera::Error::msg("filter requires `items` argument"))?;
1154 let pattern = args
1155 .get("regexp")
1156 .and_then(|v| v.as_str())
1157 .ok_or_else(|| tera::Error::msg("filter requires `regexp` argument"))?;
1158 let re = Regex::new(pattern)
1159 .map_err(|e| tera::Error::msg(format!("filter: invalid regex: {}", e)))?;
1160
1161 if let Some(s) = items_val.as_str() {
1162 let filtered: String = s
1164 .lines()
1165 .filter(|line| re.is_match(line))
1166 .collect::<Vec<_>>()
1167 .join("\n");
1168 Ok(Value::String(filtered))
1169 } else if let Some(arr) = items_val.as_array() {
1170 let filtered: Vec<Value> = arr
1172 .iter()
1173 .filter(|v| v.as_str().is_some_and(|s| re.is_match(s)))
1174 .cloned()
1175 .collect();
1176 Ok(Value::Array(filtered))
1177 } else {
1178 Err(tera::Error::msg(
1179 "filter: `items` must be a string or array",
1180 ))
1181 }
1182 },
1183 );
1184
1185 tera.register_function(
1191 "reverseFilter",
1192 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1193 let items_val = args
1194 .get("items")
1195 .ok_or_else(|| tera::Error::msg("reverseFilter requires `items` argument"))?;
1196 let pattern = args
1197 .get("regexp")
1198 .and_then(|v| v.as_str())
1199 .ok_or_else(|| tera::Error::msg("reverseFilter requires `regexp` argument"))?;
1200 let re = Regex::new(pattern)
1201 .map_err(|e| tera::Error::msg(format!("reverseFilter: invalid regex: {}", e)))?;
1202
1203 if let Some(s) = items_val.as_str() {
1204 let filtered: String = s
1206 .lines()
1207 .filter(|line| !re.is_match(line))
1208 .collect::<Vec<_>>()
1209 .join("\n");
1210 Ok(Value::String(filtered))
1211 } else if let Some(arr) = items_val.as_array() {
1212 let filtered: Vec<Value> = arr
1214 .iter()
1215 .filter(|v| !v.as_str().is_some_and(|s| re.is_match(s)))
1216 .cloned()
1217 .collect();
1218 Ok(Value::Array(filtered))
1219 } else {
1220 Err(tera::Error::msg(
1221 "reverseFilter: `items` must be a string or array",
1222 ))
1223 }
1224 },
1225 );
1226
1227 tera.register_function(
1229 "indexOrDefault",
1230 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1231 let map = args
1232 .get("map")
1233 .and_then(|v| v.as_object())
1234 .ok_or_else(|| tera::Error::msg("indexOrDefault requires `map` argument"))?;
1235 let key = args
1236 .get("key")
1237 .and_then(|v| v.as_str())
1238 .ok_or_else(|| tera::Error::msg("indexOrDefault requires `key` argument"))?;
1239 let default = args
1240 .get("default")
1241 .cloned()
1242 .unwrap_or(Value::String(String::new()));
1243 Ok(map.get(key).cloned().unwrap_or(default))
1244 },
1245 );
1246
1247 tera.register_function(
1250 "replace",
1251 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1252 let s = args
1253 .get("s")
1254 .and_then(|v| v.as_str())
1255 .ok_or_else(|| tera::Error::msg("replace requires `s` argument"))?;
1256 let old = args
1257 .get("old")
1258 .and_then(|v| v.as_str())
1259 .ok_or_else(|| tera::Error::msg("replace requires `old` argument"))?;
1260 let new = args
1261 .get("new")
1262 .and_then(|v| v.as_str())
1263 .ok_or_else(|| tera::Error::msg("replace requires `new` argument"))?;
1264 Ok(Value::String(s.replace(old, new)))
1265 },
1266 );
1267 tera.register_filter("replace", |value: &Value, args: &HashMap<String, Value>| {
1271 let s = tera::try_get_value!("replace", "value", String, value);
1272 let from = args
1273 .get("from")
1274 .and_then(|v| v.as_str())
1275 .ok_or_else(|| tera::Error::msg("replace filter requires `from` argument"))?;
1276 let to = args
1277 .get("to")
1278 .and_then(|v| v.as_str())
1279 .ok_or_else(|| tera::Error::msg("replace filter requires `to` argument"))?;
1280 Ok(Value::String(s.replace(from, to)))
1281 });
1282
1283 tera.register_function(
1286 "split",
1287 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1288 let s = args
1289 .get("s")
1290 .and_then(|v| v.as_str())
1291 .ok_or_else(|| tera::Error::msg("split requires `s` argument"))?;
1292 let sep = args
1293 .get("sep")
1294 .and_then(|v| v.as_str())
1295 .ok_or_else(|| tera::Error::msg("split requires `sep` argument"))?;
1296 let parts: Vec<Value> = s.split(sep).map(|p| Value::String(p.to_string())).collect();
1297 Ok(Value::Array(parts))
1298 },
1299 );
1300 tera.register_filter("split", |value: &Value, args: &HashMap<String, Value>| {
1302 let s = tera::try_get_value!("split", "value", String, value);
1303 let sep = args
1304 .get("sep")
1305 .and_then(|v| v.as_str())
1306 .ok_or_else(|| tera::Error::msg("split filter requires `sep` argument"))?;
1307 let parts: Vec<Value> = s.split(sep).map(|p| Value::String(p.to_string())).collect();
1308 Ok(Value::Array(parts))
1309 });
1310
1311 tera.register_filter(
1313 "contains",
1314 |value: &Value, args: &HashMap<String, Value>| {
1315 let s = tera::try_get_value!("contains", "value", String, value);
1316 let substr = args
1317 .get("substr")
1318 .and_then(|v| v.as_str())
1319 .ok_or_else(|| tera::Error::msg("contains filter requires `substr` argument"))?;
1320 Ok(Value::Bool(s.contains(substr)))
1321 },
1322 );
1323
1324 tera.register_function(
1328 "trim",
1329 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1330 let s = args
1331 .get("s")
1332 .and_then(|v| v.as_str())
1333 .ok_or_else(|| tera::Error::msg("trim requires `s` argument"))?;
1334 Ok(Value::String(s.trim().to_string()))
1335 },
1336 );
1337
1338 tera.register_function(
1342 "title",
1343 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1344 let s = args
1345 .get("s")
1346 .and_then(|v| v.as_str())
1347 .ok_or_else(|| tera::Error::msg("title requires `s` argument"))?;
1348 let titled = s
1350 .split_whitespace()
1351 .map(|word| {
1352 let mut chars = word.chars();
1353 match chars.next() {
1354 Some(c) => {
1355 let upper: String = c.to_uppercase().collect();
1356 format!("{}{}", upper, chars.as_str())
1357 }
1358 None => String::new(),
1359 }
1360 })
1361 .collect::<Vec<_>>()
1362 .join(" ");
1363 Ok(Value::String(titled))
1364 },
1365 );
1366
1367 tera.register_function(
1371 "tolower",
1372 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1373 let s = args
1374 .get("s")
1375 .and_then(|v| v.as_str())
1376 .ok_or_else(|| tera::Error::msg("tolower requires `s` argument"))?;
1377 Ok(Value::String(s.to_lowercase()))
1378 },
1379 );
1380
1381 tera.register_function(
1383 "toupper",
1384 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1385 let s = args
1386 .get("s")
1387 .and_then(|v| v.as_str())
1388 .ok_or_else(|| tera::Error::msg("toupper requires `s` argument"))?;
1389 Ok(Value::String(s.to_uppercase()))
1390 },
1391 );
1392
1393 tera.register_function(
1395 "trimprefix",
1396 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1397 let s = args
1398 .get("s")
1399 .and_then(|v| v.as_str())
1400 .ok_or_else(|| tera::Error::msg("trimprefix requires `s` argument"))?;
1401 let prefix = args
1402 .get("prefix")
1403 .and_then(|v| v.as_str())
1404 .ok_or_else(|| tera::Error::msg("trimprefix requires `prefix` argument"))?;
1405 let result = s.strip_prefix(prefix).unwrap_or(s);
1406 Ok(Value::String(result.to_string()))
1407 },
1408 );
1409
1410 tera.register_function(
1412 "trimsuffix",
1413 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1414 let s = args
1415 .get("s")
1416 .and_then(|v| v.as_str())
1417 .ok_or_else(|| tera::Error::msg("trimsuffix requires `s` argument"))?;
1418 let suffix = args
1419 .get("suffix")
1420 .and_then(|v| v.as_str())
1421 .ok_or_else(|| tera::Error::msg("trimsuffix requires `suffix` argument"))?;
1422 let result = s.strip_suffix(suffix).unwrap_or(s);
1423 Ok(Value::String(result.to_string()))
1424 },
1425 );
1426
1427 tera.register_function(
1429 "dir",
1430 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1431 let s = args
1432 .get("s")
1433 .and_then(|v| v.as_str())
1434 .ok_or_else(|| tera::Error::msg("dir requires `s` argument"))?;
1435 let p = std::path::Path::new(s);
1436 Ok(Value::String(
1437 p.parent()
1438 .map(|p| p.to_string_lossy().to_string())
1439 .unwrap_or_default(),
1440 ))
1441 },
1442 );
1443
1444 tera.register_function(
1446 "base",
1447 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1448 let s = args
1449 .get("s")
1450 .and_then(|v| v.as_str())
1451 .ok_or_else(|| tera::Error::msg("base requires `s` argument"))?;
1452 let p = std::path::Path::new(s);
1453 Ok(Value::String(
1454 p.file_name()
1455 .map(|n| n.to_string_lossy().to_string())
1456 .unwrap_or_default(),
1457 ))
1458 },
1459 );
1460
1461 tera.register_function(
1463 "abs",
1464 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1465 let s = args
1466 .get("s")
1467 .and_then(|v| v.as_str())
1468 .ok_or_else(|| tera::Error::msg("abs requires `s` argument"))?;
1469 let p = std::path::Path::new(s);
1470 if p.is_absolute() {
1471 Ok(Value::String(s.to_string()))
1472 } else {
1473 let abs = std::env::current_dir()
1474 .map(|cwd| cwd.join(p).to_string_lossy().to_string())
1475 .unwrap_or_else(|_| s.to_string());
1476 Ok(Value::String(abs))
1477 }
1478 },
1479 );
1480
1481 tera.register_function(
1483 "urlPathEscape",
1484 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1485 let s = args
1486 .get("s")
1487 .and_then(|v| v.as_str())
1488 .ok_or_else(|| tera::Error::msg("urlPathEscape requires `s` argument"))?;
1489 let encoded: String = s
1490 .bytes()
1491 .map(|b| {
1492 if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.' || b == b'~'
1493 {
1494 (b as char).to_string()
1495 } else {
1496 format!("%{:02X}", b)
1497 }
1498 })
1499 .collect();
1500 Ok(Value::String(encoded))
1501 },
1502 );
1503
1504 tera.register_function(
1506 "mdv2escape",
1507 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1508 let s = args
1509 .get("s")
1510 .and_then(|v| v.as_str())
1511 .ok_or_else(|| tera::Error::msg("mdv2escape requires `s` argument"))?;
1512 let escaped = s
1513 .chars()
1514 .map(|c| {
1515 if "_*[]()~`>#+-=|{}.!".contains(c) {
1516 format!("\\{}", c)
1517 } else {
1518 c.to_string()
1519 }
1520 })
1521 .collect::<String>();
1522 Ok(Value::String(escaped))
1523 },
1524 );
1525
1526 tera.register_filter("incpatch", |value: &Value, _: &HashMap<String, Value>| {
1530 let v = tera::try_get_value!("incpatch", "value", String, value);
1531 Ok(Value::String(increment_version(&v, VersionPart::Patch)?))
1532 });
1533
1534 tera.register_filter("incminor", |value: &Value, _: &HashMap<String, Value>| {
1536 let v = tera::try_get_value!("incminor", "value", String, value);
1537 Ok(Value::String(increment_version(&v, VersionPart::Minor)?))
1538 });
1539
1540 tera.register_filter("incmajor", |value: &Value, _: &HashMap<String, Value>| {
1542 let v = tera::try_get_value!("incmajor", "value", String, value);
1543 Ok(Value::String(increment_version(&v, VersionPart::Major)?))
1544 });
1545
1546 tera.register_filter(
1556 "now_format",
1557 |_value: &Value, args: &HashMap<String, Value>| {
1558 let fmt = args
1559 .get("format")
1560 .and_then(|v| v.as_str())
1561 .ok_or_else(|| tera::Error::msg("now_format requires a `format` argument"))?;
1562 let chrono_fmt = translate_go_time_format(fmt);
1563 let now = crate::sde::resolve_now();
1564 Ok(Value::String(now.format(&chrono_fmt).to_string()))
1565 },
1566 );
1567
1568 tera.register_function(
1573 "index",
1574 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1575 let collection = args
1576 .get("collection")
1577 .ok_or_else(|| tera::Error::msg("index requires `collection` argument"))?;
1578 let key = args
1579 .get("key")
1580 .ok_or_else(|| tera::Error::msg("index requires `key` argument"))?;
1581
1582 match collection {
1583 Value::Object(map) => {
1584 let key_str = value_to_string(key);
1585 Ok(map
1586 .get(key_str.as_ref())
1587 .cloned()
1588 .unwrap_or(Value::String(String::new())))
1589 }
1590 Value::Array(arr) => {
1591 if let Some(idx) = key.as_u64() {
1592 Ok(arr
1593 .get(idx as usize)
1594 .cloned()
1595 .unwrap_or(Value::String(String::new())))
1596 } else {
1597 Err(tera::Error::msg("index: array index must be a number"))
1598 }
1599 }
1600 _ => {
1601 Ok(Value::String(String::new()))
1603 }
1604 }
1605 },
1606 );
1607
1608 let in_filter = |value: &Value, args: &HashMap<String, Value>| {
1611 let items = value
1612 .as_array()
1613 .ok_or_else(|| tera::Error::msg("in filter requires an array as input"))?;
1614 let needle = args
1615 .get("value")
1616 .ok_or_else(|| tera::Error::msg("in filter requires `value` argument"))?;
1617 let needle_str = value_to_string(needle);
1618 let found = items.iter().any(|item| value_to_string(item) == needle_str);
1619 Ok(Value::Bool(found))
1620 };
1621 tera.register_filter("in", in_filter);
1622 tera.register_filter("contains_any", in_filter);
1623
1624 tera.register_filter("slice", |value: &Value, args: &HashMap<String, Value>| {
1632 let start = args.get("start").and_then(|v| v.as_i64()).unwrap_or(0);
1633 let end = args.get("end").and_then(|v| v.as_i64());
1634
1635 let resolve = |idx: i64, len: i64| -> i64 {
1637 let abs = if idx < 0 { len + idx } else { idx };
1638 abs.clamp(0, len)
1639 };
1640
1641 match value {
1642 Value::String(s) => {
1643 let chars: Vec<char> = s.chars().collect();
1644 let len = chars.len() as i64;
1645 let lo = resolve(start, len);
1646 let hi = resolve(end.unwrap_or(len), len).max(lo) as usize;
1647 Ok(Value::String(chars[lo as usize..hi].iter().collect()))
1648 }
1649 Value::Array(arr) => {
1650 let len = arr.len() as i64;
1651 let lo = resolve(start, len);
1652 let hi = resolve(end.unwrap_or(len), len).max(lo) as usize;
1653 Ok(Value::Array(arr[lo as usize..hi].to_vec()))
1654 }
1655 other => Err(tera::Error::msg(format!(
1656 "slice: expected a string or array, got {}",
1657 other
1658 ))),
1659 }
1660 });
1661
1662 tera.register_function(
1666 "printf",
1667 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1668 let format = args
1669 .get("format")
1670 .and_then(|v| v.as_str())
1671 .ok_or_else(|| tera::Error::msg("printf requires a `format` argument"))?;
1672 let fmt_args: Vec<Value> = args
1673 .get("args")
1674 .and_then(|v| v.as_array())
1675 .cloned()
1676 .unwrap_or_default();
1677 Ok(Value::String(sprintf(format, &fmt_args)?))
1678 },
1679 );
1680
1681 tera.register_function(
1687 "print",
1688 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1689 let fmt_args: Vec<Value> = args
1690 .get("args")
1691 .and_then(|v| v.as_array())
1692 .cloned()
1693 .unwrap_or_default();
1694 let mut out = String::new();
1695 for (i, v) in fmt_args.iter().enumerate() {
1696 if i > 0 {
1697 let prev_str = fmt_args[i - 1].is_string();
1698 let cur_str = v.is_string();
1699 if !prev_str && !cur_str {
1700 out.push(' ');
1701 }
1702 }
1703 out.push_str(&printf_default(v));
1704 }
1705 Ok(Value::String(out))
1706 },
1707 );
1708 tera.register_function(
1709 "println",
1710 |args: &HashMap<String, Value>| -> tera::Result<Value> {
1711 let fmt_args: Vec<Value> = args
1712 .get("args")
1713 .and_then(|v| v.as_array())
1714 .cloned()
1715 .unwrap_or_default();
1716 let mut joined = fmt_args
1717 .iter()
1718 .map(printf_default)
1719 .collect::<Vec<_>>()
1720 .join(" ");
1721 joined.push('\n');
1722 Ok(Value::String(joined))
1723 },
1724 );
1725
1726 tera
1727});