use super::GO_BLOCK_RE;
use super::go_blocks::{extract_block_parts, try_rewrite_control_block};
use super::static_regex;
use super::tokens::{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 = r#"(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[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 {
GO_BLOCK_RE
.replace_all(template, |caps: ®ex::Captures| {
let block = &caps[0];
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(
r#""(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[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)
})
.to_string()
}
pub(super) fn preprocess_positional_syntax(template: &str) -> String {
GO_BLOCK_RE
.replace_all(template, |caps: ®ex::Captures| {
let block = &caps[0];
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();
}
if let Some(rewritten) = try_rewrite_standalone(&tokens) {
return format!("{}{}{}", open, rewritten, close);
}
if let Some(rewritten) = try_rewrite_piped(&tokens) {
return format!("{}{}{}", open, rewritten, close);
}
block.to_string()
})
.to_string()
}
struct PositionalSyntax {
name: &'static str,
arity: usize,
standalone_params: &'static [&'static str],
piped_params: &'static [&'static str],
}
static POSITIONAL_FUNCTIONS: &[PositionalSyntax] = &[
PositionalSyntax {
name: "replace",
arity: 3,
standalone_params: &["s", "old", "new"],
piped_params: &["from", "to"],
},
PositionalSyntax {
name: "split",
arity: 2,
standalone_params: &["s", "sep"],
piped_params: &["sep"],
},
PositionalSyntax {
name: "contains",
arity: 2,
standalone_params: &["s", "substr"],
piped_params: &["substr"],
},
PositionalSyntax {
name: "in",
arity: 2,
standalone_params: &["items", "value"],
piped_params: &["value"],
},
PositionalSyntax {
name: "reReplaceAll",
arity: 3,
standalone_params: &["pattern", "input", "replacement"],
piped_params: &["pattern", "replacement"],
},
PositionalSyntax {
name: "filter",
arity: 2,
standalone_params: &["items", "regexp"],
piped_params: &["regexp"],
},
PositionalSyntax {
name: "reverseFilter",
arity: 2,
standalone_params: &["items", "regexp"],
piped_params: &["regexp"],
},
PositionalSyntax {
name: "readFile",
arity: 1,
standalone_params: &["path"],
piped_params: &[],
},
PositionalSyntax {
name: "mustReadFile",
arity: 1,
standalone_params: &["path"],
piped_params: &[],
},
PositionalSyntax {
name: "index",
arity: 2,
standalone_params: &["collection", "key"],
piped_params: &["key"],
},
];
fn lookup_positional(name: &str) -> Option<&'static PositionalSyntax> {
POSITIONAL_FUNCTIONS.iter().find(|p| p.name == name)
}
pub(super) fn try_rewrite_standalone(tokens: &[Token]) -> Option<String> {
let sig = significant_tokens(tokens);
if sig.iter().any(|t| matches!(t, Token::Other(s) if s == "(")) {
return None;
}
if sig.iter().any(|t| matches!(t, Token::Pipe)) {
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.arity + 1 {
return None;
}
let args: Vec<String> = sig[1..]
.iter()
.map(|t| format_arg_value(t))
.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 = tokens
.first()
.and_then(|t| match t {
Token::Space(s) => Some(s.as_str()),
_ => None,
})
.unwrap_or("");
let trailing_ws = tokens
.last()
.and_then(|t| match t {
Token::Space(s) => Some(s.as_str()),
_ => None,
})
.unwrap_or("");
Some(format!(
"{}{}({}){}",
leading_ws, func_name, params_str, trailing_ws
))
}
pub(super) fn try_rewrite_piped(tokens: &[Token]) -> Option<String> {
let last_pipe_idx = tokens.iter().rposition(|t| matches!(t, Token::Pipe))?;
let before_pipe = &tokens[..last_pipe_idx];
let after_pipe = &tokens[last_pipe_idx + 1..];
if after_pipe
.iter()
.any(|t| matches!(t, Token::Other(s) if s == "("))
{
return None;
}
let sig_after = significant_tokens(after_pipe);
if sig_after.is_empty() {
return None;
}
let func_name = match sig_after.first() {
Some(Token::Ident(name)) => name.as_str(),
_ => return None,
};
let spec = lookup_positional(func_name)?;
let piped_arity = spec.arity - 1;
if sig_after.len() != piped_arity + 1 {
return None;
}
let args: Vec<String> = sig_after[1..]
.iter()
.map(|t| format_arg_value(t))
.collect::<Option<Vec<_>>>()?;
let params_str: String = spec
.piped_params
.iter()
.zip(args.iter())
.map(|(name, val)| format!("{}={}", name, val))
.collect::<Vec<_>>()
.join(", ");
let before_str: String = before_pipe.iter().map(|t| token_to_str(t)).collect();
let trailing_ws = tokens
.last()
.and_then(|t| match t {
Token::Space(s) => Some(s.as_str()),
_ => None,
})
.unwrap_or("");
Some(format!(
"{} | {}({}){}",
before_str.trim_end(),
func_name,
params_str,
trailing_ws
))
}
fn format_arg_value(token: &Token) -> Option<String> {
match token {
Token::Quoted(s) => Some(s.clone()),
Token::Ident(s) => Some(s.clone()),
Token::ArrayLiteral(s) => Some(s.clone()),
_ => None,
}
}