1use regex::Regex;
2use serde_json::Value;
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::sync::LazyLock;
6use tera::TeraResult;
7
8use sha1::Digest as Sha1Digest;
9use sha2::Digest as Sha2Digest;
10use sha3::Digest as Sha3Digest;
11
12use super::engine_adapter::{JsonRegisterExt, try_get_value};
15use crate::path_util::expand_tilde;
16
17fn value_to_string(v: &Value) -> Cow<'_, str> {
21 match v {
22 Value::String(s) => Cow::Borrowed(s.as_str()),
23 Value::Number(n) => Cow::Owned(n.to_string()),
24 Value::Bool(b) => Cow::Owned(b.to_string()),
25 Value::Null => Cow::Borrowed(""),
26 other => Cow::Owned(other.to_string()),
28 }
29}
30
31fn printf_default(v: &Value) -> String {
36 value_to_string(v).into_owned()
37}
38
39#[derive(Clone, Copy)]
41struct PrintfSpec {
42 minus: bool,
43 plus: bool,
44 space: bool,
45 zero: bool,
46 hash: bool,
47 width: Option<usize>,
48 precision: Option<usize>,
49 verb: char,
50}
51
52fn pad(spec: &PrintfSpec, body: String, numeric_sign_prefix: Option<(&str, &str)>) -> String {
56 let (sign, prefix, core) = match numeric_sign_prefix {
57 Some((sign, prefix)) => (sign, prefix, body.as_str()),
58 None => ("", "", body.as_str()),
59 };
60 let assembled = format!("{}{}{}", sign, prefix, core);
61 let Some(width) = spec.width else {
62 return assembled;
63 };
64 let len = assembled.chars().count();
65 if len >= width {
66 return assembled;
67 }
68 let padding = width - len;
69 if spec.minus {
70 format!("{}{}", assembled, " ".repeat(padding))
71 } else if spec.zero && numeric_sign_prefix.is_some() {
72 format!("{}{}{}{}", sign, prefix, "0".repeat(padding), core)
74 } else if spec.zero {
75 format!("{}{}", "0".repeat(padding), assembled)
76 } else {
77 format!("{}{}", " ".repeat(padding), assembled)
78 }
79}
80
81fn pad_int(spec: &PrintfSpec, body: String, sign: &str, prefix: &str) -> String {
88 if spec.precision.is_some() && spec.zero {
89 let no_zero = PrintfSpec {
90 zero: false,
91 ..*spec
92 };
93 pad(&no_zero, body, Some((sign, prefix)))
94 } else {
95 pad(spec, body, Some((sign, prefix)))
96 }
97}
98
99fn numeric_sign(negative: bool, plus: bool, space: bool) -> &'static str {
102 if negative {
103 "-"
104 } else if plus {
105 "+"
106 } else if space {
107 " "
108 } else {
109 ""
110 }
111}
112
113fn int_precision(digits: &str, precision: Option<usize>) -> String {
121 match precision {
122 Some(0) if digits == "0" => String::new(),
123 Some(p) if digits.len() < p => format!("{}{}", "0".repeat(p - digits.len()), digits),
124 _ => digits.to_string(),
125 }
126}
127
128fn go_exponent(s: &str, uppercase: bool) -> String {
136 let letter = if uppercase { 'E' } else { 'e' };
137 let Some(pos) = s.find(['e', 'E']) else {
138 return s.to_string();
139 };
140 let (mantissa, exp_part) = s.split_at(pos);
141 let exp_str = &exp_part[1..];
143 let (sign, digits) = match exp_str.strip_prefix('-') {
144 Some(rest) => ('-', rest),
145 None => ('+', exp_str.strip_prefix('+').unwrap_or(exp_str)),
146 };
147 let padded = if digits.len() < 2 {
149 format!("{:0>2}", digits)
150 } else {
151 digits.to_string()
152 };
153 format!("{}{}{}{}", mantissa, letter, sign, padded)
154}
155
156fn trim_fraction_zeros(s: &str) -> &str {
159 if !s.contains('.') {
160 return s;
161 }
162 let trimmed = s.trim_end_matches('0');
163 trimmed.strip_suffix('.').unwrap_or(trimmed)
164}
165
166fn format_g(mag: f64, precision: Option<usize>, uppercase: bool) -> String {
174 let sci = format!("{:e}", mag);
177 let exp: i32 = sci
178 .split(['e', 'E'])
179 .nth(1)
180 .and_then(|e| e.parse().ok())
181 .unwrap_or(0);
182 let eprec = precision.map(|p| p as i32).unwrap_or(6).max(1);
183
184 if exp < -4 || exp >= eprec {
185 let body = match precision {
188 Some(p) => format!("{:.*e}", p.saturating_sub(1), mag),
189 None => sci.clone(),
190 };
191 let normalized = go_exponent(&body, uppercase);
192 if precision.is_some()
194 && let Some(epos) = normalized.find(['e', 'E'])
195 {
196 let (mantissa, exp_part) = normalized.split_at(epos);
197 return format!("{}{}", trim_fraction_zeros(mantissa), exp_part);
198 }
199 normalized
200 } else {
201 let body = match precision {
206 Some(p) => {
208 let frac = (p as i32 - (exp + 1)).max(0) as usize;
209 format!("{:.*}", frac, mag)
210 }
211 None => format!("{}", mag),
212 };
213 trim_fraction_zeros(&body).to_string()
214 }
215}
216
217const PRINTF_FIELD_MAX: usize = 100_000;
220
221fn format_verb(spec: &PrintfSpec, value: Option<&Value>) -> Result<String, tera::Error> {
227 let val = || -> Result<&Value, tera::Error> {
228 value.ok_or_else(|| {
229 tera::Error::message(format!("printf: missing argument for %{}", spec.verb))
230 })
231 };
232 match spec.verb {
233 's' => {
234 let mut s = printf_default(val()?);
235 if let Some(prec) = spec.precision {
236 s = s.chars().take(prec).collect();
237 }
238 Ok(pad(spec, s, None))
239 }
240 'v' => Ok(pad(spec, printf_default(val()?), None)),
241 't' => {
242 let b = val()?
243 .as_bool()
244 .ok_or_else(|| tera::Error::message("printf: %t expects a boolean argument"))?;
245 Ok(pad(spec, b.to_string(), None))
246 }
247 'q' => {
248 let s = printf_default(val()?);
249 Ok(pad(spec, format!("{:?}", s), None))
250 }
251 'c' => {
252 let v = val()?;
253 let code = v
254 .as_u64()
255 .ok_or_else(|| tera::Error::message("printf: %c expects a non-negative integer"))?;
256 let ch = u32::try_from(code)
257 .ok()
258 .and_then(char::from_u32)
259 .ok_or_else(|| {
260 tera::Error::message(format!("printf: %c: {} is not a valid code point", code))
261 })?;
262 Ok(pad(spec, ch.to_string(), None))
263 }
264 'd' => {
265 let n = val()?
266 .as_i64()
267 .ok_or_else(|| tera::Error::message("printf: %d expects an integer argument"))?;
268 let sign = numeric_sign(n < 0, spec.plus, spec.space);
269 Ok(pad_int(
270 spec,
271 int_precision(&n.unsigned_abs().to_string(), spec.precision),
272 sign,
273 "",
274 ))
275 }
276 'b' | 'o' | 'x' | 'X' => {
277 let n = val()?.as_i64().ok_or_else(|| {
278 tera::Error::message(format!(
279 "printf: %{} expects an integer argument",
280 spec.verb
281 ))
282 })?;
283 let mag = n.unsigned_abs();
284 let digits = match spec.verb {
285 'b' => format!("{:b}", mag),
286 'o' => format!("{:o}", mag),
287 'x' => format!("{:x}", mag),
288 'X' => format!("{:X}", mag),
289 _ => unreachable!(),
290 };
291 let body = int_precision(&digits, spec.precision);
292 let sign = numeric_sign(n < 0, spec.plus, spec.space);
293 let prefix = if spec.hash {
294 match spec.verb {
295 'b' => "0b",
296 'o' => "0",
297 'x' => "0x",
298 'X' => "0X",
299 _ => "",
300 }
301 } else {
302 ""
303 };
304 Ok(pad_int(spec, body, sign, prefix))
305 }
306 'f' | 'e' | 'E' | 'g' | 'G' => {
307 let f = val()?.as_f64().ok_or_else(|| {
308 tera::Error::message(format!("printf: %{} expects a numeric argument", spec.verb))
309 })?;
310 let prec = spec.precision.unwrap_or(6);
311 let mag = f.abs();
312 let body = match spec.verb {
313 'f' => format!("{:.*}", prec, mag),
314 'e' | 'E' => go_exponent(&format!("{:.*e}", prec, mag), spec.verb == 'E'),
318 'g' | 'G' => format_g(mag, spec.precision, spec.verb == 'G'),
321 _ => unreachable!(),
322 };
323 let sign = numeric_sign(f.is_sign_negative() && f != 0.0, spec.plus, spec.space);
324 Ok(pad(spec, body, Some((sign, ""))))
325 }
326 other => Err(tera::Error::message(format!(
327 "printf: unsupported verb %{} (supported: s d v x X o b c q f e E g G t %%)",
328 other
329 ))),
330 }
331}
332
333fn sprintf(format: &str, args: &[Value]) -> Result<String, tera::Error> {
340 let mut out = String::new();
341 let mut arg_idx = 0usize;
342 let mut chars = format.chars().peekable();
343
344 while let Some(c) = chars.next() {
345 if c != '%' {
346 out.push(c);
347 continue;
348 }
349 if chars.peek() == Some(&'%') {
351 chars.next();
352 out.push('%');
353 continue;
354 }
355
356 let mut spec = PrintfSpec {
357 minus: false,
358 plus: false,
359 space: false,
360 zero: false,
361 hash: false,
362 width: None,
363 precision: None,
364 verb: ' ',
365 };
366
367 while let Some(&f) = chars.peek() {
369 match f {
370 '-' => spec.minus = true,
371 '+' => spec.plus = true,
372 ' ' => spec.space = true,
373 '0' => spec.zero = true,
374 '#' => spec.hash = true,
375 _ => break,
376 }
377 chars.next();
378 }
379
380 let mut width_digits = String::new();
382 while let Some(&d) = chars.peek() {
383 if d.is_ascii_digit() {
384 width_digits.push(d);
385 chars.next();
386 } else {
387 break;
388 }
389 }
390 if !width_digits.is_empty() {
391 let w = width_digits.parse::<usize>().unwrap_or(usize::MAX);
393 if w > PRINTF_FIELD_MAX {
394 return Err(tera::Error::message(format!(
395 "printf width {} exceeds maximum {}",
396 width_digits, PRINTF_FIELD_MAX
397 )));
398 }
399 spec.width = Some(w);
400 }
401
402 if chars.peek() == Some(&'.') {
404 chars.next();
405 let mut prec_digits = String::new();
406 while let Some(&d) = chars.peek() {
407 if d.is_ascii_digit() {
408 prec_digits.push(d);
409 chars.next();
410 } else {
411 break;
412 }
413 }
414 let p = if prec_digits.is_empty() {
416 0
417 } else {
418 prec_digits.parse::<usize>().unwrap_or(usize::MAX)
419 };
420 if p > PRINTF_FIELD_MAX {
421 return Err(tera::Error::message(format!(
422 "printf precision {} exceeds maximum {}",
423 prec_digits, PRINTF_FIELD_MAX
424 )));
425 }
426 spec.precision = Some(p);
427 }
428
429 let verb = chars.next().ok_or_else(|| {
430 tera::Error::message("printf: format string ends with a dangling '%'")
431 })?;
432 spec.verb = verb;
433
434 let value = args.get(arg_idx);
435 out.push_str(&format_verb(&spec, value)?);
436 arg_idx += 1;
438 }
439
440 Ok(out)
441}
442
443pub(super) fn translate_go_time_format(fmt: &str) -> Cow<'_, str> {
450 if fmt.contains('%') {
452 return Cow::Borrowed(fmt);
453 }
454
455 const GO_MARKERS: &[&str] = &[
458 "2006", "06", "January", "Jan", "01", "Monday", "Mon", "02", "15", "03", "04", "05", "PM",
459 "pm", "-0700", "Z0700", "MST",
460 ];
461 let has_go_patterns = GO_MARKERS.iter().any(|p| fmt.contains(p));
462 if !has_go_patterns {
463 return Cow::Borrowed(fmt);
464 }
465
466 let mut result = fmt.to_string();
470
471 let replacements: &[(&str, &str)] = &[
472 ("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"), ];
491
492 for (go_pat, chrono_pat) in replacements {
493 result = result.replace(go_pat, chrono_pat);
494 }
495
496 Cow::Owned(result)
497}
498
499enum VersionPart {
500 Major,
501 Minor,
502 Patch,
503}
504
505fn increment_version(v: &str, part: VersionPart) -> Result<String, tera::Error> {
513 let stripped = v.strip_prefix('v').unwrap_or(v);
514 let parts: Vec<&str> = stripped.splitn(3, '.').collect();
515 let invalid = || {
516 tera::Error::message(format!(
517 "incpatch/incminor/incmajor: '{}' is not a valid semver version (expected MAJOR.MINOR.PATCH)",
518 v
519 ))
520 };
521 if parts.len() < 3 {
522 return Err(invalid());
523 }
524 let major: u64 = parts
525 .first()
526 .and_then(|s| s.parse().ok())
527 .ok_or_else(invalid)?;
528 let minor: u64 = parts
529 .get(1)
530 .and_then(|s| s.parse().ok())
531 .ok_or_else(invalid)?;
532 let patch: u64 = parts
533 .get(2)
534 .and_then(|s| {
535 s.split('-').next().and_then(|n| n.parse().ok())
537 })
538 .ok_or_else(invalid)?;
539 let prefix = if v.starts_with('v') { "v" } else { "" };
540 Ok(match part {
541 VersionPart::Major => format!("{}{}.0.0", prefix, major + 1),
542 VersionPart::Minor => format!("{}{}.{}.0", prefix, major, minor + 1),
543 VersionPart::Patch => format!("{}{}.{}.{}", prefix, major, minor, patch + 1),
544 })
545}
546
547pub fn ruby_escape_str(s: &str) -> String {
557 s.replace('\\', "\\\\").replace('"', "\\\"")
558}
559
560pub(super) fn register_ruby_escape(tera: &mut tera::Tera) {
571 tera.register_json_filter(
572 "ruby_escape",
573 |value: &Value, _: &HashMap<String, Value>| {
574 let s = try_get_value!("ruby_escape", "value", String, value);
575 Ok(Value::String(ruby_escape_str(&s)))
576 },
577 );
578}
579
580pub(super) static BASE_TERA: LazyLock<tera::Tera> = LazyLock::new(|| {
583 let mut tera = tera::Tera::default();
584 register_ruby_escape(&mut tera);
585
586 tera.register_json_filter("tolower", |value: &Value, _: &HashMap<String, Value>| {
588 let s = try_get_value!("tolower", "value", String, value);
589 Ok(Value::String(s.to_lowercase()))
590 });
591 tera.register_json_filter("toupper", |value: &Value, _: &HashMap<String, Value>| {
592 let s = try_get_value!("toupper", "value", String, value);
593 Ok(Value::String(s.to_uppercase()))
594 });
595
596 tera.register_json_filter(
598 "trimprefix",
599 |value: &Value, args: &HashMap<String, Value>| {
600 let s = try_get_value!("trimprefix", "value", String, value);
601 let prefix = args
602 .get("prefix")
603 .and_then(|v| v.as_str())
604 .ok_or_else(|| tera::Error::message("trimprefix requires a `prefix` argument"))?;
605 let result = s.strip_prefix(prefix).unwrap_or(&s);
606 Ok(Value::String(result.to_string()))
607 },
608 );
609
610 tera.register_json_filter(
612 "trimsuffix",
613 |value: &Value, args: &HashMap<String, Value>| {
614 let s = try_get_value!("trimsuffix", "value", String, value);
615 let suffix = args
616 .get("suffix")
617 .and_then(|v| v.as_str())
618 .ok_or_else(|| tera::Error::message("trimsuffix requires a `suffix` argument"))?;
619 let result = s.strip_suffix(suffix).unwrap_or(&s);
620 Ok(Value::String(result.to_string()))
621 },
622 );
623
624 tera.register_json_function(
630 "envOrDefault",
631 |args: &HashMap<String, Value>| -> TeraResult<Value> {
632 let name = args
633 .get("name")
634 .and_then(|v| v.as_str())
635 .ok_or_else(|| tera::Error::message("envOrDefault requires `name` argument"))?;
636 let default = args.get("default").and_then(|v| v.as_str()).unwrap_or("");
637 let value = std::env::var(name).unwrap_or_else(|_| default.to_string());
638 Ok(Value::String(value))
639 },
640 );
641 tera.register_json_function(
642 "isEnvSet",
643 |args: &HashMap<String, Value>| -> TeraResult<Value> {
644 let name = args
645 .get("name")
646 .and_then(|v| v.as_str())
647 .ok_or_else(|| tera::Error::message("isEnvSet requires `name` argument"))?;
648 let is_set = std::env::var(name).map(|v| !v.is_empty()).unwrap_or(false);
649 Ok(Value::Bool(is_set))
650 },
651 );
652
653 tera.register_json_function(
657 "incpatch",
658 |args: &HashMap<String, Value>| -> TeraResult<Value> {
659 let v = args
660 .get("v")
661 .and_then(|v| v.as_str())
662 .ok_or_else(|| tera::Error::message("incpatch requires `v` argument"))?;
663 Ok(Value::String(increment_version(v, VersionPart::Patch)?))
664 },
665 );
666
667 tera.register_json_function(
669 "incminor",
670 |args: &HashMap<String, Value>| -> TeraResult<Value> {
671 let v = args
672 .get("v")
673 .and_then(|v| v.as_str())
674 .ok_or_else(|| tera::Error::message("incminor requires `v` argument"))?;
675 Ok(Value::String(increment_version(v, VersionPart::Minor)?))
676 },
677 );
678
679 tera.register_json_function(
681 "incmajor",
682 |args: &HashMap<String, Value>| -> TeraResult<Value> {
683 let v = args
684 .get("v")
685 .and_then(|v| v.as_str())
686 .ok_or_else(|| tera::Error::message("incmajor requires `v` argument"))?;
687 Ok(Value::String(increment_version(v, VersionPart::Major)?))
688 },
689 );
690
691 macro_rules! register_hash_fn {
694 ($tera:expr, $name:expr, $hash_fn:expr) => {
695 $tera.register_json_function(
696 $name,
697 |args: &HashMap<String, Value>| -> TeraResult<Value> {
698 let s = args.get("s").and_then(|v| v.as_str()).ok_or_else(|| {
699 tera::Error::message(format!("{} requires `s` argument", $name))
700 })?;
701 let bytes = std::fs::read(s).map_err(|e| {
703 tera::Error::message(format!(
704 "{}: failed to read file '{}': {}",
705 $name, s, e
706 ))
707 })?;
708 Ok(Value::String($hash_fn(&bytes)))
709 },
710 );
711 };
712 }
713
714 register_hash_fn!(tera, "sha1", |b: &[u8]| {
715 let mut h = sha1::Sha1::new();
716 Sha1Digest::update(&mut h, b);
717 crate::hashing::hex_lower(&Sha1Digest::finalize(h))
718 });
719 register_hash_fn!(tera, "sha224", |b: &[u8]| {
720 let mut h = sha2::Sha224::new();
721 Sha2Digest::update(&mut h, b);
722 crate::hashing::hex_lower(&Sha2Digest::finalize(h))
723 });
724 register_hash_fn!(tera, "sha256", |b: &[u8]| {
725 let mut h = sha2::Sha256::new();
726 Sha2Digest::update(&mut h, b);
727 crate::hashing::hex_lower(&Sha2Digest::finalize(h))
728 });
729 register_hash_fn!(tera, "sha384", |b: &[u8]| {
730 let mut h = sha2::Sha384::new();
731 Sha2Digest::update(&mut h, b);
732 crate::hashing::hex_lower(&Sha2Digest::finalize(h))
733 });
734 register_hash_fn!(tera, "sha512", |b: &[u8]| {
735 let mut h = sha2::Sha512::new();
736 Sha2Digest::update(&mut h, b);
737 crate::hashing::hex_lower(&Sha2Digest::finalize(h))
738 });
739 register_hash_fn!(tera, "sha3_224", |b: &[u8]| {
740 let mut h = sha3::Sha3_224::new();
741 Sha3Digest::update(&mut h, b);
742 crate::hashing::hex_lower(&Sha3Digest::finalize(h))
743 });
744 register_hash_fn!(tera, "sha3_256", |b: &[u8]| {
745 let mut h = sha3::Sha3_256::new();
746 Sha3Digest::update(&mut h, b);
747 crate::hashing::hex_lower(&Sha3Digest::finalize(h))
748 });
749 register_hash_fn!(tera, "sha3_384", |b: &[u8]| {
750 let mut h = sha3::Sha3_384::new();
751 Sha3Digest::update(&mut h, b);
752 crate::hashing::hex_lower(&Sha3Digest::finalize(h))
753 });
754 register_hash_fn!(tera, "sha3_512", |b: &[u8]| {
755 let mut h = sha3::Sha3_512::new();
756 Sha3Digest::update(&mut h, b);
757 crate::hashing::hex_lower(&Sha3Digest::finalize(h))
758 });
759 register_hash_fn!(tera, "blake2b", |b: &[u8]| {
760 let mut h = blake2::Blake2b512::new();
761 blake2::Digest::update(&mut h, b);
762 crate::hashing::hex_lower(&blake2::Digest::finalize(h))
763 });
764 register_hash_fn!(tera, "blake2s", |b: &[u8]| {
765 let mut h = blake2::Blake2s256::new();
766 blake2::Digest::update(&mut h, b);
767 crate::hashing::hex_lower(&blake2::Digest::finalize(h))
768 });
769 register_hash_fn!(tera, "blake3", |b: &[u8]| {
770 crate::hashing::hex_lower(blake3::hash(b).as_bytes())
771 });
772 register_hash_fn!(tera, "md5", |b: &[u8]| {
773 let mut h = md5::Md5::new();
774 md5::Digest::update(&mut h, b);
775 crate::hashing::hex_lower(&md5::Digest::finalize(h))
776 });
777 register_hash_fn!(tera, "crc32", |b: &[u8]| {
778 format!("{:08x}", crc32fast::hash(b))
779 });
780
781 tera.register_json_function(
787 "readFile",
788 |args: &HashMap<String, Value>| -> TeraResult<Value> {
789 let path = args
790 .get("path")
791 .and_then(|v| v.as_str())
792 .ok_or_else(|| tera::Error::message("readFile requires `path` argument"))?;
793 let resolved = expand_tilde(path);
794 let content = std::fs::read_to_string(resolved.as_ref()).unwrap_or_default();
795 Ok(Value::String(content.trim().to_string()))
796 },
797 );
798
799 tera.register_json_function(
802 "mustReadFile",
803 |args: &HashMap<String, Value>| -> TeraResult<Value> {
804 let path = args
805 .get("path")
806 .and_then(|v| v.as_str())
807 .ok_or_else(|| tera::Error::message("mustReadFile requires `path` argument"))?;
808 let resolved = expand_tilde(path);
809 let content = std::fs::read_to_string(resolved.as_ref())
810 .map_err(|e| tera::Error::message(format!("mustReadFile: {}: {}", resolved, e)))?;
811 Ok(Value::String(content.trim().to_string()))
812 },
813 );
814
815 tera.register_json_function(
824 "time",
825 |args: &HashMap<String, Value>| -> TeraResult<Value> {
826 let fmt = args
827 .get("format")
828 .and_then(|v| v.as_str())
829 .unwrap_or("%Y-%m-%dT%H:%M:%SZ");
830 let chrono_fmt = translate_go_time_format(fmt);
831 let now = crate::sde::resolve_now();
832 Ok(Value::String(now.format(&chrono_fmt).to_string()))
833 },
834 );
835
836 tera.register_json_filter("date", |value: &Value, args: &HashMap<String, Value>| {
857 use chrono::format::{Item, StrftimeItems};
858 use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone, Utc};
859 use chrono_tz::Tz;
860
861 if args.contains_key("locale") {
862 return Err(tera::Error::message(
863 "date: the `locale` argument is not supported; remove it — \
864 output is always formatted in the POSIX locale",
865 ));
866 }
867 let format = args
868 .get("format")
869 .and_then(|v| v.as_str())
870 .unwrap_or("%Y-%m-%d");
871 if StrftimeItems::new(format).any(|item| matches!(item, Item::Error)) {
872 return Err(tera::Error::message(format!(
873 "date: invalid format `{format}` — expected chrono strftime \
874 specifiers (e.g. `%Y-%m-%d`)"
875 )));
876 }
877 let timezone = match args.get("timezone") {
878 Some(val) => {
879 let tz = try_get_value!("date", "timezone", String, val);
880 Some(tz.parse::<Tz>().map_err(|_| {
881 tera::Error::message(format!(
882 "date: error parsing `{tz}` as a timezone \
883 (expected an IANA name, e.g. `America/New_York`)"
884 ))
885 })?)
886 }
887 None => None,
888 };
889
890 let formatted = match value {
891 Value::Number(n) => {
892 let secs = n.as_i64().ok_or_else(|| {
893 tera::Error::message(format!("date: expected an integer timestamp, got {n}"))
894 })?;
895 let dt = DateTime::<Utc>::from_timestamp(secs, 0).ok_or_else(|| {
896 tera::Error::message(format!("date: timestamp {secs} is out of range"))
897 })?;
898 match timezone {
899 Some(tz) => tz
900 .from_utc_datetime(&dt.naive_utc())
901 .format(format)
902 .to_string(),
903 None => dt.format(format).to_string(),
904 }
905 }
906 Value::String(s) if s.contains('T') => match s.parse::<DateTime<FixedOffset>>() {
907 Ok(dt) => match timezone {
908 Some(tz) => dt.with_timezone(&tz).format(format).to_string(),
909 None => dt.format(format).to_string(),
910 },
911 Err(_) => {
912 let dt = s.parse::<NaiveDateTime>().map_err(|_| {
913 tera::Error::message(format!(
914 "date: cannot parse `{s}` as an RFC3339 or naive datetime"
915 ))
916 })?;
917 dt.and_utc().format(format).to_string()
920 }
921 },
922 Value::String(s) => {
923 let d = NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|_| {
924 tera::Error::message(format!("date: cannot parse `{s}` as a YYYY-MM-DD date"))
925 })?;
926 d.and_hms_opt(0, 0, 0)
929 .unwrap_or_default()
930 .and_utc()
931 .format(format)
932 .to_string()
933 }
934 other => {
935 return Err(tera::Error::message(format!(
936 "date: expected a timestamp or datetime string, got {other}"
937 )));
938 }
939 };
940 Ok(Value::String(formatted))
941 });
942
943 tera.register_json_filter("dir", |value: &Value, _: &HashMap<String, Value>| {
947 let s = try_get_value!("dir", "value", String, value);
948 let p = std::path::Path::new(&s);
949 Ok(Value::String(
950 p.parent()
951 .map(|p| p.to_string_lossy().to_string())
952 .unwrap_or_default(),
953 ))
954 });
955
956 tera.register_json_filter("base", |value: &Value, _: &HashMap<String, Value>| {
958 let s = try_get_value!("base", "value", String, value);
959 let p = std::path::Path::new(&s);
960 Ok(Value::String(
961 p.file_name()
962 .map(|n| n.to_string_lossy().to_string())
963 .unwrap_or_default(),
964 ))
965 });
966
967 tera.register_json_filter("abs", |value: &Value, _: &HashMap<String, Value>| {
969 let s = try_get_value!("abs", "value", String, value);
970 let p = std::path::Path::new(&s);
971 if p.is_absolute() {
972 Ok(Value::String(s))
973 } else {
974 let abs = std::env::current_dir()
975 .map(|cwd| cwd.join(p).to_string_lossy().to_string())
976 .unwrap_or(s);
977 Ok(Value::String(abs))
978 }
979 });
980
981 tera.register_json_filter(
983 "urlPathEscape",
984 |value: &Value, _: &HashMap<String, Value>| {
985 let s = try_get_value!("urlPathEscape", "value", String, value);
986 let encoded: String = s
989 .bytes()
990 .map(|b| {
991 if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.' || b == b'~'
992 {
993 (b as char).to_string()
994 } else {
995 format!("%{:02X}", b)
996 }
997 })
998 .collect();
999 Ok(Value::String(encoded))
1000 },
1001 );
1002
1003 tera.register_json_filter("mdv2escape", |value: &Value, _: &HashMap<String, Value>| {
1005 let s = try_get_value!("mdv2escape", "value", String, value);
1006 let escaped = s
1007 .chars()
1008 .map(|c| {
1009 if "_*[]()~`>#+-=|{}.!".contains(c) {
1010 format!("\\{}", c)
1011 } else {
1012 c.to_string()
1013 }
1014 })
1015 .collect::<String>();
1016 Ok(Value::String(escaped))
1017 });
1018
1019 tera.register_json_function(
1023 "contains",
1024 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1025 let s = args
1026 .get("s")
1027 .and_then(|v| v.as_str())
1028 .ok_or_else(|| tera::Error::message("contains requires `s` argument"))?;
1029 let substr = args
1030 .get("substr")
1031 .and_then(|v| v.as_str())
1032 .ok_or_else(|| tera::Error::message("contains requires `substr` argument"))?;
1033 Ok(Value::Bool(s.contains(substr)))
1034 },
1035 );
1036
1037 tera.register_json_function(
1043 "list",
1044 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1045 let items = args
1046 .get("items")
1047 .and_then(|v| v.as_array())
1048 .ok_or_else(|| tera::Error::message("list requires `items` argument"))?;
1049 Ok(Value::Array(items.clone()))
1050 },
1051 );
1052
1053 tera.register_json_function(
1056 "map",
1057 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1058 let pairs = args
1059 .get("pairs")
1060 .and_then(|v| v.as_array())
1061 .ok_or_else(|| tera::Error::message("map requires `pairs` argument"))?;
1062 if pairs.len() % 2 != 0 {
1063 return Err(tera::Error::message(
1064 "map requires an even number of arguments (key-value pairs)",
1065 ));
1066 }
1067 let mut result = serde_json::Map::new();
1068 for chunk in pairs.chunks(2) {
1069 let key = chunk[0].as_str().unwrap_or("").to_string();
1070 result.insert(key, chunk[1].clone());
1071 }
1072 Ok(Value::Object(result))
1073 },
1074 );
1075
1076 let in_fn = |args: &HashMap<String, Value>| -> TeraResult<Value> {
1081 let items = args
1082 .get("items")
1083 .and_then(|v| v.as_array())
1084 .ok_or_else(|| {
1085 tera::Error::message("in requires `items` argument (must be an array)")
1086 })?;
1087 let value = args
1088 .get("value")
1089 .ok_or_else(|| tera::Error::message("in requires `value` argument"))?;
1090 let needle = value_to_string(value);
1092 let found = items.iter().any(|item| value_to_string(item) == needle);
1093 Ok(Value::Bool(found))
1094 };
1095 tera.register_json_function("in", in_fn);
1096 tera.register_json_function("contains_any", in_fn);
1099
1100 tera.register_json_function(
1108 "reReplaceAll",
1109 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1110 let pattern = args
1111 .get("pattern")
1112 .and_then(|v| v.as_str())
1113 .ok_or_else(|| tera::Error::message("reReplaceAll requires `pattern` argument"))?;
1114 let input = args
1115 .get("input")
1116 .and_then(|v| v.as_str())
1117 .ok_or_else(|| tera::Error::message("reReplaceAll requires `input` argument"))?;
1118 let replacement = args
1119 .get("replacement")
1120 .and_then(|v| v.as_str())
1121 .ok_or_else(|| {
1122 tera::Error::message("reReplaceAll requires `replacement` argument")
1123 })?;
1124 let re = Regex::new(pattern).map_err(|e| {
1125 tera::Error::message(format!("reReplaceAll: invalid regex '{}': {}", pattern, e))
1126 })?;
1127 Ok(Value::String(
1128 re.replace_all(input, replacement).to_string(),
1129 ))
1130 },
1131 );
1132
1133 tera.register_json_filter(
1137 "reReplaceAll",
1138 |value: &Value, args: &HashMap<String, Value>| {
1139 let input = try_get_value!("reReplaceAll", "value", String, value);
1140 let pattern = args
1141 .get("pattern")
1142 .and_then(|v| v.as_str())
1143 .ok_or_else(|| {
1144 tera::Error::message("reReplaceAll filter requires `pattern` argument")
1145 })?;
1146 let replacement = args
1147 .get("replacement")
1148 .and_then(|v| v.as_str())
1149 .ok_or_else(|| {
1150 tera::Error::message("reReplaceAll filter requires `replacement` argument")
1151 })?;
1152 let re = Regex::new(pattern).map_err(|e| {
1153 tera::Error::message(format!("reReplaceAll: invalid regex '{}': {}", pattern, e))
1154 })?;
1155 Ok(Value::String(
1156 re.replace_all(&input, replacement).to_string(),
1157 ))
1158 },
1159 );
1160
1161 tera.register_json_function(
1164 "englishJoin",
1165 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1166 let items = args
1167 .get("items")
1168 .and_then(|v| v.as_array())
1169 .ok_or_else(|| tera::Error::message("englishJoin requires `items` argument"))?;
1170 let oxford = args.get("oxford").and_then(|v| v.as_bool()).unwrap_or(true);
1171 let strs: Vec<String> = items
1172 .iter()
1173 .map(|v| v.as_str().unwrap_or("").to_string())
1174 .filter(|s| !s.trim().is_empty())
1175 .collect();
1176 let result = match strs.len() {
1177 0 => String::new(),
1178 1 => strs[0].clone(),
1179 2 => format!("{} and {}", strs[0], strs[1]),
1180 _ => {
1181 let Some((last, rest)) = strs.split_last() else {
1184 return Ok(Value::String(String::new()));
1185 };
1186 if oxford {
1187 format!("{}, and {}", rest.join(", "), last)
1188 } else {
1189 format!("{} and {}", rest.join(", "), last)
1190 }
1191 }
1192 };
1193 Ok(Value::String(result))
1194 },
1195 );
1196
1197 tera.register_json_filter(
1199 "englishJoin",
1200 |value: &Value, args: &HashMap<String, Value>| {
1201 let items = value
1202 .as_array()
1203 .ok_or_else(|| tera::Error::message("englishJoin filter expects an array"))?;
1204 let oxford = args.get("oxford").and_then(|v| v.as_bool()).unwrap_or(true);
1205 let strs: Vec<String> = items
1206 .iter()
1207 .map(|v| v.as_str().unwrap_or("").to_string())
1208 .filter(|s| !s.trim().is_empty())
1209 .collect();
1210 let result = match strs.len() {
1211 0 => String::new(),
1212 1 => strs[0].clone(),
1213 2 => format!("{} and {}", strs[0], strs[1]),
1214 _ => {
1215 let Some((last, rest)) = strs.split_last() else {
1218 return Ok(Value::String(String::new()));
1219 };
1220 if oxford {
1221 format!("{}, and {}", rest.join(", "), last)
1222 } else {
1223 format!("{} and {}", rest.join(", "), last)
1224 }
1225 }
1226 };
1227 Ok(Value::String(result))
1228 },
1229 );
1230
1231 tera.register_json_filter("filter", |value: &Value, args: &HashMap<String, Value>| {
1233 let pattern = args
1234 .get("regexp")
1235 .and_then(|v| v.as_str())
1236 .ok_or_else(|| tera::Error::message("filter requires `regexp` argument"))?;
1237 let re = regex::Regex::new(pattern)
1238 .map_err(|e| tera::Error::message(format!("invalid regex '{}': {}", pattern, e)))?;
1239 let input = value.as_str().unwrap_or("");
1240 let result: Vec<&str> = input.lines().filter(|line| re.is_match(line)).collect();
1241 Ok(Value::String(result.join("\n")))
1242 });
1243
1244 tera.register_json_filter(
1246 "reverseFilter",
1247 |value: &Value, args: &HashMap<String, Value>| {
1248 let pattern = args
1249 .get("regexp")
1250 .and_then(|v| v.as_str())
1251 .ok_or_else(|| tera::Error::message("reverseFilter requires `regexp` argument"))?;
1252 let re = regex::Regex::new(pattern)
1253 .map_err(|e| tera::Error::message(format!("invalid regex '{}': {}", pattern, e)))?;
1254 let input = value.as_str().unwrap_or("");
1255 let result: Vec<&str> = input.lines().filter(|line| !re.is_match(line)).collect();
1256 Ok(Value::String(result.join("\n")))
1257 },
1258 );
1259
1260 tera.register_json_function(
1266 "filter",
1267 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1268 let items_val = args
1269 .get("items")
1270 .ok_or_else(|| tera::Error::message("filter requires `items` argument"))?;
1271 let pattern = args
1272 .get("regexp")
1273 .and_then(|v| v.as_str())
1274 .ok_or_else(|| tera::Error::message("filter requires `regexp` argument"))?;
1275 let re = Regex::new(pattern)
1276 .map_err(|e| tera::Error::message(format!("filter: invalid regex: {}", e)))?;
1277
1278 if let Some(s) = items_val.as_str() {
1279 let filtered: String = s
1281 .lines()
1282 .filter(|line| re.is_match(line))
1283 .collect::<Vec<_>>()
1284 .join("\n");
1285 Ok(Value::String(filtered))
1286 } else if let Some(arr) = items_val.as_array() {
1287 let filtered: Vec<Value> = arr
1289 .iter()
1290 .filter(|v| v.as_str().is_some_and(|s| re.is_match(s)))
1291 .cloned()
1292 .collect();
1293 Ok(Value::Array(filtered))
1294 } else {
1295 Err(tera::Error::message(
1296 "filter: `items` must be a string or array",
1297 ))
1298 }
1299 },
1300 );
1301
1302 tera.register_json_function(
1308 "reverseFilter",
1309 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1310 let items_val = args
1311 .get("items")
1312 .ok_or_else(|| tera::Error::message("reverseFilter requires `items` argument"))?;
1313 let pattern = args
1314 .get("regexp")
1315 .and_then(|v| v.as_str())
1316 .ok_or_else(|| tera::Error::message("reverseFilter requires `regexp` argument"))?;
1317 let re = Regex::new(pattern).map_err(|e| {
1318 tera::Error::message(format!("reverseFilter: invalid regex: {}", e))
1319 })?;
1320
1321 if let Some(s) = items_val.as_str() {
1322 let filtered: String = s
1324 .lines()
1325 .filter(|line| !re.is_match(line))
1326 .collect::<Vec<_>>()
1327 .join("\n");
1328 Ok(Value::String(filtered))
1329 } else if let Some(arr) = items_val.as_array() {
1330 let filtered: Vec<Value> = arr
1332 .iter()
1333 .filter(|v| !v.as_str().is_some_and(|s| re.is_match(s)))
1334 .cloned()
1335 .collect();
1336 Ok(Value::Array(filtered))
1337 } else {
1338 Err(tera::Error::message(
1339 "reverseFilter: `items` must be a string or array",
1340 ))
1341 }
1342 },
1343 );
1344
1345 tera.register_json_function(
1347 "indexOrDefault",
1348 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1349 let map = args
1350 .get("map")
1351 .and_then(|v| v.as_object())
1352 .ok_or_else(|| tera::Error::message("indexOrDefault requires `map` argument"))?;
1353 let key = args
1354 .get("key")
1355 .and_then(|v| v.as_str())
1356 .ok_or_else(|| tera::Error::message("indexOrDefault requires `key` argument"))?;
1357 let default = args
1358 .get("default")
1359 .cloned()
1360 .unwrap_or(Value::String(String::new()));
1361 Ok(map.get(key).cloned().unwrap_or(default))
1362 },
1363 );
1364
1365 tera.register_json_function(
1368 "replace",
1369 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1370 let s = args
1371 .get("s")
1372 .and_then(|v| v.as_str())
1373 .ok_or_else(|| tera::Error::message("replace requires `s` argument"))?;
1374 let old = args
1375 .get("old")
1376 .and_then(|v| v.as_str())
1377 .ok_or_else(|| tera::Error::message("replace requires `old` argument"))?;
1378 let new = args
1379 .get("new")
1380 .and_then(|v| v.as_str())
1381 .ok_or_else(|| tera::Error::message("replace requires `new` argument"))?;
1382 Ok(Value::String(s.replace(old, new)))
1383 },
1384 );
1385 tera.register_json_filter("replace", |value: &Value, args: &HashMap<String, Value>| {
1389 let s = try_get_value!("replace", "value", String, value);
1390 let from = args
1391 .get("from")
1392 .and_then(|v| v.as_str())
1393 .ok_or_else(|| tera::Error::message("replace filter requires `from` argument"))?;
1394 let to = args
1395 .get("to")
1396 .and_then(|v| v.as_str())
1397 .ok_or_else(|| tera::Error::message("replace filter requires `to` argument"))?;
1398 Ok(Value::String(s.replace(from, to)))
1399 });
1400
1401 tera.register_json_function(
1404 "split",
1405 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1406 let s = args
1407 .get("s")
1408 .and_then(|v| v.as_str())
1409 .ok_or_else(|| tera::Error::message("split requires `s` argument"))?;
1410 let sep = args
1411 .get("sep")
1412 .and_then(|v| v.as_str())
1413 .ok_or_else(|| tera::Error::message("split requires `sep` argument"))?;
1414 let parts: Vec<Value> = s.split(sep).map(|p| Value::String(p.to_string())).collect();
1415 Ok(Value::Array(parts))
1416 },
1417 );
1418 tera.register_json_filter("split", |value: &Value, args: &HashMap<String, Value>| {
1420 let s = try_get_value!("split", "value", String, value);
1421 let sep = args
1422 .get("sep")
1423 .and_then(|v| v.as_str())
1424 .ok_or_else(|| tera::Error::message("split filter requires `sep` argument"))?;
1425 let parts: Vec<Value> = s.split(sep).map(|p| Value::String(p.to_string())).collect();
1426 Ok(Value::Array(parts))
1427 });
1428
1429 tera.register_json_filter(
1431 "contains",
1432 |value: &Value, args: &HashMap<String, Value>| {
1433 let s = try_get_value!("contains", "value", String, value);
1434 let substr = args.get("substr").and_then(|v| v.as_str()).ok_or_else(|| {
1435 tera::Error::message("contains filter requires `substr` argument")
1436 })?;
1437 Ok(Value::Bool(s.contains(substr)))
1438 },
1439 );
1440
1441 tera.register_json_function(
1445 "trim",
1446 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1447 let s = args
1448 .get("s")
1449 .and_then(|v| v.as_str())
1450 .ok_or_else(|| tera::Error::message("trim requires `s` argument"))?;
1451 Ok(Value::String(s.trim().to_string()))
1452 },
1453 );
1454
1455 tera.register_json_function(
1459 "title",
1460 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1461 let s = args
1462 .get("s")
1463 .and_then(|v| v.as_str())
1464 .ok_or_else(|| tera::Error::message("title requires `s` argument"))?;
1465 let titled = s
1467 .split_whitespace()
1468 .map(|word| {
1469 let mut chars = word.chars();
1470 match chars.next() {
1471 Some(c) => {
1472 let upper: String = c.to_uppercase().collect();
1473 format!("{}{}", upper, chars.as_str())
1474 }
1475 None => String::new(),
1476 }
1477 })
1478 .collect::<Vec<_>>()
1479 .join(" ");
1480 Ok(Value::String(titled))
1481 },
1482 );
1483
1484 tera.register_json_function(
1488 "tolower",
1489 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1490 let s = args
1491 .get("s")
1492 .and_then(|v| v.as_str())
1493 .ok_or_else(|| tera::Error::message("tolower requires `s` argument"))?;
1494 Ok(Value::String(s.to_lowercase()))
1495 },
1496 );
1497
1498 tera.register_json_function(
1500 "toupper",
1501 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1502 let s = args
1503 .get("s")
1504 .and_then(|v| v.as_str())
1505 .ok_or_else(|| tera::Error::message("toupper requires `s` argument"))?;
1506 Ok(Value::String(s.to_uppercase()))
1507 },
1508 );
1509
1510 tera.register_json_function(
1512 "trimprefix",
1513 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1514 let s = args
1515 .get("s")
1516 .and_then(|v| v.as_str())
1517 .ok_or_else(|| tera::Error::message("trimprefix requires `s` argument"))?;
1518 let prefix = args
1519 .get("prefix")
1520 .and_then(|v| v.as_str())
1521 .ok_or_else(|| tera::Error::message("trimprefix requires `prefix` argument"))?;
1522 let result = s.strip_prefix(prefix).unwrap_or(s);
1523 Ok(Value::String(result.to_string()))
1524 },
1525 );
1526
1527 tera.register_json_function(
1529 "trimsuffix",
1530 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1531 let s = args
1532 .get("s")
1533 .and_then(|v| v.as_str())
1534 .ok_or_else(|| tera::Error::message("trimsuffix requires `s` argument"))?;
1535 let suffix = args
1536 .get("suffix")
1537 .and_then(|v| v.as_str())
1538 .ok_or_else(|| tera::Error::message("trimsuffix requires `suffix` argument"))?;
1539 let result = s.strip_suffix(suffix).unwrap_or(s);
1540 Ok(Value::String(result.to_string()))
1541 },
1542 );
1543
1544 tera.register_json_function(
1546 "dir",
1547 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1548 let s = args
1549 .get("s")
1550 .and_then(|v| v.as_str())
1551 .ok_or_else(|| tera::Error::message("dir requires `s` argument"))?;
1552 let p = std::path::Path::new(s);
1553 Ok(Value::String(
1554 p.parent()
1555 .map(|p| p.to_string_lossy().to_string())
1556 .unwrap_or_default(),
1557 ))
1558 },
1559 );
1560
1561 tera.register_json_function(
1563 "base",
1564 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1565 let s = args
1566 .get("s")
1567 .and_then(|v| v.as_str())
1568 .ok_or_else(|| tera::Error::message("base requires `s` argument"))?;
1569 let p = std::path::Path::new(s);
1570 Ok(Value::String(
1571 p.file_name()
1572 .map(|n| n.to_string_lossy().to_string())
1573 .unwrap_or_default(),
1574 ))
1575 },
1576 );
1577
1578 tera.register_json_function(
1580 "abs",
1581 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1582 let s = args
1583 .get("s")
1584 .and_then(|v| v.as_str())
1585 .ok_or_else(|| tera::Error::message("abs requires `s` argument"))?;
1586 let p = std::path::Path::new(s);
1587 if p.is_absolute() {
1588 Ok(Value::String(s.to_string()))
1589 } else {
1590 let abs = std::env::current_dir()
1591 .map(|cwd| cwd.join(p).to_string_lossy().to_string())
1592 .unwrap_or_else(|_| s.to_string());
1593 Ok(Value::String(abs))
1594 }
1595 },
1596 );
1597
1598 tera.register_json_function(
1600 "urlPathEscape",
1601 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1602 let s = args
1603 .get("s")
1604 .and_then(|v| v.as_str())
1605 .ok_or_else(|| tera::Error::message("urlPathEscape requires `s` argument"))?;
1606 let encoded: String = s
1607 .bytes()
1608 .map(|b| {
1609 if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.' || b == b'~'
1610 {
1611 (b as char).to_string()
1612 } else {
1613 format!("%{:02X}", b)
1614 }
1615 })
1616 .collect();
1617 Ok(Value::String(encoded))
1618 },
1619 );
1620
1621 tera.register_json_function(
1623 "mdv2escape",
1624 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1625 let s = args
1626 .get("s")
1627 .and_then(|v| v.as_str())
1628 .ok_or_else(|| tera::Error::message("mdv2escape requires `s` argument"))?;
1629 let escaped = s
1630 .chars()
1631 .map(|c| {
1632 if "_*[]()~`>#+-=|{}.!".contains(c) {
1633 format!("\\{}", c)
1634 } else {
1635 c.to_string()
1636 }
1637 })
1638 .collect::<String>();
1639 Ok(Value::String(escaped))
1640 },
1641 );
1642
1643 tera.register_json_filter("incpatch", |value: &Value, _: &HashMap<String, Value>| {
1647 let v = try_get_value!("incpatch", "value", String, value);
1648 Ok(Value::String(increment_version(&v, VersionPart::Patch)?))
1649 });
1650
1651 tera.register_json_filter("incminor", |value: &Value, _: &HashMap<String, Value>| {
1653 let v = try_get_value!("incminor", "value", String, value);
1654 Ok(Value::String(increment_version(&v, VersionPart::Minor)?))
1655 });
1656
1657 tera.register_json_filter("incmajor", |value: &Value, _: &HashMap<String, Value>| {
1659 let v = try_get_value!("incmajor", "value", String, value);
1660 Ok(Value::String(increment_version(&v, VersionPart::Major)?))
1661 });
1662
1663 tera.register_json_filter(
1673 "now_format",
1674 |_value: &Value, args: &HashMap<String, Value>| {
1675 let fmt = args
1676 .get("format")
1677 .and_then(|v| v.as_str())
1678 .ok_or_else(|| tera::Error::message("now_format requires a `format` argument"))?;
1679 let chrono_fmt = translate_go_time_format(fmt);
1680 let now = crate::sde::resolve_now();
1681 Ok(Value::String(now.format(&chrono_fmt).to_string()))
1682 },
1683 );
1684
1685 tera.register_json_function(
1690 "index",
1691 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1692 let collection = args
1693 .get("collection")
1694 .ok_or_else(|| tera::Error::message("index requires `collection` argument"))?;
1695 let key = args
1696 .get("key")
1697 .ok_or_else(|| tera::Error::message("index requires `key` argument"))?;
1698
1699 match collection {
1700 Value::Object(map) => {
1701 let key_str = value_to_string(key);
1702 Ok(map
1703 .get(key_str.as_ref())
1704 .cloned()
1705 .unwrap_or(Value::String(String::new())))
1706 }
1707 Value::Array(arr) => {
1708 if let Some(idx) = key.as_u64() {
1709 Ok(arr
1710 .get(idx as usize)
1711 .cloned()
1712 .unwrap_or(Value::String(String::new())))
1713 } else {
1714 Err(tera::Error::message("index: array index must be a number"))
1715 }
1716 }
1717 _ => {
1718 Ok(Value::String(String::new()))
1720 }
1721 }
1722 },
1723 );
1724
1725 let in_filter = |value: &Value, args: &HashMap<String, Value>| {
1728 let items = value
1729 .as_array()
1730 .ok_or_else(|| tera::Error::message("in filter requires an array as input"))?;
1731 let needle = args
1732 .get("value")
1733 .ok_or_else(|| tera::Error::message("in filter requires `value` argument"))?;
1734 let needle_str = value_to_string(needle);
1735 let found = items.iter().any(|item| value_to_string(item) == needle_str);
1736 Ok(Value::Bool(found))
1737 };
1738 tera.register_json_filter("in", in_filter);
1739 tera.register_json_filter("contains_any", in_filter);
1740
1741 tera.register_json_filter("slice", |value: &Value, args: &HashMap<String, Value>| {
1749 let start = args.get("start").and_then(|v| v.as_i64()).unwrap_or(0);
1750 let end = args.get("end").and_then(|v| v.as_i64());
1751
1752 let resolve = |idx: i64, len: i64| -> i64 {
1754 let abs = if idx < 0 { len + idx } else { idx };
1755 abs.clamp(0, len)
1756 };
1757
1758 match value {
1759 Value::String(s) => {
1760 let chars: Vec<char> = s.chars().collect();
1761 let len = chars.len() as i64;
1762 let lo = resolve(start, len);
1763 let hi = resolve(end.unwrap_or(len), len).max(lo) as usize;
1764 Ok(Value::String(chars[lo as usize..hi].iter().collect()))
1765 }
1766 Value::Array(arr) => {
1767 let len = arr.len() as i64;
1768 let lo = resolve(start, len);
1769 let hi = resolve(end.unwrap_or(len), len).max(lo) as usize;
1770 Ok(Value::Array(arr[lo as usize..hi].to_vec()))
1771 }
1772 other => Err(tera::Error::message(format!(
1773 "slice: expected a string or array, got {}",
1774 other
1775 ))),
1776 }
1777 });
1778
1779 tera.register_json_function(
1783 "printf",
1784 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1785 let format = args
1786 .get("format")
1787 .and_then(|v| v.as_str())
1788 .ok_or_else(|| tera::Error::message("printf requires a `format` argument"))?;
1789 let fmt_args: Vec<Value> = args
1790 .get("args")
1791 .and_then(|v| v.as_array())
1792 .cloned()
1793 .unwrap_or_default();
1794 Ok(Value::String(sprintf(format, &fmt_args)?))
1795 },
1796 );
1797
1798 tera.register_json_function(
1804 "print",
1805 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1806 let fmt_args: Vec<Value> = args
1807 .get("args")
1808 .and_then(|v| v.as_array())
1809 .cloned()
1810 .unwrap_or_default();
1811 let mut out = String::new();
1812 for (i, v) in fmt_args.iter().enumerate() {
1813 if i > 0 {
1814 let prev_str = fmt_args[i - 1].is_string();
1815 let cur_str = v.is_string();
1816 if !prev_str && !cur_str {
1817 out.push(' ');
1818 }
1819 }
1820 out.push_str(&printf_default(v));
1821 }
1822 Ok(Value::String(out))
1823 },
1824 );
1825 tera.register_json_function(
1826 "println",
1827 |args: &HashMap<String, Value>| -> TeraResult<Value> {
1828 let fmt_args: Vec<Value> = args
1829 .get("args")
1830 .and_then(|v| v.as_array())
1831 .cloned()
1832 .unwrap_or_default();
1833 let mut joined = fmt_args
1834 .iter()
1835 .map(printf_default)
1836 .collect::<Vec<_>>()
1837 .join(" ");
1838 joined.push('\n');
1839 Ok(Value::String(joined))
1840 },
1841 );
1842
1843 tera
1844});