// The template VM.
//
// Rendering uses a direct AST-walk interpreter (`eval_nodes`).
///|
/// Evaluate an expression against a context to a `Value`.
fn eval_expr(e : Expr, ctx : Value) -> Value {
match e {
EInt(i) => Int(i)
EFloat(f) => Float(f)
EStr(s) => Str(s)
EBool(b) => Bool(b)
EVar(name) => lookup(name, ctx)
EMember(obj, field) => lookup(field, eval_expr(obj, ctx))
EIndex(container, index) =>
index_value(eval_expr(container, ctx), eval_expr(index, ctx))
ENot(inner) => Bool(!eval_expr(inner, ctx).is_truthy())
EBinOp(op, l, r) => eval_binop(op, eval_expr(l, ctx), eval_expr(r, ctx))
EFilter(input, name, arg_exprs) => {
let v = eval_expr(input, ctx)
let arg_vals : Array[Value] = []
for ae in arg_exprs {
arg_vals.push(eval_expr(ae, ctx))
}
apply_filter(name, v, arg_vals)
}
EIs(e, name) => Bool(apply_test(name, eval_expr(e, ctx)))
ESuper => Str("") // placeholder; handled specially in eval_nodes
}
}
///|
/// Resolve an array integer index or an object string key.
/// Invalid types and out-of-range indexes evaluate to `Null`.
fn index_value(container : Value, index : Value) -> Value {
match (container, index) {
(Array(items), Int(position)) =>
if position >= 0 && position < items.length() {
items[position]
} else {
Null
}
(Object(_), Str(key)) => lookup(key, container)
_ => Null
}
}
///|
/// Structural equality of two values (same-type only).
fn value_eq(a : Value, b : Value) -> Bool {
match (a, b) {
(Null, Null) => true
(Bool(x), Bool(y)) => x == y
(Int(x), Int(y)) => x == y
(Float(x), Float(y)) => x == y
(Str(x), Str(y)) => x == y
_ => false
}
}
///|
/// Less-than ordering for numbers and strings.
fn value_lt(a : Value, b : Value) -> Bool {
match (a, b) {
(Int(x), Int(y)) => x < y
(Float(x), Float(y)) => x < y
(Str(x), Str(y)) => x < y
_ => false
}
}
///|
/// Evaluate a binary operator.
fn eval_binop(op : String, l : Value, r : Value) -> Value {
match op {
"==" => Bool(value_eq(l, r))
"!=" => Bool(!value_eq(l, r))
"<" => Bool(value_lt(l, r))
">" => Bool(value_lt(r, l))
"<=" => Bool(!value_lt(r, l))
">=" => Bool(!value_lt(l, r))
"and" => Bool(l.is_truthy() && r.is_truthy())
"or" => Bool(l.is_truthy() || r.is_truthy())
"+" => value_add(l, r)
"-" => value_sub(l, r)
"*" => value_mul(l, r)
"/" => value_div(l, r)
"%" => value_mod(l, r)
_ => Null
}
}
///|
fn value_add(l : Value, r : Value) -> Value {
match (l, r) {
(Int(a), Int(b)) => Int(a + b)
(Float(a), Float(b)) => Float(a + b)
(Str(a), Str(b)) => Str(a + b)
_ => Null
}
}
///|
fn value_sub(l : Value, r : Value) -> Value {
match (l, r) {
(Int(a), Int(b)) => Int(a - b)
(Float(a), Float(b)) => Float(a - b)
_ => Null
}
}
///|
fn value_mul(l : Value, r : Value) -> Value {
match (l, r) {
(Int(a), Int(b)) => Int(a * b)
(Float(a), Float(b)) => Float(a * b)
_ => Null
}
}
///|
fn value_div(l : Value, r : Value) -> Value {
match (l, r) {
(Int(a), Int(b)) => if b == 0 { Null } else { Int(a / b) }
(Float(a), Float(b)) => Float(a / b)
_ => Null
}
}
///|
fn value_mod(l : Value, r : Value) -> Value {
match (l, r) {
(Int(a), Int(b)) => if b == 0 { Null } else { Int(a % b) }
_ => Null
}
}
///|
/// Render a `Value` to its template string form.
fn render_value(v : Value) -> String {
match v {
Null => ""
Bool(b) => b.to_string()
Int(i) => i.to_string()
Float(f) => f.to_string()
Str(s) => s
Array(_) => v.to_json_string()
Object(_) => v.to_json_string()
}
}
///|
/// Evaluate an expression's truthiness (for `{% if %}`).
fn eval_truthy(e : Expr, ctx : Value) -> Bool {
eval_expr(e, ctx).is_truthy()
}
///|
/// Bind a loop variable on top of a context (loop var takes precedence).
fn bind_var(name : String, val : Value, ctx : Value) -> Value {
let entries : Array[(String, Value)] = []
entries.push((name, val))
match ctx {
Object(old) =>
for e in old {
entries.push(e)
}
_ => ()
}
Object(entries)
}
///|
/// Find a block's effective body by name (first match wins; children first).
fn lookup_block(
blocks : Array[(String, Array[Node])],
name : String,
default : Array[Node],
) -> Array[Node] {
for entry in blocks {
let (n, body) = entry
if n == name {
return body
}
}
default
}
///|
/// Find the parent (ancestor) block body: the second match for `name`.
fn lookup_parent_block(
blocks : Array[(String, Array[Node])],
name : String,
default : Array[Node],
) -> Array[Node] {
let mut found_first = false
for entry in blocks {
let (n, body) = entry
if n == name {
if found_first {
return body
}
found_first = true
}
}
default
}
///|
/// Evaluate a list of nodes into the output buffer.
/// `blocks` holds overriding block bodies; `tera` resolves includes;
/// `super_body` is the parent block body for `super()`.
fn eval_nodes(
nodes : Array[Node],
ctx : Value,
blocks : Array[(String, Array[Node])],
tera : Tera,
super_body : Array[Node],
out : StringBuilder,
) -> Unit {
let mut current_ctx = ctx
for node in nodes {
match node {
Text(s) => out.write_string(s)
Output(e) =>
match e {
ESuper =>
eval_nodes(super_body, current_ctx, blocks, tera, super_body, out)
_ => out.write_string(render_value(eval_expr(e, current_ctx)))
}
If(cond, then_branch, else_branch) =>
if eval_truthy(cond, current_ctx) {
eval_nodes(then_branch, current_ctx, blocks, tera, super_body, out)
} else {
eval_nodes(else_branch, current_ctx, blocks, tera, super_body, out)
}
For(name, iter, body, empty_body) =>
match eval_expr(iter, current_ctx) {
Array(items) => {
let cnt = items.length()
if cnt == 0 {
eval_nodes(empty_body, current_ctx, blocks, tera, super_body, out)
} else {
for idx = 0; idx < cnt; idx = idx + 1 {
let loop_val = object_value([
("index", int_value(idx + 1)),
("index0", int_value(idx)),
("first", bool_value(idx == 0)),
("last", bool_value(idx == cnt - 1)),
("length", int_value(cnt)),
])
let ctx1 = bind_var(name, items[idx], current_ctx)
let ctx2 = bind_var("loop", loop_val, ctx1)
eval_nodes(body, ctx2, blocks, tera, super_body, out)
}
}
}
_ =>
eval_nodes(empty_body, current_ctx, blocks, tera, super_body, out)
}
Block(name, body) => {
let effective = lookup_block(blocks, name, body)
let parent = lookup_parent_block(blocks, name, body)
eval_nodes(effective, current_ctx, blocks, tera, parent, out)
}
Include(name) =>
match find_tpl(tera.templates, name) {
Some(tpl) =>
eval_nodes(tpl.nodes, current_ctx, blocks, tera, super_body, out)
None => ()
}
Set(name, e) =>
current_ctx = bind_var(name, eval_expr(e, current_ctx), current_ctx)
}
}
}
///|
/// Render a parsed template (list of nodes) against a context (no inheritance).
pub fn render_nodes(nodes : Array[Node], ctx : Value) -> String {
let out = StringBuilder::new()
eval_nodes(nodes, ctx, [], Tera::new(), [], out)
out.to_string()
}