template

    A Jinja2/Tera-inspired template rendering engine for MoonBit

    template
    templating
    jinja2
    tera
    html
    engine
    Download zip
    Version
    0.1.0
    License
    MIT
    Last updated
    2 months ago
    Downloads
    15

    #MoonTera — A Jinja2/Tera-inspired Template Engine for MoonBit

    CI License: MIT

    MoonTera is a fast, safe, and easy-to-use template rendering engine for MoonBit, inspired by Jinja2 and Tera.

    {% 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 %}

    #Features

    • Variables{{ user.name }} with dot access and index access
    • Filters{{ name | upper | trim }} with 13 built-in filters
    • Conditionals{% if %}, {% elif %}, {% else %}, {% endif %}
    • Loops{% for x in items %}...{% endfor %} with loop.index, loop.first, etc.
    • Macros{% macro button(text, class) %}...{% endmacro %}
    • Includes{% include "partial.html" %}
    • Inheritance{% extends %}, {% block %}, {% endblock %} (planned)
    • Auto-escaping — Safe against XSS by default (planned)
    • Zero dependencies — Uses only moonbitlang/core

    #Quick Start

    #Installation

    moon add linzeming/template

    #Hello World

    let 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!"

    #Complete Example

    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) }

    #Syntax Guide

    #Variables

    Output variables with {{ }}:

    {{ username }} {{ user.name }} {# dot access #} {{ items[0] }} {# index access #} {{ a + b }} {# arithmetic #} {{ user.name | upper }} {# with filter #}

    #Filters

    Apply filters with the pipe operator |:

    {{ name | upper }} {# "hello" → "HELLO" #} {{ name | trim | capitalize }} {# chain filters #} {{ items | join(", ") }} {# with arguments #} {{ value | default("N/A") }} {# default value #}

    #Conditionals

    {% if user.is_admin %} <a href="/admin">Admin Panel</a> {% elif user.is_moderator %} <span>Moderator</span> {% else %} <span>Regular User</span> {% endif %}

    #Loops

    {% for item in items %} <li>{{ loop.index }}. {{ item }}</li> {% endfor %}

    loop builtin variables:

    VariableDescription
    loop.index1-based iteration counter
    loop.index00-based iteration counter
    loop.firsttrue on the first iteration
    loop.lasttrue on the last iteration
    loop.lengthtotal number of items

    #Macros

    Define reusable template fragments:

    {% macro input(name, type="text", value="") %} <input type="{{ type }}" name="{{ name }}" value="{{ value }}"> {% endmacro %} {{ input("username") }} {{ input("password", type="password") }}

    #Includes

    Include another template:

    {% include "header.html" %} <main>content</main> {% include "footer.html" %}

    Templates are loaded via TemplateLoader:

    let loader = @template.TemplateLoader::new()
    .add("header", "<header>{{ title }}</header>")
    .add("footer", "<footer>© 2026</footer>")
    let result = tpl.render_with_loader(ctx, loader)?

    #Template Inheritance (planned)

    {# 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 %}

    #Comments

    {# This is a comment and will not appear in the output #}

    #Built-in Filters

    FilterDescriptionExample
    upperUppercase string"hello""HELLO"
    lowerLowercase string"HELLO""hello"
    capitalizeCapitalize first character"hello""Hello"
    trimRemove leading/trailing whitespace" hi ""hi"
    lengthLength of string/array/object"abc"3
    reverseReverse string or array"abc""cba"
    firstFirst character/element"abc""a"
    lastLast 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"
    intConvert to integer"42"|int42
    stringConvert to string42|string"42"

    #Custom Filters

    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")
    }
    })

    #API Reference

    #Template

    MethodSignatureDescription
    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

    #Context

    MethodDescription
    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

    #Value

    VariantType
    Nullnull
    Bool(Bool)boolean
    Int(Int64)integer
    Float(Double)float
    Str(String)string
    Array(Array[Value])array
    Object(Map[String, Value])object (for member access)

    #TemplateError

    Unified error type with source location:

    match Template::parse(source) {
    Ok(t) => ...
    Err(err) => {
    println(err.to_string())
    // Error: SyntaxError in template "<source>" at line 3, column 8
    // expected '}}'
    }
    }

    #CLI Tool

    MoonTera includes a command-line tool for quick template rendering:

    # 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

    CommandDescription
    render "<tpl>" [--data '<json>']Render template with optional JSON data
    check "<tpl>"Validate template syntax without rendering
    --versionShow version information
    --helpShow help message

    The --data argument accepts a JSON object whose top-level keys become template context variables.

    #Project Structure

    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 types

    #Examples

    See the examples/ directory:

    • hello.mbt — Minimal Hello World
    • blog.mbt — Blog page with loops and conditionals
    • email.mbt — Email template with macros and includes

    #Development

    #Running Tests

    moon test

    #Project Status

    FeatureStatus
    Variables & expressions
    Filters (13 built-in)
    Conditionals (if/elif/else)
    For loops with loop.*
    Macros
    Include
    Template inheritance🚧 Planned
    Auto-escaping🚧 Planned
    Custom filters

    #License

    MIT License. See LICENSE for details.

    BinOpKind

    pub(all) enum BinOpKind {
    Add
    Sub
    Mul
    Div
    Mod
    Eq
    Neq
    Lt
    Gt
    Le
    Ge
    And
    Or
    } derive(
    Debug
    )

    Binary operator kinds.

    Context

    pub(all) struct Context {
    data : Map[String, Value]
    macros : Map[String, MacroDef]
    parent : Context?
    } derive(
    Debug
    )

    A rendering context — stores variables and macros in a nested-scope chain.

    Context::get

    fn Context::get(self : Context, name : String) -> Value?

    Look up name in the scope chain (current → parent → …).

    Context::get_macro

    fn Context::get_macro(self : Context, name : String) -> MacroDef?

    Look up a macro by name, walking the scope chain.

    Context::new

    fn Context::new() -> Context

    Create an empty root context.

    Context::new_child

    fn Context::new_child(parent : Context) -> Context

    Create a child context that delegates lookups to parent.

    Context::register_macro

    fn Context::register_macro(self : Context, m : MacroDef) -> Context

    Register a macro definition in the current scope.

    Context::set

    fn Context::set(self : Context, name : String, value : Value) -> Context

    Set name = value in the current scope (does not affect parent).

    ElifBranch

    pub(all) struct ElifBranch {
    condition : Expr?
    body : Array[Node]
    } derive(
    Debug
    )

    A single elif (or else) branch inside an {% if %} block.

    condition = None represents the final {% else %} clause.

    ErrorKind

    pub(all) enum ErrorKind {
    LexicalError
    SyntaxError
    EvalError
    RenderError
    FilterError
    NotFoundError
    } derive(
    Debug
    )

    Classification of template engine errors.

    Evaluator

    pub(all) struct Evaluator {
    ctx : Context
    filters : FilterRegistry
    }

    Evaluator::eval

    fn Evaluator::eval(self : Evaluator, expr : Expr) -> Result[Value, String]

    Evaluator::new

    fn Evaluator::new(ctx : Context) -> Evaluator

    Evaluator::new_with_filters

    fn Evaluator::new_with_filters(ctx : Context, filters : FilterRegistry) -> Evaluator
    Create an evaluator with a specific filter registry.

    Expr

    pub(all) enum Expr {
    Literal(Value)
    Variable(String)
    Member(Expr, String)
    Index(Expr, Expr)
    Call(String, Array[Expr])
    BinOp(Expr, BinOpKind, Expr)
    UnaryOp(UnaryOpKind, Expr)
    Filter(Expr, String, Array[Expr])
    } derive(
    Debug
    )

    Expression node — evaluates to a Value at render time.

    FilterRegistry

    pub(all) struct FilterRegistry {
    filters : Map[String, (Value, Array[Value]) -> Result[Value, String]]
    }
    Registry mapping filter names to their implementations.

    FilterRegistry::default

    Create a registry pre-populated with all built-in filters.

    FilterRegistry::lookup

    fn FilterRegistry::lookup(self : FilterRegistry, name : String) -> (Value, Array[Value]) -> Result[Value, String]?
    Look up a filter by name.

    FilterRegistry::new

    Create an empty registry.

    FilterRegistry::register

    fn FilterRegistry::register(self : FilterRegistry, name : String, f : (Value, Array[Value]) -> Result[Value, String]) -> FilterRegistry
    Register a filter function under name.

    MacroDef

    pub(all) struct MacroDef {
    name : String
    params : Array[String]
    body : Array[Node]
    } derive(
    Debug
    )

    A macro definition — stored in the rendering context.

    Node

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

    Template node — a top-level statement inside a template.

    Parser

    pub(all) struct Parser {
    tokens : Array[Token]
    pos : Int
    errors : Array[String]
    } derive(
    Debug
    )

    Parser state — holds the token stream and current position.

    All methods that modify state return a (result, Parser) pair so the caller always has the latest state without needing mutable references.

    Parser::advance

    fn Parser::advance(self : Parser) -> (Token?, Parser)

    Consume and return the current token, advancing the position. Returns None if already at EOF.

    Parser::current_pos

    fn Parser::current_pos(self : Parser) -> Int

    Return the current position in the token stream (0-indexed).

    Parser::error

    fn Parser::error(self : Parser, msg : String) -> Parser

    Record a parse error without consuming input.

    Parser::expect

    fn Parser::expect(self : Parser, check : (Token) -> Bool, message : String) -> Result[(Token, Parser), String]

    Consume the current token if it satisfies check, otherwise record an error and return Err(message).

    Parser::get_errors

    fn Parser::get_errors(self : Parser) -> Array[String]

    Return all accumulated parse errors.

    Parser::make_error

    fn Parser::make_error(self : Parser, kind : ErrorKind, msg : String) -> TemplateError

    Create a TemplateError positioned at the current token.

    Parser::new

    fn Parser::new(tokens : Array[Token]) -> Parser

    Create a new parser from a token array (typically produced by tokenize).

    Parser::parse_expression

    fn Parser::parse_expression(self : Parser, min_bp : Int) -> Result[(Expr, Parser), String]

    Parse an expression with the given minimum binding power (Pratt parser).

    Parser::parse_template

    fn Parser::parse_template(self : Parser) -> Result[(Array[Node], Parser), String]

    Parse the complete token stream into a list of template Nodes.

    Parser::peek

    fn Parser::peek(self : Parser) -> Token?

    Return the token at the current position, or None if at EOF.

    Parser::peek_at

    fn Parser::peek_at(self : Parser, offset : Int) -> Token?

    Look ahead offset tokens from the current position. peek_at(0) is equivalent to peek().

    Renderer

    pub(all) struct Renderer {
    evaluator : Evaluator
    output : StringBuilder
    loader : TemplateLoader
    }

    Template renderer — converts Node trees into a string.

    Renderer::new

    fn Renderer::new(ctx : Context, loader : TemplateLoader) -> Renderer

    Create a new renderer with the given ctx and loader.

    Renderer::render

    fn Renderer::render(self : Renderer, nodes : Array[Node]) -> Result[String, String]

    Render a list of nodes and return the final output string.

    Renderer::render_node

    fn Renderer::render_node(self : Renderer, node : Node) -> Result[Renderer, String]

    Render a single node into the output buffer.

    SourceLocation

    pub(all) struct SourceLocation {
    template : String
    line : Int
    column : Int
    } derive(
    Debug
    )

    A position in a template source file.

    Template

    pub(all) struct Template {
    nodes : Array[Node]
    } derive(
    Debug
    )

    Template::parse

    fn Template::parse(source : String) -> Result[Template, TemplateError]

    Parse a template source string into a Template. Returns TemplateError with location info on failure.

    Template::render

    fn Template::render(self : Template, ctx : Context) -> Result[String, TemplateError]

    Render with empty loader (no includes).

    Template::render_with_loader

    fn Template::render_with_loader(self : Template, ctx : Context, loader : TemplateLoader) -> Result[String, TemplateError]

    Render with a specific loader for {% include %} support.

    TemplateError

    pub(all) struct TemplateError {
    kind : ErrorKind
    message : String
    location : SourceLocation?
    snippet : String?
    } derive(
    Debug
    )

    The primary error type returned by all engine operations.

    TemplateError::new

    fn TemplateError::new(kind : ErrorKind, message : String) -> TemplateError
    Create a bare error without location info.

    TemplateError::to_string

    fn TemplateError::to_string(self : TemplateError) -> String

    Format the error as a human-readable string.

    TemplateError::with_location

    fn TemplateError::with_location(self : TemplateError, loc : SourceLocation) -> TemplateError
    Attach source location to an error.

    TemplateError::with_snippet

    fn TemplateError::with_snippet(self : TemplateError, snippet : String) -> TemplateError
    Attach a source snippet to an error.

    TemplateLoader

    pub(all) struct TemplateLoader {
    sources : Map[String, String]
    }

    A simple in-memory template source registry.

    TemplateLoader::add

    fn TemplateLoader::add(self : TemplateLoader, name : String, source : String) -> TemplateLoader

    Register a named template source.

    TemplateLoader::get

    fn TemplateLoader::get(self : TemplateLoader, name : String) -> String?

    Look up a template source by name.

    TemplateLoader::new

    Create an empty loader.

    Token

    pub(all) struct Token {
    kind : TokenKind
    line : Int
    col : Int
    } derive(
    Debug
    )

    A token together with its source position (1-based line / column).

    TokenKind

    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
    )

    Every kind of token the lexer can produce.

    UnaryOpKind

    pub(all) enum UnaryOpKind {
    Not
    Neg
    } derive(
    Debug
    )

    Unary operator kinds.

    Value

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

    Runtime value — used for template context data and literal values.

    Value::is_truthy

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

    Test whether a value is "truthy" (for use in if conditions).

    Value::to_string

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

    Convert a Value to its string representation (for output).

    Value::type_name

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

    Return a human-readable type name for this value.

    VERSION

    let VERSION : String

    Version of the template engine.

    eval_error

    fn eval_error(msg : String) -> TemplateError
    Create an eval error (often without precise location).

    extract_snippet

    fn extract_snippet(source : String, line : Int) -> String

    Extract a single line from source for error display.

    Returns a formatted string like: 5 | {% if x > %}.

    filter_error

    fn filter_error(msg : String) -> TemplateError
    Create a filter error.

    from_string

    fn from_string(msg : String) -> TemplateError
    Wrap a plain string into a TemplateError (for gradual migration).

    lex_error

    fn lex_error(msg : String, line : Int, col : Int) -> TemplateError
    Create a lexer error at a source position.

    loc_at

    fn loc_at(line : Int, column : Int) -> SourceLocation
    Create a source location with default template name.

    not_found_error

    fn not_found_error(msg : String) -> TemplateError
    Create a not-found error (template, variable, field, key).

    render_error

    fn render_error(msg : String) -> TemplateError
    Create a render error.

    syntax_error

    fn syntax_error(msg : String, line : Int, col : Int) -> TemplateError
    Create a syntax error at a source position.

    tokenize

    fn tokenize(input : String) -> Result[Array[Token], String]

    Tokenise input into a flat Array[Token].

    Comment blocks ({# … #}) are silently consumed — they never appear in the returned token stream. Recoverable errors (e.g. unclosed string or unknown character) are represented as TokenKind::Error tokens so the parser can report multiple problems in a single pass.