- Erlang 100%
Replace the inline node-less checkout and rebar3 build/test steps with the shared composite actions fakehacker/actions/checkout and rebar3-ci. The matrix and erlang container stay in the workflow since composite actions can't declare them. Reviewed-on: #1 |
||
|---|---|---|
| .forgejo/workflows | ||
| include | ||
| src | ||
| test | ||
| .gitignore | ||
| CHANGELOG.md | ||
| LICENSE | ||
| README.md | ||
| rebar.config | ||
| rebar.lock | ||
erlstache
A spec-compliant Mustache templating engine for Erlang.
erlstache implements the entire Mustache specification v1.4.3 — all six core modules and all three optional modules (lambdas, dynamic names, and template inheritance). Every test case in the specification is executed against the engine; see Conformance.
Highlights
- Complete v1.4.3 conformance. 194/194 spec cases pass, including the notoriously tricky inheritance re-indentation cases that most engines skip.
- Hand-written, non-regex parser. No
leex, noyecc, no regular expressions. The scanner walks the template withbinary:match/2and slices withbinary:part/3, so no character data is copied while scanning. - Zero runtime dependencies. The library depends only on
kernelandstdlib. - Unambiguous data model. Maps are hashes, lists are arrays, binaries are strings — no proplist-or-hash or string-or-list guessing.
- Compiled templates. Parse once, render many times.
Installation
erlstache requires Erlang/OTP 27 or newer (it uses the EEP-48 documentation
attributes and the float_to_binary/2 short form).
rebar3
%% rebar.config
{deps, [
{erlstache, {git, "https://fakehacker.cc/dxtr/erlstache", {branch, "main"}}}
]}.
Mix / Elixir
# mix.exs
{:erlstache, git: "https://fakehacker.cc/dxtr/erlstache", branch: "main"}
Usage
1> erlstache:render(<<"Hello, {{name}}!">>, #{<<"name">> => <<"world">>}).
<<"Hello, world!">>
Compiling once, rendering many times
1> {ok, T} = erlstache:parse(<<"{{#items}}- {{.}}\n{{/items}}">>).
2> erlstache:render_template(T, #{<<"items">> => [<<"a">>, <<"b">>]}).
<<"- a\n- b\n">>
Partials
Partials are supplied via the partials option, as template source or as a
compiled template:
1> erlstache:render(
.. <<"{{>greeting}}">>,
.. #{<<"who">> => <<"there">>},
.. #{partials => #{<<"greeting">> => <<"Hi {{who}}!">>}}).
<<"Hi there!">>
Data model
| Mustache value | Erlang term |
|---|---|
| hash / object | a map (keys may be binaries, atoms or strings) |
| list / array | a list (always iterated by a section) |
| string | a binary |
| number | an integer or float |
| boolean | true / false |
| null | the atom null (or undefined) |
| lambda | a fun |
false, null, undefined and the empty list [] are falsey. Every other
value — including 0, 0.0 and the empty binary — is truthy.
Strings must be binaries and hashes must be maps. This avoids the
classic Erlang ambiguities (is "foo" a string or a list of integers? is
[{a, 1}] a hash or a one-element list?). Map keys are matched against the
tag name as a binary first, then as an atom, then as a string.
Lambdas
A fun in the context is a lambda:
- Interpolation (
{{fn}}): the fun takes no arguments. Its result is parsed with the default delimiters, rendered, and interpolated. - Section (
{{#fn}}…{{/fn}}): the fun takes one argument — the raw, unprocessed section body as a binary. Its result is parsed with the delimiters active at the section, rendered, and interpolated.
1> Bold = fun(Body) -> <<"<b>", Body/binary, "</b>">> end,
1> erlstache:render(<<"{{#bold}}{{x}}{{/bold}}">>,
.. #{<<"bold">> => Bold, <<"x">> => <<"hi">>}).
<<"<b>hi</b>">>
Lambdas may return a binary, an iolist/charlist, a number or an atom.
Template inheritance
erlstache implements the optional inheritance module — parent templates
({{<parent}}) and overridable blocks ({{$block}}…{{/block}}):
1> Partials = #{<<"layout">> => <<"<h1>{{$title}}Untitled{{/title}}</h1>">>},
1> erlstache:render(
.. <<"{{<layout}}{{$title}}Home{{/title}}{{/layout}}">>, #{},
.. #{partials => Partials}).
<<"<h1>Home</h1>">>
Options
render/3 and render_template/3 take an options map:
| Key | Type | Default |
|---|---|---|
partials |
#{binary() => iodata() | template()} |
#{} |
escape |
fun((binary()) -> binary()) |
HTML escaping (& < > " ') |
%% Render without any HTML escaping:
erlstache:render(Template, Data, #{escape => fun(B) -> B end}).
Error handling
parse/1returns{ok, Template}or{error, Reason}— use it for templates from untrusted or dynamic sources.render/2,3raiseerror({erlstache, Reason})if a template (or partial) fails to parse.
Reason is one of {scan, {unclosed_tag, Pos}},
{scan, {bad_delimiters, Raw}}, {parse, {unclosed_section, Name}},
{parse, {mismatched_close_tag, Open, Close}} or
{parse, {unexpected_close_tag, Name}}.
Architecture
| Module | Responsibility |
|---|---|
erlstache |
Public API. |
erlstache_scanner |
Delimiter-aware tokenizer (no regex, no leex/yecc). |
erlstache_parser |
Standalone-line trimming and nesting into an AST. |
erlstache_render |
AST + context → output, including indentation & lambdas. |
erlstache_data |
Context-stack resolution, truthiness, escaping. |
Because Set Delimiter tags can change the delimiters mid-template, Mustache is not a static grammar — which is exactly why a hand-written, delimiter-aware scanner is used instead of generated parser tables.
Conformance
The full v1.4.3 specification is vendored under test/spec/ and run as the
test suite. The eight data-driven spec files are executed via
yamerl (a test-only dependency); the
~lambdas.yml cases are ported in erlstache_lambda_tests because their lambda
bodies are embedded as source in other languages and cannot be loaded as data.
$ rebar3 eunit
…
All 235 tests passed.
In addition to the spec and unit tests, the suite includes property-based tests
(PropEr) covering parser robustness
against random input, escaping invariants, agreement between the rendering entry
points, and delimiter independence across randomly generated templates. They run
as part of rebar3 eunit.
Building and testing
rebar3 compile # compile (warnings are errors)
rebar3 eunit # run the spec, unit and property tests
rebar3 dialyzer # type analysis
rebar3 xref # cross-reference checks
rebar3 ex_doc # generate HTML documentation
Author
dxtr <dxtr@fakehacker.cc> — https://fakehacker.cc/dxtr/erlstache
License
MIT © 2026 dxtr. See LICENSE.