// Copyright (c) 2024 LinZeming
// Released under the MIT License
//
// Renderer — walks an AST and produces the final output string.
//
// The renderer evaluates `Output` expressions against a `Context` and
// concatenates `Text` nodes. Other node types will be added incrementally.
// ---------------------------------------------------------------------------
// Renderer struct
// ---------------------------------------------------------------------------
///|
/// Template renderer — converts `Node` trees into a string.
pub(all) struct Renderer {
evaluator : Evaluator
output : StringBuilder
loader : TemplateLoader
}
// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------
///|
/// Create a new renderer with the given *ctx* and *loader*.
pub fn Renderer::new(ctx : Context, loader : TemplateLoader) -> Renderer {
{
evaluator: Evaluator::new(ctx),
output: StringBuilder::new(),
loader: loader,
}
}
// ---------------------------------------------------------------------------
// Top-level render entry point
// ---------------------------------------------------------------------------
///|
/// Render a list of nodes and return the final output string.
pub fn Renderer::render(
self : Renderer,
nodes : Array[Node]
) -> Result[String, String] {
let r = self.render_nodes(nodes)
match r {
Ok(renderer) => Ok(renderer.output.to_string())
Err(msg) => Err(msg)
}
}
///|
/// Render a list of nodes, returning the updated renderer.
fn Renderer::render_nodes(
self : Renderer,
nodes : Array[Node]
) -> Result[Renderer, String] {
let mut r = self
for node in nodes {
let result = r.render_node(node)
r = match result {
Ok(renderer) => renderer
Err(msg) => return Err(msg)
}
}
Ok(r)
}
// ---------------------------------------------------------------------------
// Single-node rendering
// ---------------------------------------------------------------------------
///|
/// Render a single node into the output buffer.
pub fn Renderer::render_node(
self : Renderer,
node : Node
) -> Result[Renderer, String] {
match node {
// -- Text: append directly --------------------------------------------
Text(content) => {
let output = self.output
output.write_string(content)
Ok({ ..self, output })
}
// -- Output: evaluate expression, convert to string, append ----------
Output(expr) => {
let eval_result = self.evaluator.eval(expr)
let val = match eval_result {
Ok(v) => v
Err(msg) => return Err(msg)
}
let output = self.output
output.write_string(val.to_string())
Ok({ ..self, output })
}
// -- If: condition with elif / else branches ---------------------------
If(condition, then_body, elif_branches, else_body) => {
// Evaluate the main condition.
let eval_result = self.evaluator.eval(condition)
let cond_val = match eval_result {
Ok(v) => v
Err(msg) => return Err(msg)
}
if cond_val.is_truthy() {
return self.render_nodes(then_body)
}
// Try each elif branch.
for branch in elif_branches {
let branch_cond = match branch.condition {
Option::Some(expr) => expr
Option::None => continue
}
let branch_result = self.evaluator.eval(branch_cond)
let branch_val = match branch_result {
Ok(v) => v
Err(msg) => return Err(msg)
}
if branch_val.is_truthy() {
return self.render_nodes(branch.body)
}
}
// Fall through to else body.
if else_body.length() > 0 {
return self.render_nodes(else_body)
}
Ok(self)
}
// -- For loop ----------------------------------------------------------
For(loop_var, iterable, body) => {
let iter_result = self.evaluator.eval(iterable)
let iter_val = match iter_result {
Ok(v) => v
Err(msg) => return Err(msg)
}
match iter_val {
Array(items) => {
let len = items.length()
if len == 0 {
return Ok(self)
}
let mut r = self
for i = 0; i < len; i = i + 1 {
// Build loop builtins.
let loop_obj : Map[String, Value] = Map::new()
loop_obj["index"] = Int((i + 1).to_int64())
loop_obj["index0"] = Int(i.to_int64())
loop_obj["first"] = Bool(i == 0)
loop_obj["last"] = Bool(i + 1 == len)
loop_obj["length"] = Int(len.to_int64())
// Create child context with loop var + loop builtins.
let child_ctx = Context::new_child(r.evaluator.ctx)
.set(loop_var, items[i])
.set("loop", Object(loop_obj))
r = { ..r, evaluator: Evaluator::new(child_ctx) }
let render_result = r.render_nodes(body)
r = match render_result {
Ok(rr) => rr
Err(msg) => return Err(msg)
}
// Restore evaluator context for next iteration
// (the render result contains the updated evaluator state;
// we just need to keep the output buffer).
}
Ok(r)
}
_ => Err("For loop requires an array, got: " + iter_val.type_name())
}
}
// -- Macro: register definition in context, no output ------------------
Macro(name, params, body) => {
let def = MacroDef::{ name: name, params: params, body: body }
let ctx = self.evaluator.ctx.register_macro(def)
Ok({ ..self, evaluator: Evaluator::new_with_filters(ctx, self.evaluator.filters) })
}
// -- Include: load + parse + render sub-template ----------------------
Include(tpl_name) => {
let source_opt = self.loader.get(tpl_name)
let source = match source_opt {
Some(s) => s
None => return Err("Template not found: " + tpl_name)
}
let tpl_result = Template::parse(source)
let tpl = match tpl_result {
Ok(t) => t
Err(te) => return Err("Error parsing included template '" + tpl_name + "': " + te.to_string())
}
let render_result = tpl.render_with_loader(self.evaluator.ctx, self.loader)
let rendered = match render_result {
Ok(s) => s
Err(te) => return Err(te.to_string())
}
let output = self.output
output.write_string(rendered)
Ok({ ..self, output })
}
// -- Not implemented yet -----------------------------------------------
Block(_, _) => Err("blocks not yet implemented")
Extends(_) => Err("extends not yet implemented")
FilterBlock(_, _, _) => Err("filter blocks not yet implemented")
}
}