use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::LazyLock;
use tera::Value;
use sha1::Digest as Sha1Digest;
use sha2::Digest as Sha2Digest;
use sha3::Digest as Sha3Digest;
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
fn expand_tilde(path: &str) -> String {
if let Some(rest) = path.strip_prefix("~/")
&& let Ok(home) = std::env::var("HOME")
{
return format!("{}/{}", home, rest);
}
path.to_string()
}
fn value_to_string(v: &Value) -> Cow<'_, str> {
match v {
Value::String(s) => Cow::Borrowed(s.as_str()),
Value::Number(n) => Cow::Owned(n.to_string()),
Value::Bool(b) => Cow::Owned(b.to_string()),
Value::Null => Cow::Borrowed(""),
other => Cow::Owned(other.to_string()),
}
}
pub(super) fn translate_go_time_format(fmt: &str) -> Cow<'_, str> {
if fmt.contains('%') {
return Cow::Borrowed(fmt);
}
const GO_MARKERS: &[&str] = &[
"2006", "06", "January", "Jan", "01", "Monday", "Mon", "02", "15", "03", "04", "05", "PM",
"pm", "-0700", "Z0700", "MST",
];
let has_go_patterns = GO_MARKERS.iter().any(|p| fmt.contains(p));
if !has_go_patterns {
return Cow::Borrowed(fmt);
}
let mut result = fmt.to_string();
let replacements: &[(&str, &str)] = &[
("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"), ];
for (go_pat, chrono_pat) in replacements {
result = result.replace(go_pat, chrono_pat);
}
Cow::Owned(result)
}
enum VersionPart {
Major,
Minor,
Patch,
}
fn increment_version(v: &str, part: VersionPart) -> Result<String, tera::Error> {
let stripped = v.strip_prefix('v').unwrap_or(v);
let parts: Vec<&str> = stripped.splitn(3, '.').collect();
let invalid = || {
tera::Error::msg(format!(
"incpatch/incminor/incmajor: '{}' is not a valid semver version (expected MAJOR.MINOR.PATCH)",
v
))
};
if parts.len() < 3 {
return Err(invalid());
}
let major: u64 = parts
.first()
.and_then(|s| s.parse().ok())
.ok_or_else(invalid)?;
let minor: u64 = parts
.get(1)
.and_then(|s| s.parse().ok())
.ok_or_else(invalid)?;
let patch: u64 = parts
.get(2)
.and_then(|s| {
s.split('-').next().and_then(|n| n.parse().ok())
})
.ok_or_else(invalid)?;
let prefix = if v.starts_with('v') { "v" } else { "" };
Ok(match part {
VersionPart::Major => format!("{}{}.0.0", prefix, major + 1),
VersionPart::Minor => format!("{}{}.{}.0", prefix, major, minor + 1),
VersionPart::Patch => format!("{}{}.{}.{}", prefix, major, minor, patch + 1),
})
}
pub(super) static BASE_TERA: LazyLock<tera::Tera> = LazyLock::new(|| {
let mut tera = tera::Tera::default();
tera.register_filter("tolower", |value: &Value, _: &HashMap<String, Value>| {
let s = tera::try_get_value!("tolower", "value", String, value);
Ok(Value::String(s.to_lowercase()))
});
tera.register_filter("toupper", |value: &Value, _: &HashMap<String, Value>| {
let s = tera::try_get_value!("toupper", "value", String, value);
Ok(Value::String(s.to_uppercase()))
});
tera.register_filter(
"trimprefix",
|value: &Value, args: &HashMap<String, Value>| {
let s = tera::try_get_value!("trimprefix", "value", String, value);
let prefix = args
.get("prefix")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("trimprefix requires a `prefix` argument"))?;
let result = s.strip_prefix(prefix).unwrap_or(&s);
Ok(Value::String(result.to_string()))
},
);
tera.register_filter(
"trimsuffix",
|value: &Value, args: &HashMap<String, Value>| {
let s = tera::try_get_value!("trimsuffix", "value", String, value);
let suffix = args
.get("suffix")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("trimsuffix requires a `suffix` argument"))?;
let result = s.strip_suffix(suffix).unwrap_or(&s);
Ok(Value::String(result.to_string()))
},
);
tera.register_function(
"envOrDefault",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let name = args
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("envOrDefault requires `name` argument"))?;
let default = args.get("default").and_then(|v| v.as_str()).unwrap_or("");
let value = std::env::var(name).unwrap_or_else(|_| default.to_string());
Ok(Value::String(value))
},
);
tera.register_function(
"isEnvSet",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let name = args
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("isEnvSet requires `name` argument"))?;
let is_set = std::env::var(name).map(|v| !v.is_empty()).unwrap_or(false);
Ok(Value::Bool(is_set))
},
);
tera.register_function(
"incpatch",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let v = args
.get("v")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("incpatch requires `v` argument"))?;
Ok(Value::String(increment_version(v, VersionPart::Patch)?))
},
);
tera.register_function(
"incminor",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let v = args
.get("v")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("incminor requires `v` argument"))?;
Ok(Value::String(increment_version(v, VersionPart::Minor)?))
},
);
tera.register_function(
"incmajor",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let v = args
.get("v")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("incmajor requires `v` argument"))?;
Ok(Value::String(increment_version(v, VersionPart::Major)?))
},
);
macro_rules! register_hash_fn {
($tera:expr, $name:expr, $hash_fn:expr) => {
$tera.register_function(
$name,
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args.get("s").and_then(|v| v.as_str()).ok_or_else(|| {
tera::Error::msg(format!("{} requires `s` argument", $name))
})?;
let bytes = std::fs::read(s).map_err(|e| {
tera::Error::msg(format!("{}: failed to read file '{}': {}", $name, s, e))
})?;
Ok(Value::String($hash_fn(&bytes)))
},
);
};
}
register_hash_fn!(tera, "sha1", |b: &[u8]| {
let mut h = sha1::Sha1::new();
Sha1Digest::update(&mut h, b);
hex_encode(&Sha1Digest::finalize(h))
});
register_hash_fn!(tera, "sha224", |b: &[u8]| {
let mut h = sha2::Sha224::new();
Sha2Digest::update(&mut h, b);
hex_encode(&Sha2Digest::finalize(h))
});
register_hash_fn!(tera, "sha256", |b: &[u8]| {
let mut h = sha2::Sha256::new();
Sha2Digest::update(&mut h, b);
hex_encode(&Sha2Digest::finalize(h))
});
register_hash_fn!(tera, "sha384", |b: &[u8]| {
let mut h = sha2::Sha384::new();
Sha2Digest::update(&mut h, b);
hex_encode(&Sha2Digest::finalize(h))
});
register_hash_fn!(tera, "sha512", |b: &[u8]| {
let mut h = sha2::Sha512::new();
Sha2Digest::update(&mut h, b);
hex_encode(&Sha2Digest::finalize(h))
});
register_hash_fn!(tera, "sha3_224", |b: &[u8]| {
let mut h = sha3::Sha3_224::new();
Sha3Digest::update(&mut h, b);
hex_encode(&Sha3Digest::finalize(h))
});
register_hash_fn!(tera, "sha3_256", |b: &[u8]| {
let mut h = sha3::Sha3_256::new();
Sha3Digest::update(&mut h, b);
hex_encode(&Sha3Digest::finalize(h))
});
register_hash_fn!(tera, "sha3_384", |b: &[u8]| {
let mut h = sha3::Sha3_384::new();
Sha3Digest::update(&mut h, b);
hex_encode(&Sha3Digest::finalize(h))
});
register_hash_fn!(tera, "sha3_512", |b: &[u8]| {
let mut h = sha3::Sha3_512::new();
Sha3Digest::update(&mut h, b);
hex_encode(&Sha3Digest::finalize(h))
});
register_hash_fn!(tera, "blake2b", |b: &[u8]| {
let mut h = blake2::Blake2b512::new();
blake2::Digest::update(&mut h, b);
hex_encode(&blake2::Digest::finalize(h))
});
register_hash_fn!(tera, "blake2s", |b: &[u8]| {
let mut h = blake2::Blake2s256::new();
blake2::Digest::update(&mut h, b);
hex_encode(&blake2::Digest::finalize(h))
});
register_hash_fn!(tera, "blake3", |b: &[u8]| {
hex_encode(blake3::hash(b).as_bytes())
});
register_hash_fn!(tera, "md5", |b: &[u8]| {
let mut h = md5::Md5::new();
md5::Digest::update(&mut h, b);
hex_encode(&md5::Digest::finalize(h))
});
register_hash_fn!(tera, "crc32", |b: &[u8]| {
format!("{:08x}", crc32fast::hash(b))
});
tera.register_function(
"readFile",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let path = args
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("readFile requires `path` argument"))?;
let resolved = expand_tilde(path);
let content = std::fs::read_to_string(resolved).unwrap_or_default();
Ok(Value::String(content.trim().to_string()))
},
);
tera.register_function(
"mustReadFile",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let path = args
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("mustReadFile requires `path` argument"))?;
let resolved = expand_tilde(path);
let content = std::fs::read_to_string(&resolved)
.map_err(|e| tera::Error::msg(format!("mustReadFile: {}: {}", resolved, e)))?;
Ok(Value::String(content.trim().to_string()))
},
);
tera.register_function(
"time",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let fmt = args
.get("format")
.and_then(|v| v.as_str())
.unwrap_or("%Y-%m-%dT%H:%M:%SZ");
let chrono_fmt = translate_go_time_format(fmt);
let now = chrono::Utc::now();
Ok(Value::String(now.format(&chrono_fmt).to_string()))
},
);
tera.register_filter("dir", |value: &Value, _: &HashMap<String, Value>| {
let s = tera::try_get_value!("dir", "value", String, value);
let p = std::path::Path::new(&s);
Ok(Value::String(
p.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default(),
))
});
tera.register_filter("base", |value: &Value, _: &HashMap<String, Value>| {
let s = tera::try_get_value!("base", "value", String, value);
let p = std::path::Path::new(&s);
Ok(Value::String(
p.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default(),
))
});
tera.register_filter("abs", |value: &Value, _: &HashMap<String, Value>| {
let s = tera::try_get_value!("abs", "value", String, value);
let p = std::path::Path::new(&s);
if p.is_absolute() {
Ok(Value::String(s))
} else {
let abs = std::env::current_dir()
.map(|cwd| cwd.join(p).to_string_lossy().to_string())
.unwrap_or(s);
Ok(Value::String(abs))
}
});
tera.register_filter(
"urlPathEscape",
|value: &Value, _: &HashMap<String, Value>| {
let s = tera::try_get_value!("urlPathEscape", "value", String, value);
let encoded: String = s
.bytes()
.map(|b| {
if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.' || b == b'~'
{
(b as char).to_string()
} else {
format!("%{:02X}", b)
}
})
.collect();
Ok(Value::String(encoded))
},
);
tera.register_filter("mdv2escape", |value: &Value, _: &HashMap<String, Value>| {
let s = tera::try_get_value!("mdv2escape", "value", String, value);
let escaped = s
.chars()
.map(|c| {
if "_*[]()~`>#+-=|{}.!".contains(c) {
format!("\\{}", c)
} else {
c.to_string()
}
})
.collect::<String>();
Ok(Value::String(escaped))
});
tera.register_function(
"contains",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("contains requires `s` argument"))?;
let substr = args
.get("substr")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("contains requires `substr` argument"))?;
Ok(Value::Bool(s.contains(substr)))
},
);
tera.register_function(
"list",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let items = args
.get("items")
.and_then(|v| v.as_array())
.ok_or_else(|| tera::Error::msg("list requires `items` argument"))?;
Ok(Value::Array(items.clone()))
},
);
tera.register_function(
"map",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let pairs = args
.get("pairs")
.and_then(|v| v.as_array())
.ok_or_else(|| tera::Error::msg("map requires `pairs` argument"))?;
if pairs.len() % 2 != 0 {
return Err(tera::Error::msg(
"map requires an even number of arguments (key-value pairs)",
));
}
let mut result = tera::Map::new();
for chunk in pairs.chunks(2) {
let key = chunk[0].as_str().unwrap_or("").to_string();
result.insert(key, chunk[1].clone());
}
Ok(Value::Object(result))
},
);
let in_fn = |args: &HashMap<String, Value>| -> tera::Result<Value> {
let items = args
.get("items")
.and_then(|v| v.as_array())
.ok_or_else(|| tera::Error::msg("in requires `items` argument (must be an array)"))?;
let value = args
.get("value")
.ok_or_else(|| tera::Error::msg("in requires `value` argument"))?;
let needle = value_to_string(value);
let found = items.iter().any(|item| value_to_string(item) == needle);
Ok(Value::Bool(found))
};
tera.register_function("in", in_fn);
tera.register_function("contains_any", in_fn);
tera.register_function(
"reReplaceAll",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let pattern = args
.get("pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("reReplaceAll requires `pattern` argument"))?;
let input = args
.get("input")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("reReplaceAll requires `input` argument"))?;
let replacement = args
.get("replacement")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("reReplaceAll requires `replacement` argument"))?;
let re = Regex::new(pattern).map_err(|e| {
tera::Error::msg(format!("reReplaceAll: invalid regex '{}': {}", pattern, e))
})?;
Ok(Value::String(
re.replace_all(input, replacement).to_string(),
))
},
);
tera.register_filter(
"reReplaceAll",
|value: &Value, args: &HashMap<String, Value>| {
let input = tera::try_get_value!("reReplaceAll", "value", String, value);
let pattern = args
.get("pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| {
tera::Error::msg("reReplaceAll filter requires `pattern` argument")
})?;
let replacement = args
.get("replacement")
.and_then(|v| v.as_str())
.ok_or_else(|| {
tera::Error::msg("reReplaceAll filter requires `replacement` argument")
})?;
let re = Regex::new(pattern).map_err(|e| {
tera::Error::msg(format!("reReplaceAll: invalid regex '{}': {}", pattern, e))
})?;
Ok(Value::String(
re.replace_all(&input, replacement).to_string(),
))
},
);
tera.register_function(
"englishJoin",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let items = args
.get("items")
.and_then(|v| v.as_array())
.ok_or_else(|| tera::Error::msg("englishJoin requires `items` argument"))?;
let oxford = args.get("oxford").and_then(|v| v.as_bool()).unwrap_or(true);
let strs: Vec<String> = items
.iter()
.map(|v| v.as_str().unwrap_or("").to_string())
.filter(|s| !s.trim().is_empty())
.collect();
let result = match strs.len() {
0 => String::new(),
1 => strs[0].clone(),
2 => format!("{} and {}", strs[0], strs[1]),
_ => {
let Some((last, rest)) = strs.split_last() else {
return Ok(Value::String(String::new()));
};
if oxford {
format!("{}, and {}", rest.join(", "), last)
} else {
format!("{} and {}", rest.join(", "), last)
}
}
};
Ok(Value::String(result))
},
);
tera.register_filter(
"englishJoin",
|value: &Value, args: &HashMap<String, Value>| {
let items = value
.as_array()
.ok_or_else(|| tera::Error::msg("englishJoin filter expects an array"))?;
let oxford = args.get("oxford").and_then(|v| v.as_bool()).unwrap_or(true);
let strs: Vec<String> = items
.iter()
.map(|v| v.as_str().unwrap_or("").to_string())
.filter(|s| !s.trim().is_empty())
.collect();
let result = match strs.len() {
0 => String::new(),
1 => strs[0].clone(),
2 => format!("{} and {}", strs[0], strs[1]),
_ => {
let Some((last, rest)) = strs.split_last() else {
return Ok(Value::String(String::new()));
};
if oxford {
format!("{}, and {}", rest.join(", "), last)
} else {
format!("{} and {}", rest.join(", "), last)
}
}
};
Ok(Value::String(result))
},
);
tera.register_filter("filter", |value: &Value, args: &HashMap<String, Value>| {
let pattern = args
.get("regexp")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("filter requires `regexp` argument"))?;
let re = regex::Regex::new(pattern)
.map_err(|e| tera::Error::msg(format!("invalid regex '{}': {}", pattern, e)))?;
let input = value.as_str().unwrap_or("");
let result: Vec<&str> = input.lines().filter(|line| re.is_match(line)).collect();
Ok(Value::String(result.join("\n")))
});
tera.register_filter(
"reverseFilter",
|value: &Value, args: &HashMap<String, Value>| {
let pattern = args
.get("regexp")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("reverseFilter requires `regexp` argument"))?;
let re = regex::Regex::new(pattern)
.map_err(|e| tera::Error::msg(format!("invalid regex '{}': {}", pattern, e)))?;
let input = value.as_str().unwrap_or("");
let result: Vec<&str> = input.lines().filter(|line| !re.is_match(line)).collect();
Ok(Value::String(result.join("\n")))
},
);
tera.register_function(
"filter",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let items_val = args
.get("items")
.ok_or_else(|| tera::Error::msg("filter requires `items` argument"))?;
let pattern = args
.get("regexp")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("filter requires `regexp` argument"))?;
let re = Regex::new(pattern)
.map_err(|e| tera::Error::msg(format!("filter: invalid regex: {}", e)))?;
if let Some(s) = items_val.as_str() {
let filtered: String = s
.lines()
.filter(|line| re.is_match(line))
.collect::<Vec<_>>()
.join("\n");
Ok(Value::String(filtered))
} else if let Some(arr) = items_val.as_array() {
let filtered: Vec<Value> = arr
.iter()
.filter(|v| v.as_str().is_some_and(|s| re.is_match(s)))
.cloned()
.collect();
Ok(Value::Array(filtered))
} else {
Err(tera::Error::msg(
"filter: `items` must be a string or array",
))
}
},
);
tera.register_function(
"reverseFilter",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let items_val = args
.get("items")
.ok_or_else(|| tera::Error::msg("reverseFilter requires `items` argument"))?;
let pattern = args
.get("regexp")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("reverseFilter requires `regexp` argument"))?;
let re = Regex::new(pattern)
.map_err(|e| tera::Error::msg(format!("reverseFilter: invalid regex: {}", e)))?;
if let Some(s) = items_val.as_str() {
let filtered: String = s
.lines()
.filter(|line| !re.is_match(line))
.collect::<Vec<_>>()
.join("\n");
Ok(Value::String(filtered))
} else if let Some(arr) = items_val.as_array() {
let filtered: Vec<Value> = arr
.iter()
.filter(|v| !v.as_str().is_some_and(|s| re.is_match(s)))
.cloned()
.collect();
Ok(Value::Array(filtered))
} else {
Err(tera::Error::msg(
"reverseFilter: `items` must be a string or array",
))
}
},
);
tera.register_function(
"indexOrDefault",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let map = args
.get("map")
.and_then(|v| v.as_object())
.ok_or_else(|| tera::Error::msg("indexOrDefault requires `map` argument"))?;
let key = args
.get("key")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("indexOrDefault requires `key` argument"))?;
let default = args
.get("default")
.cloned()
.unwrap_or(Value::String(String::new()));
Ok(map.get(key).cloned().unwrap_or(default))
},
);
tera.register_function(
"replace",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("replace requires `s` argument"))?;
let old = args
.get("old")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("replace requires `old` argument"))?;
let new = args
.get("new")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("replace requires `new` argument"))?;
Ok(Value::String(s.replace(old, new)))
},
);
tera.register_filter("replace", |value: &Value, args: &HashMap<String, Value>| {
let s = tera::try_get_value!("replace", "value", String, value);
let from = args
.get("from")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("replace filter requires `from` argument"))?;
let to = args
.get("to")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("replace filter requires `to` argument"))?;
Ok(Value::String(s.replace(from, to)))
});
tera.register_function(
"split",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("split requires `s` argument"))?;
let sep = args
.get("sep")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("split requires `sep` argument"))?;
let parts: Vec<Value> = s.split(sep).map(|p| Value::String(p.to_string())).collect();
Ok(Value::Array(parts))
},
);
tera.register_filter("split", |value: &Value, args: &HashMap<String, Value>| {
let s = tera::try_get_value!("split", "value", String, value);
let sep = args
.get("sep")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("split filter requires `sep` argument"))?;
let parts: Vec<Value> = s.split(sep).map(|p| Value::String(p.to_string())).collect();
Ok(Value::Array(parts))
});
tera.register_filter(
"contains",
|value: &Value, args: &HashMap<String, Value>| {
let s = tera::try_get_value!("contains", "value", String, value);
let substr = args
.get("substr")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("contains filter requires `substr` argument"))?;
Ok(Value::Bool(s.contains(substr)))
},
);
tera.register_function(
"trim",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("trim requires `s` argument"))?;
Ok(Value::String(s.trim().to_string()))
},
);
tera.register_function(
"title",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("title requires `s` argument"))?;
let titled = s
.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
Some(c) => {
let upper: String = c.to_uppercase().collect();
format!("{}{}", upper, chars.as_str())
}
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ");
Ok(Value::String(titled))
},
);
tera.register_function(
"tolower",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("tolower requires `s` argument"))?;
Ok(Value::String(s.to_lowercase()))
},
);
tera.register_function(
"toupper",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("toupper requires `s` argument"))?;
Ok(Value::String(s.to_uppercase()))
},
);
tera.register_function(
"trimprefix",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("trimprefix requires `s` argument"))?;
let prefix = args
.get("prefix")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("trimprefix requires `prefix` argument"))?;
let result = s.strip_prefix(prefix).unwrap_or(s);
Ok(Value::String(result.to_string()))
},
);
tera.register_function(
"trimsuffix",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("trimsuffix requires `s` argument"))?;
let suffix = args
.get("suffix")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("trimsuffix requires `suffix` argument"))?;
let result = s.strip_suffix(suffix).unwrap_or(s);
Ok(Value::String(result.to_string()))
},
);
tera.register_function(
"dir",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("dir requires `s` argument"))?;
let p = std::path::Path::new(s);
Ok(Value::String(
p.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default(),
))
},
);
tera.register_function(
"base",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("base requires `s` argument"))?;
let p = std::path::Path::new(s);
Ok(Value::String(
p.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default(),
))
},
);
tera.register_function(
"abs",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("abs requires `s` argument"))?;
let p = std::path::Path::new(s);
if p.is_absolute() {
Ok(Value::String(s.to_string()))
} else {
let abs = std::env::current_dir()
.map(|cwd| cwd.join(p).to_string_lossy().to_string())
.unwrap_or_else(|_| s.to_string());
Ok(Value::String(abs))
}
},
);
tera.register_function(
"urlPathEscape",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("urlPathEscape requires `s` argument"))?;
let encoded: String = s
.bytes()
.map(|b| {
if b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.' || b == b'~'
{
(b as char).to_string()
} else {
format!("%{:02X}", b)
}
})
.collect();
Ok(Value::String(encoded))
},
);
tera.register_function(
"mdv2escape",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let s = args
.get("s")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("mdv2escape requires `s` argument"))?;
let escaped = s
.chars()
.map(|c| {
if "_*[]()~`>#+-=|{}.!".contains(c) {
format!("\\{}", c)
} else {
c.to_string()
}
})
.collect::<String>();
Ok(Value::String(escaped))
},
);
tera.register_filter("incpatch", |value: &Value, _: &HashMap<String, Value>| {
let v = tera::try_get_value!("incpatch", "value", String, value);
Ok(Value::String(increment_version(&v, VersionPart::Patch)?))
});
tera.register_filter("incminor", |value: &Value, _: &HashMap<String, Value>| {
let v = tera::try_get_value!("incminor", "value", String, value);
Ok(Value::String(increment_version(&v, VersionPart::Minor)?))
});
tera.register_filter("incmajor", |value: &Value, _: &HashMap<String, Value>| {
let v = tera::try_get_value!("incmajor", "value", String, value);
Ok(Value::String(increment_version(&v, VersionPart::Major)?))
});
tera.register_filter(
"now_format",
|_value: &Value, args: &HashMap<String, Value>| {
let fmt = args
.get("format")
.and_then(|v| v.as_str())
.ok_or_else(|| tera::Error::msg("now_format requires a `format` argument"))?;
let chrono_fmt = translate_go_time_format(fmt);
let now = chrono::Utc::now();
Ok(Value::String(now.format(&chrono_fmt).to_string()))
},
);
tera.register_function(
"index",
|args: &HashMap<String, Value>| -> tera::Result<Value> {
let collection = args
.get("collection")
.ok_or_else(|| tera::Error::msg("index requires `collection` argument"))?;
let key = args
.get("key")
.ok_or_else(|| tera::Error::msg("index requires `key` argument"))?;
match collection {
Value::Object(map) => {
let key_str = value_to_string(key);
Ok(map
.get(key_str.as_ref())
.cloned()
.unwrap_or(Value::String(String::new())))
}
Value::Array(arr) => {
if let Some(idx) = key.as_u64() {
Ok(arr
.get(idx as usize)
.cloned()
.unwrap_or(Value::String(String::new())))
} else {
Err(tera::Error::msg("index: array index must be a number"))
}
}
_ => {
Ok(Value::String(String::new()))
}
}
},
);
let in_filter = |value: &Value, args: &HashMap<String, Value>| {
let items = value
.as_array()
.ok_or_else(|| tera::Error::msg("in filter requires an array as input"))?;
let needle = args
.get("value")
.ok_or_else(|| tera::Error::msg("in filter requires `value` argument"))?;
let needle_str = value_to_string(needle);
let found = items.iter().any(|item| value_to_string(item) == needle_str);
Ok(Value::Bool(found))
};
tera.register_filter("in", in_filter);
tera.register_filter("contains_any", in_filter);
tera
});