use alloc::{
string::{String, ToString},
sync::Arc,
vec::Vec,
};
#[cfg(feature = "std")]
use std::path::{Path, PathBuf};
use crate::{
compat::{HashMap, HashSet},
compiled::{self, CompiledInlineTemplate, Segment},
context::Context,
error::TemplateError,
frontmatter::{self, Frontmatter},
scope::Scope,
types::{VarDecl, VarType},
value::Value,
};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default)]
pub struct CompileOptions<'a> {
pub allow_unused: bool,
#[cfg(feature = "std")]
pub base_dir: Option<&'a std::path::Path>,
#[cfg(not(feature = "std"))]
_phantom: core::marker::PhantomData<&'a ()>,
}
#[cfg(feature = "std")]
impl<'a> CompileOptions<'a> {
#[must_use]
pub fn base_dir(mut self, dir: &'a std::path::Path) -> Self {
self.base_dir = Some(dir);
self
}
}
impl CompileOptions<'_> {
#[must_use]
pub fn allow_unused(mut self, allow: bool) -> Self {
self.allow_unused = allow;
self
}
}
#[derive(Debug, Clone)]
pub struct Template {
body: String,
name: Option<String>,
description: Option<String>,
segments: Arc<[Segment]>,
declared_variables: Arc<[VarDecl]>,
#[cfg(feature = "std")]
base_dir: Option<PathBuf>,
inline_templates: Arc<HashMap<String, CompiledInlineTemplate>>,
source_hash: u64,
max_include_depth: usize,
has_defaults: bool,
consts: Arc<HashMap<String, crate::value::Value>>,
imported_consts: Arc<HashMap<String, crate::value::Value>>,
estimated_capacity: usize,
}
#[cfg(feature = "std")]
pub(crate) struct CachedTemplateData {
pub segments: Arc<[Segment]>,
pub declared_variables: Arc<[VarDecl]>,
pub base_dir: Option<PathBuf>,
pub inline_templates: Arc<HashMap<String, CompiledInlineTemplate>>,
pub source_hash: u64,
pub consts: Arc<HashMap<String, crate::value::Value>>,
pub imported_consts: Arc<HashMap<String, crate::value::Value>>,
pub name: Option<String>,
pub description: Option<String>,
}
#[doc(hidden)]
pub struct PrecompiledTemplateData<'a> {
pub segments: &'a [Segment],
pub declared_variables: &'a [VarDecl],
pub inline_templates: &'a [(&'a str, CompiledInlineTemplate)],
pub source_hash: u64,
pub consts: &'a [(&'a str, crate::value::Value)],
pub imported_consts: &'a [(&'a str, crate::value::Value)],
pub name: Option<&'a str>,
pub description: Option<&'a str>,
}
impl Template {
#[cfg(feature = "std")]
pub fn from_file(path: &Path) -> Result<Self, TemplateError> {
let source = std::fs::read_to_string(path)?;
let (tmpl, _fm) =
Self::compile_from_source(&source, Some(path.parent().unwrap_or(Path::new("."))))?;
Ok(tmpl)
}
pub fn from_source(source: &str) -> Result<Self, TemplateError> {
#[cfg(feature = "std")]
let (tmpl, _fm) = Self::compile_from_source(source, None)?;
#[cfg(not(feature = "std"))]
let (tmpl, _fm) = Self::compile_from_source_no_std(source)?;
Ok(tmpl)
}
#[deprecated(
since = "0.2.0",
note = "Use `Template::compile(source, CompileOptions::default().allow_unused(true))` instead"
)]
pub fn from_source_allowing_unused(source: &str) -> Result<Self, TemplateError> {
let (tmpl, _fm) = Self::compile(source, CompileOptions::default().allow_unused(true))?;
Ok(tmpl)
}
#[cfg(feature = "std")]
#[deprecated(
since = "0.2.0",
note = "Use `Template::compile(source, CompileOptions::default().base_dir(dir))` instead"
)]
pub fn from_source_with_base_dir(source: &str, base_dir: &Path) -> Result<Self, TemplateError> {
let (tmpl, _fm) = Self::compile(source, CompileOptions::default().base_dir(base_dir))?;
Ok(tmpl)
}
#[deprecated(
since = "0.2.0",
note = "Use `Template::compile(source, CompileOptions::default())` which always returns Frontmatter"
)]
pub fn from_source_with_frontmatter(
source: &str,
) -> Result<(Self, Frontmatter), TemplateError> {
Self::compile(source, CompileOptions::default())
}
#[cfg(feature = "std")]
#[deprecated(
since = "0.2.0",
note = "Use `Template::compile_file(path, CompileOptions::default())` which always returns Frontmatter"
)]
pub fn from_file_with_frontmatter(path: &Path) -> Result<(Self, Frontmatter), TemplateError> {
Self::compile_file(path, CompileOptions::default())
}
pub fn compile(
source: &str,
options: CompileOptions<'_>,
) -> Result<(Self, Frontmatter), TemplateError> {
#[cfg(feature = "std")]
return Self::compile_inner(source, options.base_dir, options.allow_unused);
#[cfg(not(feature = "std"))]
return Self::compile_inner_no_std(source, options.allow_unused);
}
#[cfg(feature = "std")]
pub fn compile_file(
path: &Path,
options: CompileOptions<'_>,
) -> Result<(Self, Frontmatter), TemplateError> {
let source = std::fs::read_to_string(path)?;
let base_dir = options.base_dir.or_else(|| path.parent());
Self::compile_inner(&source, base_dir, options.allow_unused)
}
#[cfg(feature = "std")]
fn compile_from_source(
source: &str,
base_dir: Option<&Path>,
) -> Result<(Self, Frontmatter), TemplateError> {
Self::compile_inner(source, base_dir, false)
}
#[cfg(feature = "std")]
fn compile_inner(
source: &str,
base_dir: Option<&Path>,
force_allow_unused: bool,
) -> Result<(Self, Frontmatter), TemplateError> {
let source_hash = crate::cache::hash_source(source);
let (fm, body) = if let Some(dir) = base_dir {
frontmatter::parse_frontmatter_with_base_dir(source, dir)?
} else {
frontmatter::parse_frontmatter(source)?
};
let body = body.to_string();
let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
let referenced = compiled::collect_referenced_params(&segments);
check_undeclared_variables(&referenced, &fm, &inline_templates)?;
check_unused_params(
&fm.declarations,
&referenced,
force_allow_unused || fm.allow_unused,
)?;
check_name_collisions(&fm, &inline_templates, &segments)?;
let enum_keys = collect_enum_type_keys(&fm);
check_bare_enum_access(&segments, &enum_keys)?;
let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
let mut consts: HashMap<String, Value> = fm
.consts
.iter()
.filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
.collect();
inject_enum_type_constants(&fm.type_aliases, &mut consts);
let segments: Arc<[Segment]> = Arc::from(segments);
let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
let tmpl = Self {
body,
name: fm.name.clone(),
description: fm.description.clone(),
segments,
declared_variables: Arc::from(fm.declarations.clone()),
base_dir: base_dir.map(Path::to_path_buf),
inline_templates: Arc::new(inline_templates),
source_hash,
max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
has_defaults,
consts: Arc::new(consts),
imported_consts: Arc::new(fm.imported_consts.clone()),
estimated_capacity,
};
Ok((tmpl, fm))
}
#[cfg(not(feature = "std"))]
fn compile_from_source_no_std(source: &str) -> Result<(Self, Frontmatter), TemplateError> {
Self::compile_inner_no_std(source, false)
}
#[cfg(not(feature = "std"))]
fn compile_inner_no_std(
source: &str,
force_allow_unused: bool,
) -> Result<(Self, Frontmatter), TemplateError> {
let source_hash = hash_source_no_std(source);
let (fm, body) = frontmatter::parse_frontmatter(source)?;
let body = body.to_string();
let (segments, inline_templates) = compiled::compile(&body, &fm.type_aliases)?;
let referenced = compiled::collect_referenced_params(&segments);
check_undeclared_variables(&referenced, &fm, &inline_templates)?;
check_unused_params(
&fm.declarations,
&referenced,
force_allow_unused || fm.allow_unused,
)?;
check_name_collisions(&fm, &inline_templates, &segments)?;
let enum_keys = collect_enum_type_keys(&fm);
check_bare_enum_access(&segments, &enum_keys)?;
let has_defaults = fm.declarations.iter().any(|d| d.default_value.is_some());
let mut consts: HashMap<String, Value> = fm
.consts
.iter()
.filter_map(|d| d.default_value.clone().map(|v| (d.name.clone(), v)))
.collect();
inject_enum_type_constants(&fm.type_aliases, &mut consts);
let segments: Arc<[Segment]> = Arc::from(segments);
let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
let tmpl = Self {
body,
name: fm.name.clone(),
description: fm.description.clone(),
segments,
declared_variables: Arc::from(fm.declarations.clone()),
inline_templates: Arc::new(inline_templates),
source_hash,
max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
has_defaults,
consts: Arc::new(consts),
imported_consts: Arc::new(fm.imported_consts.clone()),
estimated_capacity,
};
Ok((tmpl, fm))
}
#[cfg(feature = "std")]
pub(crate) fn from_cached(data: CachedTemplateData) -> Self {
let has_defaults = data
.declared_variables
.iter()
.any(|d| d.default_value.is_some());
let estimated_capacity = compiled::render::estimate_output_capacity(&data.segments);
Self {
body: String::new(),
name: data.name,
description: data.description,
segments: data.segments,
declared_variables: data.declared_variables,
base_dir: data.base_dir,
inline_templates: data.inline_templates,
source_hash: data.source_hash,
max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
has_defaults,
consts: data.consts,
imported_consts: data.imported_consts,
estimated_capacity,
}
}
#[doc(hidden)]
#[must_use]
pub fn from_precompiled(data: &PrecompiledTemplateData<'_>) -> Self {
let inline_map = data
.inline_templates
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
let const_map = data
.consts
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
let imported_const_map = data
.imported_consts
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
let has_defaults = data
.declared_variables
.iter()
.any(|d| d.default_value.is_some());
let segments: Arc<[Segment]> = Arc::from(data.segments);
let estimated_capacity = compiled::render::estimate_output_capacity(&segments);
Self {
body: String::new(),
name: data.name.map(String::from),
description: data.description.map(String::from),
segments,
declared_variables: Arc::from(data.declared_variables),
#[cfg(feature = "std")]
base_dir: None,
inline_templates: Arc::new(inline_map),
source_hash: data.source_hash,
max_include_depth: crate::scope::MAX_INCLUDE_DEPTH,
has_defaults,
consts: Arc::new(const_map),
imported_consts: Arc::new(imported_const_map),
estimated_capacity,
}
}
fn validate_context(&self, ctx: &Context, allow_extra: bool) -> Result<(), TemplateError> {
let mut missing = Vec::new();
let mut mismatch: Option<(String, crate::types::TypeCheckError)> = None;
for decl in self.declared_variables.iter() {
match ctx.get(&decl.name) {
None => {
if decl.default_value.is_none() {
missing.push(decl.name.as_str());
}
}
Some(value) => {
if mismatch.is_none()
&& let Err(e) = decl.var_type.check(value)
{
mismatch = Some((decl.name.clone(), e));
}
}
}
}
if !missing.is_empty() {
return Err(TemplateError::MissingParams(
missing.into_iter().map(String::from).collect(),
));
}
if let Some((name, check_err)) = mismatch {
let detail = if check_err.path.is_empty() {
String::new()
} else {
format!(" (at .{})", check_err.path)
};
return Err(TemplateError::TypeMismatch {
name: format!("{name}{detail}"),
expected: check_err.expected,
actual: check_err.actual,
actual_value: check_err.actual_value,
});
}
if !allow_extra {
let mut declared: HashSet<&str> = self
.declared_variables
.iter()
.map(|d| d.name.as_str())
.collect();
for name in self.consts.keys() {
declared.insert(name.as_str());
}
let extra: Vec<String> = ctx
.values
.keys()
.filter(|k| !declared.contains(k.as_str()))
.cloned()
.collect();
if !extra.is_empty() {
return Err(TemplateError::ExtraParams(extra));
}
}
Ok(())
}
#[must_use]
pub fn defaults(&self) -> HashMap<String, crate::value::Value> {
self.declared_variables
.iter()
.filter_map(|d| {
d.default_value
.as_ref()
.map(|v| (d.name.clone(), v.clone()))
})
.collect()
}
#[must_use]
pub fn default(&self, name: &str) -> Option<&crate::value::Value> {
self.declared_variables
.iter()
.find(|d| d.name == name)
.and_then(|d| d.default_value.as_ref())
}
#[must_use]
pub fn defaults_context(&self) -> Context {
let defaults = self.defaults();
let mut ctx = Context::with_capacity(defaults.len());
for (k, v) in defaults {
ctx.set(k, v);
}
ctx
}
#[must_use]
pub fn body(&self) -> &str {
&self.body
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[must_use]
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn set_max_include_depth(&mut self, depth: usize) {
self.max_include_depth = depth;
}
#[must_use]
pub fn with_max_include_depth(mut self, depth: usize) -> Self {
self.max_include_depth = depth;
self
}
#[must_use]
pub fn declarations(&self) -> &[VarDecl] {
&self.declared_variables
}
pub(crate) fn segments(&self) -> &[crate::compiled::Segment] {
&self.segments
}
#[cfg(feature = "std")]
#[must_use]
pub fn base_dir(&self) -> Option<&Path> {
self.base_dir.as_deref()
}
#[must_use]
pub fn consts(&self) -> Arc<HashMap<String, Value>> {
self.consts.clone()
}
#[must_use]
pub fn consts_ref(&self) -> &HashMap<String, Value> {
&self.consts
}
#[must_use]
pub fn imported_consts(&self) -> Arc<HashMap<String, Value>> {
self.imported_consts.clone()
}
#[must_use]
pub fn imported_consts_ref(&self) -> &HashMap<String, Value> {
&self.imported_consts
}
pub(crate) fn inline_templates(&self) -> &HashMap<String, CompiledInlineTemplate> {
&self.inline_templates
}
#[must_use]
pub fn source_hash(&self) -> u64 {
self.source_hash
}
pub fn validate_declarations(&self, expected: &[VarDecl]) -> Result<(), TemplateError> {
let current: HashMap<&str, &crate::types::VarType> = self
.declared_variables
.iter()
.map(|d| (d.name.as_str(), &d.var_type))
.collect();
let expected_map: HashMap<&str, &crate::types::VarType> = expected
.iter()
.map(|d| (d.name.as_str(), &d.var_type))
.collect();
let current_names: HashSet<&str> = current.keys().copied().collect();
let expected_names: HashSet<&str> = expected_map.keys().copied().collect();
let missing: Vec<&str> = expected_names.difference(¤t_names).copied().collect();
let extra: Vec<&str> = current_names.difference(&expected_names).copied().collect();
let retyped: Vec<String> = current_names
.intersection(&expected_names)
.filter_map(|name| {
let cur_type = current[name];
let exp_type = expected_map[name];
if cur_type == exp_type {
None
} else {
Some(format!("{name}: {exp_type} → {cur_type}"))
}
})
.collect();
if missing.is_empty() && extra.is_empty() && retyped.is_empty() {
return Ok(());
}
let mut parts = Vec::new();
if !missing.is_empty() {
parts.push(format!("removed: {}", missing.join(", ")));
}
if !extra.is_empty() {
parts.push(format!("added: {}", extra.join(", ")));
}
if !retyped.is_empty() {
parts.push(format!("retyped: {}", retyped.join(", ")));
}
Err(TemplateError::DeclarationsMutated {
details: parts.join("; "),
})
}
pub fn render_ctx(&self, ctx: &Context) -> Result<String, TemplateError> {
self.render_inner(ctx, false)
}
pub fn render_ctx_allowing_extra(&self, ctx: &Context) -> Result<String, TemplateError> {
self.render_inner(ctx, true)
}
pub fn render_empty(&self) -> Result<String, TemplateError> {
let ctx = if self.has_defaults {
self.defaults_context()
} else {
Context::new()
};
self.render_ctx(&ctx)
}
pub fn render_empty_into(&self, output: &mut String) -> Result<(), TemplateError> {
let ctx = if self.has_defaults {
self.defaults_context()
} else {
Context::new()
};
self.render_ctx_into(&ctx, output)
}
fn render_inner(&self, ctx: &Context, allow_extra: bool) -> Result<String, TemplateError> {
let mut output = String::with_capacity(self.estimated_capacity);
self.render_into_inner(ctx, allow_extra, &mut output)?;
Ok(output)
}
pub fn render_ctx_into(&self, ctx: &Context, output: &mut String) -> Result<(), TemplateError> {
self.render_into_inner(ctx, false, output)
}
pub fn render_ctx_into_allowing_extra(
&self,
ctx: &Context,
output: &mut String,
) -> Result<(), TemplateError> {
self.render_into_inner(ctx, true, output)
}
fn render_into_inner(
&self,
ctx: &Context,
allow_extra: bool,
output: &mut String,
) -> Result<(), TemplateError> {
self.validate_context(ctx, allow_extra)?;
self.render_core(ctx, output)
}
fn render_core(&self, ctx: &Context, output: &mut String) -> Result<(), TemplateError> {
let ctx = self.inject_defaults(ctx);
let mut scope = Scope::new(&ctx).with_max_include_depth(self.max_include_depth);
if !self.consts.is_empty() || !self.imported_consts.is_empty() {
scope.set_consts(&self.consts, &self.imported_consts);
}
scope.set_inline_templates(&self.inline_templates);
#[cfg(feature = "std")]
return compiled::render::render_segments_into(
&self.segments,
&mut scope,
self.base_dir.as_deref(),
output,
);
#[cfg(not(feature = "std"))]
return compiled::render_segments_into_no_std(&self.segments, &mut scope, output);
}
pub fn render_ctx_unchecked(&self, ctx: &Context) -> Result<String, TemplateError> {
let mut output = String::with_capacity(self.estimated_capacity);
self.render_core(ctx, &mut output)?;
Ok(output)
}
pub fn render_ctx_into_unchecked(
&self,
ctx: &Context,
output: &mut String,
) -> Result<(), TemplateError> {
self.render_core(ctx, output)
}
#[cfg(feature = "std")]
pub fn render_ctx_cached<S: core::hash::BuildHasher + Send + Sync>(
&self,
ctx: &Context,
cache: &crate::TemplateCache<S>,
) -> Result<String, TemplateError> {
self.validate_context(ctx, false)?;
let ctx = self.inject_defaults(ctx);
let mut scope =
Scope::with_cache(&ctx, cache).with_max_include_depth(self.max_include_depth);
if !self.consts.is_empty() || !self.imported_consts.is_empty() {
scope.set_consts(&self.consts, &self.imported_consts);
}
scope.set_inline_templates(&self.inline_templates);
compiled::render_segments(&self.segments, &mut scope, self.base_dir.as_deref())
}
#[cfg(feature = "std")]
pub fn render_ctx_cached_allowing_extra<S: core::hash::BuildHasher + Send + Sync>(
&self,
ctx: &Context,
cache: &crate::TemplateCache<S>,
) -> Result<String, TemplateError> {
self.validate_context(ctx, true)?;
let ctx = self.inject_defaults(ctx);
let mut scope =
Scope::with_cache(&ctx, cache).with_max_include_depth(self.max_include_depth);
if !self.consts.is_empty() || !self.imported_consts.is_empty() {
scope.set_consts(&self.consts, &self.imported_consts);
}
scope.set_inline_templates(&self.inline_templates);
compiled::render_segments(&self.segments, &mut scope, self.base_dir.as_deref())
}
fn inject_defaults<'a>(&self, ctx: &'a Context) -> alloc::borrow::Cow<'a, Context> {
if !self.has_defaults {
return alloc::borrow::Cow::Borrowed(ctx);
}
let mut owned: Option<Context> = None;
for decl in self.declared_variables.iter() {
if let Some(ref default) = decl.default_value {
let effective = owned.as_ref().unwrap_or(ctx);
if effective.get(&decl.name).is_none() {
let ctx_mut = owned.get_or_insert_with(|| ctx.clone());
ctx_mut.set(decl.name.clone(), default.clone());
}
}
}
match owned {
Some(ctx) => alloc::borrow::Cow::Owned(ctx),
None => alloc::borrow::Cow::Borrowed(ctx),
}
}
}
#[cfg(feature = "serde")]
impl Template {
pub fn render<T: serde::Serialize>(
&self,
value: &T,
) -> Result<String, crate::error::TemplateError> {
let ctx = Context::from_serialize(value)?;
self.render_ctx(&ctx)
}
pub fn render_into<T: serde::Serialize>(
&self,
value: &T,
output: &mut String,
) -> Result<(), crate::error::TemplateError> {
let ctx = Context::from_serialize(value)?;
self.render_ctx_into(&ctx, output)
}
}
impl PartialEq for Template {
fn eq(&self, other: &Self) -> bool {
self.source_hash == other.source_hash
}
}
impl Eq for Template {}
#[cfg(feature = "serde")]
impl serde::Serialize for Template {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&format!("template:{:016x}", self.source_hash))
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Template {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let _ = <serde::de::IgnoredAny as serde::Deserialize>::deserialize(deserializer)?;
Err(serde::de::Error::custom(
"Template cannot be deserialized; construct from source with \
Template::from_source() or Template::from_file()",
))
}
}
#[cfg(feature = "std")]
pub fn load_template(dir: &Path, name: &str) -> Result<Template, TemplateError> {
let path = dir.join(format!("{name}.tmpl.md"));
Template::from_file(&path)
}
fn inject_enum_type_constants(
type_aliases: &HashMap<String, VarType>,
consts: &mut HashMap<String, Value>,
) {
for (type_name, var_type) in type_aliases {
let VarType::Enum(variants) = var_type else {
continue;
};
if consts.contains_key(type_name) {
continue;
}
let mut variant_map = HashMap::new();
for variant in variants {
if variant.fields.is_empty() {
variant_map.insert(variant.name.clone(), Value::Str(variant.name.clone()));
} else {
let mut partial = HashMap::new();
partial.insert(
crate::consts::ENUM_TAG_KEY.into(),
Value::Str(variant.name.clone()),
);
variant_map.insert(variant.name.clone(), Value::Struct(Arc::new(partial)));
}
}
consts.insert(type_name.clone(), Value::Struct(Arc::new(variant_map)));
}
}
fn collect_enum_type_keys(fm: &Frontmatter) -> HashSet<String> {
let mut keys = HashSet::new();
for (name, ty) in &fm.type_aliases {
if matches!(ty, VarType::Enum(_)) {
keys.insert(name.clone());
}
}
for key in &fm.imported_enum_type_keys {
keys.insert(key.clone());
}
keys
}
fn check_bare_enum_access(
segments: &[compiled::Segment],
enum_keys: &HashSet<String>,
) -> Result<(), TemplateError> {
for seg in segments {
match seg {
compiled::Segment::Expr {
expr: compiled::CompiledExpr::Path(path),
..
} => {
let parts = path.parts();
if parts.len() >= 2 && is_enum_path(parts, enum_keys) {
return Err(TemplateError::syntax(format!(
"bare enum literal '{}' is not allowed — \
use kind({}) to get the variant name as a string",
path.as_str(),
path.as_str(),
)));
}
}
compiled::Segment::ForLoop { body, .. } => {
check_bare_enum_access(body, enum_keys)?;
}
compiled::Segment::If {
branches,
else_body,
} => {
for (_, branch_body) in branches {
check_bare_enum_access(branch_body, enum_keys)?;
}
check_bare_enum_access(else_body, enum_keys)?;
}
compiled::Segment::Match { arms, .. } => {
for (_, arm_body) in arms {
check_bare_enum_access(arm_body, enum_keys)?;
}
}
compiled::Segment::Include(inc) => {
if let Some(ref inline) = inc.inline_compiled {
check_bare_enum_access(&inline.segments, enum_keys)?;
}
}
_ => {}
}
}
Ok(())
}
fn is_enum_path(parts: &[String], enum_keys: &HashSet<String>) -> bool {
if enum_keys.contains(&parts[0]) {
return true;
}
if parts.len() >= 3 {
let key = format!("{}.{}", parts[0], parts[1]);
if enum_keys.contains(&key) {
return true;
}
}
false
}
fn check_undeclared_variables(
referenced: &HashSet<String>,
fm: &Frontmatter,
inline_templates: &HashMap<String, CompiledInlineTemplate>,
) -> Result<(), TemplateError> {
let mut declared: HashSet<String> = fm.params.iter().cloned().collect();
for c in &fm.consts {
declared.insert(c.name.clone());
}
for import in &fm.imports {
declared.insert(import.stem.clone());
}
for (name, ty) in &fm.type_aliases {
if matches!(ty, VarType::Enum(_)) {
declared.insert(name.clone());
}
}
for inline_name in inline_templates.keys() {
declared.insert(inline_name.clone());
}
let undeclared: Vec<&String> = referenced
.iter()
.filter(|v| !declared.contains(v.as_str()))
.collect();
if undeclared.is_empty() {
return Ok(());
}
let mut names: Vec<&str> = undeclared.iter().map(|s| s.as_str()).collect();
names.sort_unstable();
let mut suggestions = Vec::new();
for name in &names {
let mut best: Option<(&str, usize)> = None;
for candidate in &declared {
let dist = crate::error::levenshtein_distance(name, candidate);
if dist > 0 && dist <= 2 && best.is_none_or(|b| dist < b.1) {
best = Some((candidate, dist));
}
}
if let Some((suggestion, _)) = best {
suggestions.push(format!("'{name}' (did you mean '{suggestion}'?)"));
}
}
let suffix = if suggestions.is_empty() {
String::new()
} else {
format!(". Suggestions: {}", suggestions.join(", "))
};
Err(TemplateError::syntax(format!(
"{}{}{suffix}",
crate::consts::ERR_UNDECLARED_PREFIX,
names.join(", ")
)))
}
fn check_unused_params(
declarations: &[VarDecl],
referenced: &HashSet<String>,
allow_unused: bool,
) -> Result<(), TemplateError> {
if allow_unused {
return Ok(());
}
let unused: Vec<&str> = declarations
.iter()
.filter(|decl| !referenced.contains(&decl.name))
.map(|decl| decl.name.as_str())
.collect();
if unused.is_empty() {
return Ok(());
}
Err(TemplateError::syntax(format!(
"unused declared parameter(s): {}. Reference them in the template body, \
in a {{# comment #}}, or remove them from the frontmatter `params:` list. \
To suppress this check, add `allow_unused: true` to the frontmatter",
unused.join(", ")
)))
}
fn check_name_collisions(
fm: &Frontmatter,
inline_templates: &HashMap<String, CompiledInlineTemplate>,
segments: &[Segment],
) -> Result<(), TemplateError> {
for import in &fm.imports {
if inline_templates.contains_key(&import.stem) {
return Err(TemplateError::syntax(format!(
"import stem '{}' conflicts with inline template name",
import.stem
)));
}
}
let param_and_const_names: HashSet<&str> = fm
.params
.iter()
.map(String::as_str)
.chain(fm.consts.iter().map(|c| c.name.as_str()))
.collect();
for inline_name in inline_templates.keys() {
if param_and_const_names.contains(inline_name.as_str()) {
return Err(TemplateError::syntax(format!(
"inline template name '{inline_name}' conflicts with a declared parameter or constant"
)));
}
}
let protected_names: HashSet<&str> = fm
.params
.iter()
.map(String::as_str)
.chain(fm.consts.iter().map(|c| c.name.as_str()))
.chain(fm.imports.iter().map(|i| i.stem.as_str()))
.chain(inline_templates.keys().map(String::as_str))
.collect();
validate_for_bindings(segments, &protected_names)
}
fn validate_for_bindings(
segments: &[crate::compiled::Segment],
protected: &HashSet<&str>,
) -> Result<(), TemplateError> {
use crate::compiled::Segment;
for seg in segments {
match seg {
Segment::ForLoop { binding, body, .. } => {
if protected.contains(binding.as_ref()) {
return Err(TemplateError::syntax(format!(
"{} declared name '{binding}'",
crate::consts::ERR_FOR_BINDING_SHADOWS,
)));
}
validate_for_bindings(body, protected)?;
}
Segment::If {
branches,
else_body,
} => {
for (_cond, branch_body) in branches {
validate_for_bindings(branch_body, protected)?;
}
validate_for_bindings(else_body, protected)?;
}
Segment::Match { arms, .. } => {
for (_variants, arm_body) in arms {
validate_for_bindings(arm_body, protected)?;
}
}
_ => {}
}
}
Ok(())
}
#[cfg(not(feature = "std"))]
fn hash_source_no_std(source: &str) -> u64 {
crate::__private::fnv1a_hash(source.as_bytes())
}
#[cfg(all(test, feature = "std"))]
mod adversarial_tests;
#[cfg(all(test, feature = "std"))]
mod collision_and_scope_tests;
#[cfg(all(test, feature = "std"))]
mod const_tests;
#[cfg(all(test, feature = "std"))]
mod error_diagnostic_tests;
#[cfg(all(test, feature = "std"))]
mod higher_order_tests;
#[cfg(all(test, feature = "std"))]
mod inline_edge_tests;
#[cfg(all(test, feature = "std"))]
mod render_integration_tests;
#[cfg(all(test, feature = "std"))]
mod shared_tests;
#[cfg(all(test, feature = "std"))]
mod tests;