@eryx/template Module
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.
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:
<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
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.
Parameters
The template source string to compile.
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.
Parameters
The template source string to compile.
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" }))
Parameters
The AST produced by compile.
Optional context table whose keys become template variables.
Optional table of custom filter functions.
Returns
The fully rendered template output.
Like evaluate, but propagates a TemplateError table via error()
instead of a formatted string.
Parameters
The AST produced by compile or compileRaw.
Optional context table whose keys become template variables.
Optional table of custom filter functions.
Returns
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.
Implements: Parser.TemplateError
Implements: Parser.AstSuite