// Top-level engine entry point.
///|
/// Render a standalone template string against a context `Value`.
pub fn render(template : String, ctx : Value) -> String {
let (nodes, _) = parse(lex(template))
render_nodes(nodes, ctx)
}
///|
/// A parsed template: its nodes, the `extends` parent name, and block defs.
struct Tpl {
nodes : Array[Node]
parent : String
blocks : Array[(String, Array[Node])]
}
///|
/// A template engine holding multiple named templates (for inheritance/include).
pub struct Tera {
templates : Array[(String, Tpl)]
}
///|
/// Create an empty engine.
pub fn Tera::new() -> Tera {
Tera::{ templates: [] }
}
///|
/// Register a named template by parsing its source.
pub fn Tera::add_template(self : Tera, name : String, src : String) -> Unit {
let (nodes, parent) = parse(lex(src))
let blocks : Array[(String, Array[Node])] = []
collect_blocks(nodes, blocks)
let compiled = Tpl::{ nodes, parent, blocks }
for index, entry in self.templates {
let (existing_name, _) = entry
if existing_name == name {
self.templates[index] = (name, compiled)
return
}
}
self.templates.push((name, compiled))
}
///|
/// Return whether a template is registered under `name`.
pub fn Tera::contains_template(self : Tera, name : String) -> Bool {
find_tpl(self.templates, name) is Some(_)
}
///|
/// Return registered template names in insertion order.
pub fn Tera::template_names(self : Tera) -> Array[String] {
let names : Array[String] = []
for entry in self.templates {
let (name, _) = entry
names.push(name)
}
names
}
///|
/// Return the number of registered templates.
pub fn Tera::template_count(self : Tera) -> Int {
self.templates.length()
}
///|
/// Remove a registered template. Returns `true` when an entry was removed.
pub fn Tera::remove_template(self : Tera, name : String) -> Bool {
for index, entry in self.templates {
let (existing_name, _) = entry
if existing_name == name {
ignore(self.templates.remove(index))
return true
}
}
false
}
///|
/// Remove every registered template.
pub fn Tera::clear(self : Tera) -> Unit {
self.templates.clear()
}
///|
/// Collect top-level `Block` definitions from a node list.
fn collect_blocks(
nodes : Array[Node],
acc : Array[(String, Array[Node])],
) -> Unit {
for node in nodes {
match node {
Block(name, body) => acc.push((name, body))
_ => ()
}
}
}
///|
/// Look up a registered template by name.
fn find_tpl(templates : Array[(String, Tpl)], name : String) -> Tpl? {
for entry in templates {
let (n, tpl) = entry
if n == name {
return Some(tpl)
}
}
None
}
///|
/// Resolve the inheritance chain for `name`.
/// Returns (base_nodes, effective_blocks) where base is the top ancestor and
/// effective_blocks lists children first (so they override ancestors).
fn contains_name(names : Array[String], target : String) -> Bool {
for name in names {
if name == target {
return true
}
}
false
}
///|
fn with_name(names : Array[String], name : String) -> Array[String] {
let result : Array[String] = []
for item in names {
result.push(item)
}
result.push(name)
result
}
///|
fn validate_includes(
nodes : Array[Node],
templates : Array[(String, Tpl)],
stack : Array[String],
) -> RenderError? {
for node in nodes {
let nested = match node {
Include(name) => {
if contains_name(stack, name) {
return Some(IncludeCycle(name))
}
match find_tpl(templates, name) {
None => return Some(IncludeNotFound(name))
Some(template) =>
validate_includes(template.nodes, templates, with_name(stack, name))
}
}
If(_, then_branch, else_branch) =>
match validate_includes(then_branch, templates, stack) {
Some(error) => Some(error)
None => validate_includes(else_branch, templates, stack)
}
For(_, _, body, empty_body) =>
match validate_includes(body, templates, stack) {
Some(error) => Some(error)
None => validate_includes(empty_body, templates, stack)
}
Block(_, body) => validate_includes(body, templates, stack)
_ => None
}
match nested {
Some(error) => return Some(error)
None => ()
}
}
None
}
///|
fn resolve_chain(
templates : Array[(String, Tpl)],
name : String,
) -> Result[(Array[Node], Array[(String, Array[Node])]), RenderError] {
let chain : Array[Tpl] = []
let visited : Array[String] = []
let mut current = name
while current != "" {
if contains_name(visited, current) {
return Err(InheritanceCycle(current))
}
visited.push(current)
match find_tpl(templates, current) {
None =>
if current == name {
return Err(TemplateNotFound(current))
} else {
return Err(ParentNotFound(current))
}
Some(template) => {
chain.push(template)
current = template.parent
}
}
}
let base = chain[chain.length() - 1]
let blocks : Array[(String, Array[Node])] = []
for template in chain {
for entry in template.blocks {
blocks.push(entry)
}
}
Ok((base.nodes, blocks))
}
///|
/// Resolve and structurally validate one named template without rendering it.
fn resolve_validated(
templates : Array[(String, Tpl)],
name : String,
) -> Result[(Array[Node], Array[(String, Array[Node])]), RenderError] {
match resolve_chain(templates, name) {
Err(error) => Err(error)
Ok((base_nodes, blocks)) => {
match validate_includes(base_nodes, templates, [name]) {
Some(error) => return Err(error)
None => ()
}
for entry in blocks {
let (_, body) = entry
match validate_includes(body, templates, [name]) {
Some(error) => return Err(error)
None => ()
}
}
Ok((base_nodes, blocks))
}
}
}
///|
/// Validate every registered template without rendering output.
///
/// This preflight check catches missing parents, inheritance cycles, missing
/// includes, and include cycles anywhere in the registry, including templates
/// that have not been rendered yet.
pub fn Tera::validate(self : Tera) -> Result[Unit, RenderError] {
for entry in self.templates {
let (name, _) = entry
match resolve_validated(self.templates, name) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
Ok(())
}
///|
/// Render a named template with structural validation and explicit errors.
pub fn Tera::try_render(
self : Tera,
name : String,
ctx : Value,
) -> Result[String, RenderError] {
match resolve_validated(self.templates, name) {
Err(error) => Err(error)
Ok((base_nodes, blocks)) => {
let out = StringBuilder::new()
eval_nodes(base_nodes, ctx, blocks, self, [], out)
Ok(out.to_string())
}
}
}
///|
/// Render a registered template by name against a context.
///
/// This compatibility API returns an empty string on structural errors. New
/// applications should prefer `try_render` and surface its diagnostic.
pub fn Tera::render(self : Tera, name : String, ctx : Value) -> String {
match self.try_render(name, ctx) {
Ok(output) => output
Err(_) => ""
}
}