A Jinja2/Tera-inspired template rendering engine for MoonBit
{% extends "layout.html" %}
{% block title %}Welcome{% endblock %}
{% block content %}
<h1>Hello, {{ user.name | upper }}!</h1>
<ul>
{% for item in user.items %}
<li>{{ loop.index }}. {{ item | escape }}</li>
{% endfor %}
</ul>
{% endblock %}moon add linzeming/templatelet tpl = @template.Template::parse("Hello, {{ name }}!")?
let ctx = @template.Context::new()
.set("name", @template.Value::Str("MoonBit"))
let result = tpl.render(ctx)?
println(result) // "Hello, MoonBit!"fn main {
// 1. Parse the template
let source = #|
<h1>{{ title }}</h1>
{% if items | length > 0 %}
<ul>
{% for item in items %}
<li>{{ loop.index }}. {{ item }}</li>
{% endfor %}
</ul>
{% else %}
<p>No items found.</p>
{% endif %}
|#
let tpl = @template.Template::parse(source)?
// 2. Build the context
let ctx = @template.Context::new()
.set("title", @template.Value::Str("Shopping List"))
.set("items", @template.Value::Array([
@template.Value::Str("Apples"),
@template.Value::Str("Bananas"),
@template.Value::Str("Oranges"),
]))
// 3. Render
let html = tpl.render(ctx)?
println(html)
}{{ username }}
{{ user.name }} {# dot access #}
{{ items[0] }} {# index access #}
{{ a + b }} {# arithmetic #}
{{ user.name | upper }} {# with filter #}{{ name | upper }} {# "hello" → "HELLO" #}
{{ name | trim | capitalize }} {# chain filters #}
{{ items | join(", ") }} {# with arguments #}
{{ value | default("N/A") }} {# default value #}{% if user.is_admin %}
<a href="/admin">Admin Panel</a>
{% elif user.is_moderator %}
<span>Moderator</span>
{% else %}
<span>Regular User</span>
{% endif %}{% for item in items %}
<li>{{ loop.index }}. {{ item }}</li>
{% endfor %}| Variable | Description |
|---|---|
| loop.index | 1-based iteration counter |
| loop.index0 | 0-based iteration counter |
| loop.first | true on the first iteration |
| loop.last | true on the last iteration |
| loop.length | total number of items |
{% macro input(name, type="text", value="") %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{{ input("username") }}
{{ input("password", type="password") }}{% include "header.html" %}
<main>content</main>
{% include "footer.html" %}let loader = @template.TemplateLoader::new()
.add("header", "<header>{{ title }}</header>")
.add("footer", "<footer>© 2026</footer>")
let result = tpl.render_with_loader(ctx, loader)?{# base.html #}
<html>
<head><title>{% block title %}Default{% endblock %}</title></head>
<body>{% block body %}{% endblock %}</body>
</html>
{# page.html #}
{% extends "base.html" %}
{% block title %}My Page{% endblock %}
{% block body %}<p>Hello!</p>{% endblock %}{# This is a comment and will not appear in the output #}| Filter | Description | Example |
|---|---|---|
| upper | Uppercase string | "hello" → "HELLO" |
| lower | Lowercase string | "HELLO" → "hello" |
| capitalize | Capitalize first character | "hello" → "Hello" |
| trim | Remove leading/trailing whitespace | " hi " → "hi" |
| length | Length of string/array/object | "abc" → 3 |
| reverse | Reverse string or array | "abc" → "cba" |
| first | First character/element | "abc" → "a" |
| last | Last character/element | "abc" → "c" |
| join(sep) | Join array with separator | [a,b] → "a,b" |
| replace(from, to) | Replace substrings | "hi"|replace("i","ey") → "hey" |
| default(val) | Fallback if value is falsy | ""|default("N/A") → "N/A" |
| int | Convert to integer | "42"|int → 42 |
| string | Convert to string | 42|string → "42" |
let filters = @template.FilterRegistry::default()
.register("greet", fn(value, _args) {
match value {
@template.Value::Str(s) => Ok(@template.Value::Str("Hello, " + s + "!"))
_ => Err("greet: expected string")
}
})| Method | Signature | Description |
|---|---|---|
| parse | (source: String) → Result[Template, TemplateError] | Compile source to template |
| render | (ctx: Context) → Result[String, TemplateError] | Render with context |
| render_with_loader | (ctx: Context, loader: TemplateLoader) → Result[String, TemplateError] | Render with includes |
| Method | Description |
|---|---|
| new() | Create empty root context |
| new_child(parent) | Create nested scope |
| set(name, value) | Set variable in current scope |
| get(name) | Look up variable (walks scope chain) |
| register_macro(MacroDef) | Register a macro definition |
| Variant | Type |
|---|---|
| Null | null |
| Bool(Bool) | boolean |
| Int(Int64) | integer |
| Float(Double) | float |
| Str(String) | string |
| Array(Array[Value]) | array |
| Object(Map[String, Value]) | object (for member access) |
match Template::parse(source) {
Ok(t) => ...
Err(err) => {
println(err.to_string())
// Error: SyntaxError in template "<source>" at line 3, column 8
// expected '}}'
}
}# Render a template with JSON context
mbtemplate render "Hello, {{ name }}!" --data '{"name":"World"}'
# Check template syntax
mbtemplate check "{% if x %}ok{% endif %}"
# Show version
mbtemplate --version| Command | Description |
|---|---|
| render "<tpl>" [--data '<json>'] | Render template with optional JSON data |
| check "<tpl>" | Validate template syntax without rendering |
| --version | Show version information |
| --help | Show help message |
src/
├── lib.mbt # Public API entry point
├── ast.mbt # AST node types (Value, Expr, Node, ...)
├── lexer.mbt # Template source → Tokens
├── parser.mbt # Tokens → AST (Pratt expression parser)
├── evaluator.mbt # Expression evaluation
├── renderer.mbt # AST → output string
├── template.mbt # Top-level parse + render
├── context.mbt # Nested-scope variable storage
├── filters.mbt # Built-in filter functions
├── loader.mbt # Include template resolver
└── errors.mbt # Unified error typesmoon test| Feature | Status |
|---|---|
| Variables & expressions | ✅ |
| Filters (13 built-in) | ✅ |
| Conditionals (if/elif/else) | ✅ |
| For loops with loop.* | ✅ |
| Macros | ✅ |
| Include | ✅ |
| Template inheritance | 🚧 Planned |
| Auto-escaping | 🚧 Planned |
| Custom filters | ✅ |
pub(all) enum ErrorKind {
LexicalError
SyntaxError
EvalError
RenderError
FilterError
NotFoundError
} derive(Debug)fn FilterRegistry::lookup(self : FilterRegistry, name : String) -> (Value, Array[Value]) -> Result[Value, String]?fn FilterRegistry::register(self : FilterRegistry, name : String, f : (Value, Array[Value]) -> Result[Value, String]) -> FilterRegistryfn Template::render_with_loader(self : Template, ctx : Context, loader : TemplateLoader) -> Result[String, TemplateError]pub(all) struct TemplateError {
kind : ErrorKind
message : String
location : SourceLocation?
snippet : String?
} derive(Debug)pub(all) enum TokenKind {
Text(String)
OpenVar
CloseVar
OpenTag
CloseTag
OpenComment
CloseComment
If
Elif
Else
Endif
For
In
Endfor
Block
Endblock
Extends
Macro
Endmacro
Include
Ident(String)
Str(String)
Int(Int64)
Float(Double)
Bool(Bool)
Plus
Minus
Star
Slash
Percent
Eq
NotEq
Lt
Gt
LtEq
GtEq
And
Or
Not
Pipe
Colon
Comma
Dot
LParen
RParen
LBracket
RBracket
Assign
Tilde
Eof
Error(String)
} derive(Eq, Debug)fn extract_snippet(source : String, line : Int) -> StringInstall
Download zipA Jinja2/Tera-inspired template rendering engine for MoonBit