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_slice(&tokens) {
return format!("{}{}{}", open, rewritten, close);
}
if let Some(rewritten) = try_rewrite_printf_like(&tokens) {
return format!("{}{}{}", open, rewritten, close);
}
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"],
},
PositionalSyntax {
name: "time",
arity: 1,
standalone_params: &["format"],
piped_params: &[],
},
];
fn lookup_positional(name: &str) -> Option<&'static PositionalSyntax> {
POSITIONAL_FUNCTIONS.iter().find(|p| p.name == name)
}
fn already_named_or_piped(sig: &[&Token]) -> bool {
sig.iter()
.any(|t| matches!(t, Token::Other(s) if s == "(") || matches!(t, Token::Pipe))
}
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]) -> Option<String> {
let sig = significant_tokens(tokens);
if already_named_or_piped(&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])?;
let start = format_arg_value(sig[2])?;
let params = if sig.len() == 4 {
let end = format_arg_value(sig[3])?;
format!("start={}, end={}", start, end)
} else {
format!("start={}", start)
};
let (leading, trailing) = block_whitespace(tokens);
Some(format!(
"{}{} | slice({}){}",
leading, item, params, trailing
))
}
fn try_rewrite_printf_like(tokens: &[Token]) -> Option<String> {
let sig = significant_tokens(tokens);
if already_named_or_piped(&sig) {
return None;
}
let func_name = match sig.first() {
Some(Token::Ident(name)) => name.as_str(),
_ => return None,
};
if !matches!(func_name, "printf" | "print" | "println") {
return None;
}
let rest = &sig[1..];
let (format_part, value_tokens) = if func_name == "printf" {
let fmt = rest.first()?;
(Some(format_arg_value(fmt)?), &rest[1..])
} else {
(None, rest)
};
let values: Vec<String> = value_tokens
.iter()
.map(|t| format_arg_value(t))
.collect::<Option<Vec<_>>>()?;
let args_literal = format!("args=[{}]", 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]) -> Option<String> {
let sig = significant_tokens(tokens);
if already_named_or_piped(&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.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,
}
}