use super::environment_models::expr_desc;
use crate::core::{AssignOp, CallTarget, Element, ElementKind, Expr, MacroDef};
use crate::error::{ErrorCtx, TemplateError};
use crate::span::Span;
pub(crate) fn element_shown_in_stack_trace(el: &Element) -> bool {
matches!(
el.kind,
ElementKind::Interpolation { .. }
| ElementKind::Call { .. }
| ElementKind::Include { .. }
| ElementKind::Import { .. }
| ElementKind::Nested { .. }
| ElementKind::Transform { .. }
| ElementKind::Visit { .. }
| ElementKind::Recurse { .. }
| ElementKind::Fallback
)
}
pub(crate) fn describe_element(env: &super::Environment, el: &Element) -> Option<String> {
use ElementKind as E;
let d = match &el.kind {
E::Text { .. }
| E::NoParse { .. }
| E::Comment { .. }
| E::FtlHeader { .. }
| E::TrimLineStart
| E::NoTrimLineStart
| E::TrimLineEnd
| E::LeftTrimLine
| E::RawText(_) => return None,
E::Interpolation {
expr,
legacy_min_frac,
..
} => {
let s = if legacy_min_frac.is_some() {
format!("#{{{}}}", expr_desc(expr))
} else {
format!("${{{}}}", expr_desc(expr))
};
if legacy_min_frac.is_none() && (!env.escapes.is_empty() || env.auto_escape) {
format!("{s} auto-escaped")
} else {
s
}
}
E::If { cond, .. } => format!("#if {}", expr_desc(cond)),
E::List { seq, var, var2, .. } => {
let mut s = format!("#list {}", expr_desc(seq));
if !var.is_empty() {
s.push_str(&format!(" as {var}"));
if let Some(v2) = var2 {
s.push_str(&format!(", {v2}"));
}
}
s
}
E::Items { var, var2, .. } => {
let mut s = format!("#items as {var}");
if let Some(v2) = var2 {
s.push_str(&format!(", {v2}"));
}
s
}
E::Assign {
target, expr, op, ..
} => format!(
"#assign {target} {} {}",
assign_op_symbol(*op),
expr_desc(expr)
),
E::Global {
target,
expr: Some(e),
..
} => format!("#global {target} = {}", expr_desc(e)),
E::Local {
target,
expr: Some(e),
..
} => format!("#local {target} = {}", expr_desc(e)),
E::Macro { def } => macro_def_description(def),
E::Call {
callee,
args,
body_params,
..
} => {
let mut s = String::from("@");
match callee {
CallTarget::Name(n) => s.push_str(n),
CallTarget::Namespaced { ns, name } => {
s.push_str(ns);
s.push('.');
s.push_str(name);
}
CallTarget::Expr(e) => s.push_str(&expr_desc(e)),
}
let mut parts: Vec<String> = Vec::new();
for (k, e) in args {
if k.is_empty() {
parts.push(expr_desc(e));
} else {
parts.push(format!("{k}={}", expr_desc(e)));
}
}
if !parts.is_empty() {
s.push(' ');
s.push_str(&parts.join(", "));
}
if !body_params.is_empty() {
s.push_str("; ");
s.push_str(&body_params.join(", "));
}
s
}
E::Nested { .. } => "#nested".to_string(),
E::Switch { expr, .. } => format!("#switch {}", expr_desc(expr)),
E::Break => "#break".to_string(),
E::Continue => "#continue".to_string(),
E::Return { expr } => match expr {
Some(e) => format!("#return {}", expr_desc(e)),
None => "#return".to_string(),
},
E::Stop { msg } => match msg {
Some(e) => format!("#stop {}", expr_desc(e)),
None => "#stop".to_string(),
},
E::Flush => "#flush".to_string(),
E::Include { path, .. } => format!("#include {}", expr_desc(path)),
E::Import { path, ns } => format!("#import {} as {ns}", expr_desc(path)),
E::Setting { key, value } => format!("#setting {key}={}", expr_desc(value)),
E::Transform { expr, .. } => format!("#transform {}", expr_desc(expr)),
E::Visit { expr, .. } => match expr {
Some(e) => format!("#visit {}", expr_desc(e)),
None => "#visit".to_string(),
},
E::Recurse { expr, .. } => match expr {
Some(e) => format!("#recurse {}", expr_desc(e)),
None => "#recurse".to_string(),
},
E::On { expr, .. } => format!("#on {}", expr_desc(expr)),
E::Fallback => "#fallback".to_string(),
_ => return None,
};
Some(shorten_java(&d, 40))
}
fn assign_op_symbol(op: AssignOp) -> &'static str {
match op {
AssignOp::Equals => "=",
AssignOp::PlusEq => "+=",
AssignOp::MinusEq => "-=",
AssignOp::TimesEq => "*=",
AssignOp::DivideEq => "/=",
AssignOp::ModuloEq => "%=",
AssignOp::PlusPlus => "++",
AssignOp::MinusMinus => "--",
}
}
pub(crate) fn macro_def_description(def: &MacroDef) -> String {
let mut s = format!(
"{} {name}",
if def.is_function {
"#function"
} else {
"#macro"
},
name = def.name
);
for p in &def.params {
if p.catch_all {
s.push_str(&format!(
" {}{}",
p.name,
if p.default.is_some() { "..." } else { "" }
));
continue;
}
match &p.default {
Some(e) => s.push_str(&format!(" {}={}", p.name, expr_desc(e))),
None => s.push_str(&format!(" {}", p.name)),
}
}
s
}
fn shorten_java(s: &str, max_len: usize) -> String {
let count = s.chars().count();
if count <= max_len {
return s.to_string();
}
let cut = max_len.saturating_sub(3);
let head: String = s.chars().take(cut).collect();
format!("{head}...")
}
pub(crate) fn collect_ident_names(e: &Expr) -> Vec<String> {
let mut out = Vec::new();
collect_ident_names_into(e, &mut out);
out
}
fn collect_ident_names_into(e: &Expr, out: &mut Vec<String>) {
use crate::core::ExprKind as K;
match &e.kind {
K::Ident(n) => out.push(n.clone()),
K::InterpStr(parts) => {
for p in parts {
if let crate::core::StrPart::Interp(inner) = p {
collect_ident_names_into(inner, out);
}
}
}
K::Dot { target, .. }
| K::UnaryMinus(target)
| K::Not(target)
| K::Exists(target)
| K::Paren(target) => collect_ident_names_into(target, out),
K::DynKey { target, key } => {
collect_ident_names_into(target, out);
collect_ident_names_into(key, out);
}
K::Default { target, default } => {
collect_ident_names_into(target, out);
if let Some(d) = default {
collect_ident_names_into(d, out);
}
}
K::Add(a, b)
| K::Sub(a, b)
| K::Mul(a, b)
| K::Div(a, b)
| K::Mod(a, b)
| K::Eq(a, b)
| K::NotEq(a, b)
| K::Gt(a, b)
| K::Gte(a, b)
| K::Lt(a, b)
| K::Lte(a, b)
| K::And(a, b)
| K::Or(a, b) => {
collect_ident_names_into(a, out);
collect_ident_names_into(b, out);
}
K::Range { start, end, .. } => {
collect_ident_names_into(start, out);
if let Some(end) = end {
collect_ident_names_into(end, out);
}
}
K::BuiltIn { target, args, .. } => {
collect_ident_names_into(target, out);
if let Some(args) = args {
for a in args {
collect_ident_names_into(a, out);
}
}
}
K::Call { callee, args } => {
collect_ident_names_into(callee, out);
for a in args {
collect_ident_names_into(a, out);
}
}
K::ListLit(items) => {
for i in items {
collect_ident_names_into(i, out);
}
}
K::HashLit(pairs) => {
for (k, v) in pairs {
collect_ident_names_into(k, out);
collect_ident_names_into(v, out);
}
}
K::Lambda { body, .. } => collect_ident_names_into(body, out),
_ => {}
}
}
pub(crate) fn attach_location(
err: TemplateError,
template_name: &str,
span: Span,
) -> TemplateError {
if err.has_location() {
return err;
}
match err {
TemplateError::InvalidReference { name, ctx } => {
if name.contains("[in template") {
TemplateError::InvalidReference { name, ctx }
} else {
TemplateError::InvalidReference {
name,
ctx: Box::new(ErrorCtx {
span,
template_name: Some(template_name.to_string()),
..*ctx
}),
}
}
}
TemplateError::TypeMismatch {
expected,
actual,
ctx,
} => {
if actual.contains("[in template") || ctx.assignment_target.is_some() {
TemplateError::TypeMismatch {
expected,
actual,
ctx,
}
} else {
TemplateError::TypeMismatch {
expected,
actual,
ctx: Box::new(ErrorCtx {
span,
template_name: Some(template_name.to_string()),
..*ctx
}),
}
}
}
other => other,
}
}