moon_tera

A deterministic Tera-style template engine for MoonBit

template
jinja
tera
ssg
web
rendering
moon add btlqql/moon_tera@0.3.0
Download zip
Author
Version
0.3.0
License
MIT
Last updated
21 days ago
Downloads
5
README

#moon_tera

moon_tera is a deterministic, dependency-light Tera-style template engine implemented in MoonBit. It is designed for embedded rendering, static-site generation, configuration files, and prompt templates where predictable output and a portable core matter more than full Jinja compatibility.

The implementation is inspired by the syntax and behavior of Keats/tera, but is written for MoonBit's type system and package model. Rendering currently uses a direct AST interpreter.

#Highlights

  • Expressions: literals, variables, dotted and indexed access, arithmetic, comparisons, and / or / not, filters, and is tests.
  • Control flow: if / elif / else, for with an empty branch, and set.
  • Multi-template rendering: extends, block, super(), and include.
  • Deterministic Value objects that preserve insertion order.
  • Explicit RenderError results for missing templates, missing parents, missing includes, inheritance cycles, and include cycles.
  • Whole-registry structural validation through Tera::validate().
  • Built-in string, collection, number, escaping, and JSON filters.
  • No third-party runtime dependencies.
  • 82 tests executed on wasm, wasm-gc, JavaScript, and native in CI.

#Install

After the package is available on MoonCakes:

moon add btlqql/moon_tera@0.3.0

Import the root package:

import {
"btlqql/moon_tera" @tera,
}

#Quick start

let engine = @tera.Tera::new()
engine.add_template("hello", "Hello {{ name | upper }}!")

let context = @tera.object_value([
("name", @tera.str_value("MoonBit")),
])

match engine.try_render("hello", context) {
Ok(output) => println(output)
Err(error) => println(error.message())
}

Validate every registered template before serving requests or generating a site:

match engine.validate() {
Ok(()) => println("template registry is valid")
Err(error) => println(error.message())
}

Run the included end-to-end example:

moon run cmd/main --target js

Expected output:

<main><h1>MOON TERA</h1><p>deterministic output</p><p>portable MoonBit core</p></main>

#Verification

moon fmt --check moon check --deny-warn moon build --target all --deny-warn moon test --target all --deny-warn moon run cmd/main --target js

#Scope

moon_tera intentionally implements a focused Tera-compatible subset. It does not currently support macros, imports, user-defined filters, streaming output, or automatic HTML escaping. Unsupported syntax must not be assumed to behave like upstream Tera. See COMPATIBILITY.md for the exact boundary and planned work.

#Project structure

PathResponsibility
value.mbtOrdered dynamic values and deterministic JSON encoding
lexer.mbt, token.mbtTemplate-level tokenization
expr.mbtExpression lexer and Pratt parser
parser.mbt, ast.mbtTemplate parser and AST
vm.mbtAST evaluator
filters.mbt, is_tests.mbtBuilt-in filters and tests
tera.mbt, error.mbtTemplate registry, inheritance, validation, diagnostics
cmd/mainRunnable integration example

#Project materials

#License and attribution

The project is released under the MIT License. Tera attribution and the upstream license are preserved in NOTICE and LICENSE-TERA.

#
Expr

pub(all) enum Expr {
EInt(Int)
EFloat(Double)
EStr(String)
EBool(Bool)
EVar(String)
EMember(Expr, String)
EIndex(Expr, Expr)
EBinOp(String, Expr, Expr)
ENot(Expr)
EFilter(Expr, String, Array[Expr])
EIs(Expr, String)
ESuper
} derive(
Debug
)

A parsed template expression.

#
ExprToken

pub(all) enum ExprToken {
EIdent(String)
EInt(Int)
EStr(String)
EOp(String)
EDot
ELparen
ERparen
ELbracket
ERbracket
EComma
EPipe
EEof
} derive(
Debug
)

A token in the expression sub-language.

#
Node

pub(all) enum Node {
Text(String)
Output(Expr)
If(Expr, Array[Node], Array[Node])
For(String, Expr, Array[Node], Array[Node])
Block(String, Array[Node])
Include(String)
Set(String, Expr)
} derive(
Debug
)

A node in a parsed template's syntax tree.

#
RenderError

pub(all) enum RenderError {
TemplateNotFound(String)
ParentNotFound(String)
InheritanceCycle(String)
IncludeNotFound(String)
IncludeCycle(String)
} derive(Eq,
Debug
)

A structural error discovered before or during named-template rendering.

#
RenderError::message

fn RenderError::message(self : RenderError) -> String

Return a stable diagnostic message suitable for CLIs and logs.

#
Tera

pub struct Tera {
templates : Array[(String, Tpl)]
}

A template engine holding multiple named templates (for inheritance/include).

#
Tera::add_template

fn Tera::add_template(self : Tera, name : String, src : String) -> Unit

Register a named template by parsing its source.

#
Tera::clear

fn Tera::clear(self : Tera) -> Unit

Remove every registered template.

#
Tera::contains_template

fn Tera::contains_template(self : Tera, name : String) -> Bool

Return whether a template is registered under name.

#
Tera::new

fn Tera::new() -> Tera

Create an empty engine.

#
Tera::remove_template

fn Tera::remove_template(self : Tera, name : String) -> Bool

Remove a registered template. Returns true when an entry was removed.

#
Tera::render

fn Tera::render(self : Tera, name : String, ctx : Value) -> 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.

#
Tera::template_count

fn Tera::template_count(self : Tera) -> Int

Return the number of registered templates.

#
Tera::template_names

fn Tera::template_names(self : Tera) -> Array[String]

Return registered template names in insertion order.

#
Tera::try_render

fn Tera::try_render(self : Tera, name : String, ctx : Value) -> Result[String, RenderError]

Render a named template with structural validation and explicit errors.

#
Tera::validate

fn Tera::validate(self : Tera) -> Result[Unit, RenderError]

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.

#
Token

pub(all) enum Token {
Text(String)
Variable(String)
Tag(String)
} derive(Eq,
Debug
)

A token produced by the template-level lexer.

The lexer splits raw template text into three kinds of segments: literal text, variable expressions ({{ ... }}), and block tags ({% ... %}). Comments ({# ... #}) are discarded during lexing.

#
Tpl

type Tpl

A parsed template: its nodes, the extends parent name, and block defs.

#
Value

pub(all) enum Value {
Null
Bool(Bool)
Int(Int)
Float(Double)
Str(String)
Array(Array[Value])
Object(Array[(String, Value)])
} derive(
Debug
)

A dynamic value used as the template rendering context and intermediate data.

#
Value::describe

fn Value::describe(self : Value) -> String

Return a stable, human-readable description for diagnostics and tests.

#
Value::is_truthy

fn Value::is_truthy(self : Value) -> Bool

Truthiness used by {% if %} and filters like default.

Following Tera/Jinja: Null, Bool(false), Int(0), Float(0.0), empty Str/Array/Object are falsy; everything else is truthy.

#
Value::to_json_string

fn Value::to_json_string(self : Value) -> String

Encode a value as deterministic JSON while preserving object insertion order.

#
apply_filter

fn apply_filter(name : String, input : Value, args : Array[Value]) -> Value

Apply a named filter to a value with already-evaluated arguments. Unknown filters pass the input through unchanged.

#
apply_test

fn apply_test(name : String, v : Value) -> Bool

Apply a named is test to a value. Unknown tests return false.

#
array_value

fn array_value(a : Array[Value]) -> Value

Construct an array value.

#
bool_value

fn bool_value(b : Bool) -> Value

Construct a boolean value.

#
expr_lex

fn expr_lex(s : String) -> Array[ExprToken]

Lex an expression string into tokens.

#
expr_parse

fn expr_parse(toks : Array[ExprToken]) -> Expr

Parse a token list into an Expr.

#
float_value

fn float_value(f : Double) -> Value

Construct a float value.

#
int_value

fn int_value(i : Int) -> Value

Construct an integer value.

#
lex

fn lex(src : String) -> Array[Token]

Lex a template string into top-level tokens.

#
lookup

fn lookup(field : String, ctx : Value) -> Value

Look up a field in a Value context. Returns Null when the field is absent or the context is not an object.

#
null_value

fn null_value() -> Value

Construct a null value.

#
object_value

fn object_value(o : Array[(String, Value)]) -> Value

Construct an object value from insertion-ordered key/value pairs.

#
parse

fn parse(tokens : Array[Token]) -> (Array[Node], String)

Parse tokens into AST nodes and the extends parent name ("" if none).

#
parse_expr

fn parse_expr(s : String) -> Expr

Parse a raw expression string into an Expr (lexer + Pratt parser).

#
render

fn render(template : String, ctx : Value) -> String

Render a standalone template string against a context Value.

#
render_nodes

fn render_nodes(nodes : Array[Node], ctx : Value) -> String

Render a parsed template (list of nodes) against a context (no inheritance).

#
str_value

fn str_value(s : String) -> Value

Construct a string value.