@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
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" }))
Summary
Functions
template.evaluate(nodes: Template, ctx: { [string]: any }?, filters: { [string]: ((any, { [string]: any }) → any) }?) → string
API Reference
Functions
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
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
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" }))
template.evaluate(nodes: Template, ctx: { [string]: any }?, filters: { [string]: ((any, { [string]: any }) → any) }?) → string
Parameters
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.
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
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
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.