use anyhow::{Result, bail};
use regex::Regex;
mod blocks;
mod builtins;
mod dots_dollars;
mod go_blocks;
mod methods;
mod positional;
mod shell_guard;
pub(crate) mod string_lit;
mod tokens;
#[cfg(test)]
mod tests;
use blocks::live_blocks;
pub(crate) use blocks::{raw_span_at, raw_spans};
use builtins::{preprocess_go_builtins, preprocess_list_subexpr};
use dots_dollars::{preprocess_strip_dots, rewrite_numeric_index_segments};
use go_blocks::{extract_block_parts, preprocess_go_blocks};
use methods::preprocess_method_calls;
use positional::{preprocess_map_syntax, preprocess_positional_syntax};
pub(crate) use shell_guard::{protect_shell_param_length, restore_shell_param_length};
use tokens::{MAX_EXPR_NESTING, ParenImbalance, scan_parens};
fn static_regex(pattern: &str) -> Regex {
Regex::new(pattern)
.unwrap_or_else(|e| panic!("invalid static regex literal `{}`: {}", pattern, e))
}
const MAX_BLOCK_SNIPPET: usize = 160;
pub(crate) fn check_block_expressions(template: &str) -> Result<()> {
for block in live_blocks(template) {
let (_, inner, _) = extract_block_parts(block);
let scan = scan_parens(inner);
if let Some(imbalance) = scan.imbalance {
const PAREN_HINT: &str = "A sub-expression argument must open and close \
inside the same block — for example `trimprefix (base Path) \"v\"`.";
let detail = match imbalance {
ParenImbalance::Unclosed(1) => format!("1 unclosed `(`. {PAREN_HINT}"),
ParenImbalance::Unclosed(n) => format!("{n} unclosed `(`. {PAREN_HINT}"),
ParenImbalance::UnmatchedClose => {
format!("a `)` with no matching `(`. {PAREN_HINT}")
}
ParenImbalance::UnterminatedString => "an unterminated string literal, so the \
rest of the block — including any `)` — reads as string contents. A literal \
closes at the next matching quote; a backslash does not escape it."
.to_string(),
};
bail!(
"unbalanced expression in template {}: {detail}",
quote_block(block)
);
}
if scan.max_depth > MAX_EXPR_NESTING {
bail!(
"over-nested expression in template {}: parentheses nest {} deep, past the \
limit of {MAX_EXPR_NESTING}. Every group is rewritten by descending into it, \
so a deeper nest exhausts memory or the stack before the engine sees the \
block. Bind an intermediate result to a variable and nest fewer calls.",
quote_block(block),
scan.max_depth
);
}
}
Ok(())
}
fn quote_block(block: &str) -> String {
if block.len() <= MAX_BLOCK_SNIPPET {
return format!("`{block}`");
}
let mut end = MAX_BLOCK_SNIPPET;
while end > 0 && !block.is_char_boundary(end) {
end -= 1;
}
format!("`{}…`", &block[..end])
}
pub fn preprocess(template: &str) -> String {
let block_converted = preprocess_go_blocks(template);
let dot_stripped = preprocess_strip_dots(&block_converted);
let list_rewritten = preprocess_list_subexpr(&dot_stripped);
let comparison_rewritten = preprocess_go_builtins(&list_rewritten);
let map_rewritten = preprocess_map_syntax(&comparison_rewritten);
let positional_rewritten = preprocess_positional_syntax(&map_rewritten);
let method_rewritten = preprocess_method_calls(&positional_rewritten);
rewrite_numeric_index_segments(&method_rewritten)
}