@eryx/template Module

JSON

Jinja-inspired template engine for Luau.

Compiles template source strings into an AST, then evaluates them against a context table to produce rendered output.

Template Syntax

Expressions - {{ expr }}

Outputs the value of an expression, coerced to a string.

{{ name }}              -- variable lookup from context
{{ user.email }}        -- nested/dotted property access
{{ 42 }}               -- numeric literal (integer or decimal)
{{ "hello" }}           -- string literal (supports \n \t \r \\ \")
{{ true }}              -- boolean constant (renders as "true")
{{ false }}             -- boolean constant (renders as "false")
{{ not logged_in }}     -- unary not (negates truthiness)
{{ items[i] }}          -- subscript lookup with a variable key
{{ data["key"] }}       -- subscript lookup with a string key
{{ name | length }}     -- pipe a value through a filter
{{ x | filter1 | f2 }}  -- chain multiple filters left-to-right

Comments - {# ... #}

Ignored by the engine; produces no output.

{# This is a comment #}

Conditionals - {% if … then %}

Conditional blocks. The condition is truthy/falsy (nil and false are falsy; everything else is truthy).

{% if logged_in then %}Welcome!{% end %}

{% if count == 0 then %}
  Empty
{% elseif count < 5 then %}
  A few
{% else %}
  Many
{% end %}

Supported comparison operators: ==, ~=, <, >, <=, >=.

Conditions can be chained with and / or:

{% if role == "admin" and active then %}...{% end %}
{% if x or y then %}...{% end %}

A bare expression (no operator) is treated as a truthiness check:

{% if user then %}...{% end %}
{% if true then %}...{% end %}
{% if not logged_in then %}...{% end %}

Table For-Loop - {% for k, v in tbl do %}

Iterates over key/value pairs of a table expression. Both loop variables are injected into the context and restored after the loop.

{% for i, item in items do %}
  {{ i }}: {{ item.name }}
{% end %}

Numeric For-Loop - {% for i = start, end do %}

Counts from start to end (inclusive). The loop variable is injected into the context and restored after the loop.

{% for i = 1, 5 do %}
  Row {{ i }}
{% end %}

Macros - {% macro name(args) %}

Defines a reusable template block. Arguments become context variables within the macro body.

{% macro greeting(name, title) %}
  Hello, {{ title }} {{ name }}!
{% end %}

Call a macro with {% use %} to provide a body, or {{ }} for a simple inline call:

{% use greeting("Alice", "Dr.") %}
  <p>Extra content here</p>
{% end %}

Inside a macro, {% slot default %} renders the caller's body. Named slots can also be used - see Slots below.

Slots - {% slot name %} / {% fill name %}

Slots define placeholder regions inside a macro body that callers can fill with custom content. Each slot has a default body that is used when the caller does not provide a {% fill %} for it.

{% macro card(title) %}
  <div class="card">
    <h2>{{ title }}</h2>
    {% slot body %}<p>Default body</p>{% end %}
    {% slot footer %}<small>Default footer</small>{% end %}
  </div>
{% end %}

{% use card("My Card") %}
  {% fill body %}<p>Custom body!</p>{% end %}
  {% fill footer %}<small>Custom footer</small>{% end %}
{% end %}

The special slot name "default" is reserved and automatically receives the caller's top-level body content (anything not inside a {% fill %}). You cannot explicitly name a slot "default".

Includes - {% include "path" %}

Inlines another template file at parse time. The path is resolved relative to the directory of the current template file.

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

Includes accept a body block between {% include %} and {% end %}. The body content is available to the included file as the "default" slot, and named slots can be filled with {% fill %} - just like macro calls.

{% include "layout.html" %}
  {% fill sidebar %}<nav>Custom nav</nav>{% end %}
  {% fill content %}<p>Page body here</p>{% end %}
{% end %}

Inside the included file, use {% slot %} to define the placeholder regions that callers can fill:

{# layout.html #}
<div class="sidebar">{% slot sidebar %}<nav>Default nav</nav>{% end %}</div>
<div class="content">{% slot content %}<p>Default content</p>{% end %}</div>

Caution

A file path must be provided to compile for includes to work.

Literal Text

Anything outside {{ }}, {% %}, and {# #} delimiters is emitted as-is.

Built-in Filters

String Filters

Filter Description
length Returns #tbl for tables, or string.len for everything else.
upper Converts to uppercase.
lower Converts to lowercase.
trim Strips leading and trailing whitespace.
capitalize Capitalizes the first letter, lowercases the rest.
title Capitalizes the first letter of each word.
replace Replaces occurrences using replace_from and replace_to context variables.

Collection Filters

Filter Description
first Returns the first element of an array.
last Returns the last element of an array.
join Joins array elements with join_separator (default ", ").
reverse Reverses an array or string.
keys Returns the keys of a table as an array.
values Returns the values of a table as an array.
sort Returns a sorted copy of an array (lexicographic).

Numeric Filters

Filter Description
abs Returns the absolute value.
round Rounds to the nearest integer.
floor Rounds down.
ceil Rounds up.

Utility Filters

Filter Description
default Returns default_value from context if value is nil/false (defaults to "").
json Encodes the value as a JSON string.
type Returns the Luau type name of the value.

Error Handling

compile and evaluate are the standard entry points. On failure they call error() with a human-readable string (e.g. "3:7: Unknown filter 'foo'").

If you want the structured TemplateError table instead, call the Raw variants (compileRaw / evaluateRaw), which propagate the table directly so you can inspect .message, .line, .col, and .filepath yourself.

Example

local template = require("@eryx/template")

local tmpl = template.compile("Hello, {{ name | length }}!")
print(template.evaluate(tmpl, { name = "World" })) --> "Hello, 5!"

Summary

Functions

template.compile(source: string, filepath: string?)Template
template.compileRaw(source: string, filepath: string?)Template
template.evaluate(nodes: Template, ctx: { [string]: any }?, filters: { [string]: ((any, { [string]: any }) → any) }?)string
template.evaluateRaw(nodes: Template, ctx: { [string]: any }?, filters: { [string]: ((any, { [string]: any }) → any) }?)string

API Reference

Functions

template.compile

Parses a template source string into a list of AST nodes.

The returned nodes can be passed to evaluate one or more times with different context tables, avoiding repeated parsing.

On failure, calls error() with a formatted string such as "myfile.html:3:7: Unterminated string literal". Use compileRaw if you need the structured TemplateError table.

template.compile(source: string, filepath: string?)Template

Parameters

source: string

The template source string to compile.

filepath: string?

Optional file path of the template, required for {% include %} resolution.

Returns

The compiled AST.

template.compileRaw

Like compile, but propagates a TemplateError table via error() instead of a formatted string. Use this when you want to inspect the structured error (.message, .line, .col, .filepath) rather than wrapping the call in pcall yourself just to re-format the message.

template.compileRaw(source: string, filepath: string?)Template

Parameters

source: string

The template source string to compile.

filepath: string?

Optional file path of the template.

Returns

The compiled AST.

template.evaluate

Renders a compiled template (array of AST nodes) into a string.

Custom filters can be supplied and will be merged with (and can override) the built-in filter set.

On failure, calls error() with a formatted string such as "myfile.html:5:12: Unknown filter 'foo'". Use evaluateRaw if you need the structured TemplateError table.

local nodes = template.compile("Hello, {{ name }}!")
print(template.evaluate(nodes, { name = "World" })) --> "Hello, World!"
template.evaluate(nodes: Template, ctx: { [string]: any }?, filters: { [string]: ((any, { [string]: any }) → any) }?)string

Parameters

nodes: Template

The AST produced by compile.

ctx: { [string]: any }?

Optional context table whose keys become template variables.

filters: { [string]: ((any, { [string]: any }) → any) }?

Optional table of custom filter functions.

Returns

string

The fully rendered template output.

template.evaluateRaw

Like evaluate, but propagates a TemplateError table via error() instead of a formatted string.

template.evaluateRaw(nodes: Template, ctx: { [string]: any }?, filters: { [string]: ((any, { [string]: any }) → any) }?)string

Parameters

nodes: Template

The AST produced by compile or compileRaw.

ctx: { [string]: any }?

Optional context table whose keys become template variables.

filters: { [string]: ((any, { [string]: any }) → any) }?

Optional table of custom filter functions.

Returns

string

The fully rendered template output.

Types

TemplateError

A structured error value thrown by compileRaw and evaluateRaw.

Re-exported from the parser so consumers only need to import this module when working with structured errors.

Template

type Template = Parser.AstSuite