use super::blocks::replace_live_blocks;
use super::go_blocks::{extract_block_parts, try_rewrite_control_block};
use super::static_regex;
use super::string_lit::RAW_STRING_RE_ALT;
use super::tokens::{MAX_EXPR_NESTING, Token, significant_tokens, token_to_str, tokenize_block};
use regex::Regex;
use std::sync::LazyLock;
static MAP_POSITIONAL_RE: LazyLock<Regex> = LazyLock::new(|| {
let item = format!(r"(?:{RAW_STRING_RE_ALT}|[a-zA-Z_][a-zA-Z0-9_.]*)");
let pattern = format!(r"(?:^|(?P<pre>[^a-zA-Z0-9_]))map\s+(?P<args>{item}(?:\s+{item})+)");
static_regex(&pattern)
});
pub(super) fn preprocess_map_syntax(template: &str) -> String {
replace_live_blocks(template, |block: &str| {
if !block.contains("map ") {
return block.to_string();
}
if block.contains("map(") {
return block.to_string();
}
let (open, inner, close) = extract_block_parts(block);
let rewritten = MAP_POSITIONAL_RE
.replace_all(inner, |mcaps: ®ex::Captures| {
let pre = mcaps.name("pre").map_or("", |m| m.as_str());
let args_str = mcaps.name("args").map_or("", |m| m.as_str());
static ITEM_RE: LazyLock<Regex> = LazyLock::new(|| {
static_regex(&format!(r"{RAW_STRING_RE_ALT}|[a-zA-Z_][a-zA-Z0-9_.]*"))
});
let items: Vec<&str> = ITEM_RE.find_iter(args_str).map(|m| m.as_str()).collect();
let array_literal = format!("[{}]", items.join(", "));
format!("{}map(pairs={})", pre, array_literal)
})
.to_string();
format!("{}{}{}", open, rewritten, close)
})
}
pub(super) fn preprocess_positional_syntax(template: &str) -> String {
replace_live_blocks(template, |block: &str| {
let (open, inner, close) = extract_block_parts(block);
if block.starts_with("{%") {
if let Some(rewritten) = try_rewrite_control_block(inner) {
return format!("{}{}{}", open, rewritten, close);
}
return block.to_string();
}
let tokens = tokenize_block(inner);
if tokens.is_empty() {
return block.to_string();
}
match rewrite_expr_tokens(&tokens) {
Some(rewritten) => format!("{}{}{}", open, rewritten, close),
None => block.to_string(),
}
})
}
pub(super) fn rewrite_expr_tokens(tokens: &[Token]) -> Option<String> {
rewrite_expr_tokens_at(tokens, 0)
}
fn rewrite_expr_tokens_at(tokens: &[Token], depth: usize) -> Option<String> {
let segments = pipe_segments(tokens);
match segments.as_slice() {
[only] => rewrite_head_segment(only, depth),
_ => rewrite_pipeline(&segments, depth),
}
}
fn pipe_segments(tokens: &[Token]) -> Vec<&[Token]> {
let mut segments = Vec::new();
let mut start = 0;
for (i, token) in tokens.iter().enumerate() {
if matches!(token, Token::Pipe) {
segments.push(&tokens[start..i]);
start = i + 1;
}
}
segments.push(&tokens[start..]);
segments
}
fn rewrite_head_segment(tokens: &[Token], depth: usize) -> Option<String> {
try_rewrite_slice(tokens, depth)
.or_else(|| try_rewrite_variadic(tokens, depth))
.or_else(|| try_rewrite_standalone(tokens, depth))
.or_else(|| rewrite_subexprs_only(tokens, depth))
}
fn rewrite_pipeline(segments: &[&[Token]], depth: usize) -> Option<String> {
let mut out = String::new();
let mut changed = false;
for (i, segment) in segments.iter().enumerate() {
if i > 0 {
out.push('|');
}
let rewritten = if i == 0 {
rewrite_head_segment(segment, depth)
} else {
try_rewrite_filter(segment, depth).or_else(|| rewrite_subexprs_only(segment, depth))
};
match rewritten {
Some(text) => {
changed = true;
out.push_str(&text);
}
None => out.extend(segment.iter().map(|t| token_to_str(t))),
}
}
changed.then_some(out)
}
pub(super) fn rewrite_subexprs_only(tokens: &[Token], depth: usize) -> Option<String> {
tokens
.iter()
.any(|t| matches!(t, Token::SubExpr(_)))
.then(|| render_tokens(tokens, depth))
}
fn render_tokens(tokens: &[Token], depth: usize) -> String {
tokens
.iter()
.map(|t| match t {
Token::SubExpr(text) => rewrite_subexpr(text, depth),
other => token_to_str(other).into_owned(),
})
.collect()
}
fn rewrite_subexpr(text: &str, depth: usize) -> String {
if depth + 1 > MAX_EXPR_NESTING {
return text.to_string();
}
let inner = &text[1..text.len() - 1];
match rewrite_expr_tokens_at(&tokenize_block(inner), depth + 1) {
Some(rewritten) => format!("({})", rewritten),
None => text.to_string(),
}
}
#[derive(Clone, Copy)]
struct PositionalSyntax {
name: &'static str,
standalone_params: &'static [&'static str],
piped_params: &'static [&'static str],
}
static POSITIONAL_FUNCTIONS: &[PositionalSyntax] = &[
PositionalSyntax {
name: "replace",
standalone_params: &["s", "old", "new"],
piped_params: &["from", "to"],
},
PositionalSyntax {
name: "split",
standalone_params: &["s", "sep"],
piped_params: &["sep"],
},
PositionalSyntax {
name: "contains",
standalone_params: &["s", "substr"],
piped_params: &["substr"],
},
PositionalSyntax {
name: "in",
standalone_params: &["items", "value"],
piped_params: &["value"],
},
PositionalSyntax {
name: "reReplaceAll",
standalone_params: &["pattern", "input", "replacement"],
piped_params: &["pattern", "replacement"],
},
PositionalSyntax {
name: "filter",
standalone_params: &["items", "regexp"],
piped_params: &["regexp"],
},
PositionalSyntax {
name: "reverseFilter",
standalone_params: &["items", "regexp"],
piped_params: &["regexp"],
},
PositionalSyntax {
name: "index",
standalone_params: &["collection", "key"],
piped_params: &["key"],
},
PositionalSyntax {
name: "trimprefix",
standalone_params: &["s", "prefix"],
piped_params: &["prefix"],
},
PositionalSyntax {
name: "trimsuffix",
standalone_params: &["s", "suffix"],
piped_params: &["suffix"],
},
PositionalSyntax {
name: "envOrDefault",
standalone_params: &["name", "default"],
piped_params: &[],
},
PositionalSyntax {
name: "indexOrDefault",
standalone_params: &["map", "key", "default"],
piped_params: &[],
},
];
const PARAM_S: &[&str] = &["s"];
const PARAM_V: &[&str] = &["v"];
const PARAM_NAME: &[&str] = &["name"];
const PARAM_PATH: &[&str] = &["path"];
const PARAM_ITEMS: &[&str] = &["items"];
const PARAM_FORMAT: &[&str] = &["format"];
static UNARY_FUNCTIONS: &[(&str, &[&str])] = &[
("abs", PARAM_S),
("base", PARAM_S),
("blake2b", PARAM_S),
("blake2s", PARAM_S),
("blake3", PARAM_S),
("crc32", PARAM_S),
("dir", PARAM_S),
("englishJoin", PARAM_ITEMS),
("incmajor", PARAM_V),
("incminor", PARAM_V),
("incpatch", PARAM_V),
("isEnvSet", PARAM_NAME),
("md5", PARAM_S),
("mdv2escape", PARAM_S),
("mustReadFile", PARAM_PATH),
("readFile", PARAM_PATH),
("sha1", PARAM_S),
("sha224", PARAM_S),
("sha256", PARAM_S),
("sha384", PARAM_S),
("sha512", PARAM_S),
("sha3_224", PARAM_S),
("sha3_256", PARAM_S),
("sha3_384", PARAM_S),
("sha3_512", PARAM_S),
("time", PARAM_FORMAT),
("title", PARAM_S),
("tolower", PARAM_S),
("toupper", PARAM_S),
("trim", PARAM_S),
("urlPathEscape", PARAM_S),
];
fn lookup_positional(name: &str) -> Option<PositionalSyntax> {
if let Some(spec) = POSITIONAL_FUNCTIONS.iter().find(|p| p.name == name) {
return Some(*spec);
}
UNARY_FUNCTIONS
.iter()
.find(|(unary_name, _)| *unary_name == name)
.map(|(name, standalone_params)| PositionalSyntax {
name,
standalone_params,
piped_params: &[],
})
}
#[cfg(test)]
pub(super) const PREPROCESSED_ELSEWHERE: &[&str] =
&["list", "map", "print", "printf", "println", "slice"];
#[cfg(test)]
pub(super) const NO_POSITIONAL_FORM: &[&str] =
&["contains_any", "date", "now_format", "ruby_escape"];
#[cfg(test)]
pub(super) fn positional_builtin_names() -> impl Iterator<Item = &'static str> {
positional_specs().map(|(name, _)| name)
}
#[cfg(test)]
pub(super) fn positional_specs() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
POSITIONAL_FUNCTIONS
.iter()
.map(|spec| (spec.name, spec.standalone_params))
.chain(UNARY_FUNCTIONS.iter().copied())
}
fn is_named_arg_call(tokens: &[&Token]) -> bool {
tokens
.iter()
.any(|t| matches!(t, Token::Other(s) if s == "("))
}
fn block_whitespace(tokens: &[Token]) -> (&str, &str) {
let leading = tokens
.first()
.and_then(|t| match t {
Token::Space(s) => Some(s.as_str()),
_ => None,
})
.unwrap_or("");
let trailing = tokens
.last()
.and_then(|t| match t {
Token::Space(s) => Some(s.as_str()),
_ => None,
})
.unwrap_or("");
(leading, trailing)
}
fn try_rewrite_slice(tokens: &[Token], depth: usize) -> Option<String> {
let sig = significant_tokens(tokens);
if is_named_arg_call(&sig) {
return None;
}
if !matches!(sig.first(), Some(Token::Ident(name)) if name == "slice") {
return None;
}
if sig.len() != 3 && sig.len() != 4 {
return None;
}
let item = format_arg_value(sig[1], depth)?;
let start = format_arg_value(sig[2], depth)?;
let params = if sig.len() == 4 {
let end = format_arg_value(sig[3], depth)?;
format!("start={}, end={}", start, end)
} else {
format!("start={}", start)
};
let (leading, trailing) = block_whitespace(tokens);
Some(format!(
"{}{} | slice({}){}",
leading, item, params, trailing
))
}
const VARIADIC_FUNCTIONS: &[(&str, &str)] = &[
("printf", "args"),
("print", "args"),
("println", "args"),
("list", "items"),
];
fn try_rewrite_variadic(tokens: &[Token], depth: usize) -> Option<String> {
let sig = significant_tokens(tokens);
if is_named_arg_call(&sig) {
return None;
}
let func_name = match sig.first() {
Some(Token::Ident(name)) => name.as_str(),
_ => return None,
};
let array_param = VARIADIC_FUNCTIONS
.iter()
.find(|(name, _)| *name == func_name)
.map(|(_, param)| *param)?;
if func_name == "list" && sig.len() < 2 {
return None;
}
let rest = &sig[1..];
let (format_part, value_tokens) = if func_name == "printf" {
let fmt = rest.first()?;
(Some(format_arg_value(fmt, depth)?), &rest[1..])
} else {
(None, rest)
};
let values: Vec<String> = value_tokens
.iter()
.map(|t| format_arg_value(t, depth))
.collect::<Option<Vec<_>>>()?;
let args_literal = format!("{}=[{}]", array_param, values.join(", "));
let params = match format_part {
Some(fmt) => format!("format={}, {}", fmt, args_literal),
None => args_literal,
};
let (leading, trailing) = block_whitespace(tokens);
Some(format!("{}{}({}){}", leading, func_name, params, trailing))
}
pub(super) fn try_rewrite_standalone(tokens: &[Token], depth: usize) -> Option<String> {
let sig = significant_tokens(tokens);
if is_named_arg_call(&sig) {
return None;
}
let func_name = match sig.first() {
Some(Token::Ident(name)) => name.as_str(),
_ => return None,
};
let spec = lookup_positional(func_name)?;
if sig.len() != spec.standalone_params.len() + 1 {
return None;
}
let args: Vec<String> = sig[1..]
.iter()
.map(|t| format_arg_value(t, depth))
.collect::<Option<Vec<_>>>()?;
let params_str: String = spec
.standalone_params
.iter()
.zip(args.iter())
.map(|(name, val)| format!("{}={}", name, val))
.collect::<Vec<_>>()
.join(", ");
let (leading_ws, trailing_ws) = block_whitespace(tokens);
Some(format!(
"{}{}({}){}",
leading_ws, func_name, params_str, trailing_ws
))
}
fn try_rewrite_filter(tokens: &[Token], depth: usize) -> Option<String> {
let sig = significant_tokens(tokens);
if is_named_arg_call(&sig) {
return None;
}
let func_name = match sig.first() {
Some(Token::Ident(name)) => name.as_str(),
_ => return None,
};
let spec = lookup_positional(func_name)?;
if spec.piped_params.is_empty() {
return None;
}
if sig.len() != spec.piped_params.len() + 1 {
return None;
}
let args: Vec<String> = sig[1..]
.iter()
.map(|t| format_arg_value(t, depth))
.collect::<Option<Vec<_>>>()?;
let params_str: String = spec
.piped_params
.iter()
.zip(args.iter())
.map(|(name, val)| format!("{}={}", name, val))
.collect::<Vec<_>>()
.join(", ");
let (leading_ws, trailing_ws) = block_whitespace(tokens);
Some(format!(
"{}{}({}){}",
leading_ws, func_name, params_str, trailing_ws
))
}
fn format_arg_value(token: &Token, depth: usize) -> Option<String> {
match token {
Token::Quoted(s) => Some(s.clone()),
Token::Ident(s) => Some(s.clone()),
Token::ArrayLiteral(s) => Some(s.clone()),
Token::SubExpr(s) => Some(rewrite_subexpr(s, depth)),
_ => None,
}
}