Alembic
A Liquid-compatible template engine for Elixir, with zero runtime dependencies — built entirely on Elixir and OTP standard library functionality.
Quick start
# 1. Add the dependency (the OTP app is :alembic; the Hex package name is
# alembic_template_engine, so the `hex:` key is required)
def deps do
[{:alembic, "~> 0.1.0", hex: "alembic_template_engine"}]
end
# 2. Configure template roots (config/config.exs) — only needed for render_file/3
config :alembic, template_roots: ["priv/templates"]
# 3. Render
Alembic.render_string("Hello, {{ name }}!", %{"name" => "World"})
#=> {:ok, "Hello, World!"}
Alembic.render_file("index.html", %{"title" => "Home"})
#=> {:ok, "<html>...</html>"}
Features
- Compile once, render many times —
compile/2andrender/3are separate steps, so a caller can parse a template once and reuse the AST. SeeAlembic. - Full control flow —
{% if %}/{% elsif %}/{% else %},{% for %}withforloopmetadata and{% break %}/{% continue %},{% unless %},{% case %}/{% when %},{% capture %},{% cycle %}, range iterables ({% for i in (1..5) %}),{% assign %}, comparison and logical operators (including theempty/blankkeywords). SeeAlembic.Parseranddocs/grammar.md. - Built-in filter library — the full Liquid string/array/number/misc
filter catalog, plus custom filter registration. See
Alembic.FiltersandAlembic.Filter. - Template inheritance — multi-level
{% extends %}/{% block %}chains with{{ block.super }}. SeeAlembic.Inheritance. - Output expressions —
{{ user.name }},{{ items[key] }}dynamic bracket access, and literal/filter bases such as{{ 42 }}or{{ "hi" | upcase }}. - Partials — shared-scope
{% include %}with optional variable passing, and isolated-scope{% render %}(only explicitly passed variables are visible). - ETS-backed compiled-template cache — automatic mtime-based
invalidation,
cache: falseper-call bypass. SeeAlembic.Cache. - Strict mode —
strict: trueerrors on undefined variables instead of silently rendering"", useful in development. - Path-traversal-safe file loading across multiple template roots. See
Alembic.Loader.
See COMPATIBILITY.md for the full picture of what's
supported, what intentionally deviates from upstream Liquid, and what's out
of scope for this release.
Configuration
# config/config.exs
config :alembic,
template_roots: ["priv/templates"],
template_extensions: [".html", ".liquid"],
cache: true,
custom_filters: [],
max_inheritance_depth: 10
| Key | Type | Default | Description |
|---|---|---|---|
:template_roots |
[String.t()] |
[] |
Directories searched, in order, by render_file/3 |
:template_extensions |
[String.t()] |
[".html", ".liquid"] |
Extensions tried when a name has none |
:cache |
boolean() |
true |
Enable/disable the compiled-AST cache |
:custom_filters |
[module()] |
[] |
Modules implementing Alembic.Filter |
:max_inheritance_depth |
pos_integer() |
10 |
Max {% extends %} chain length |
Every key has a per-call override too — see the options table in
Alembic's moduledoc.
Custom filters
defmodule MyApp.Filters.Money do
@behaviour Alembic.Filter
@impl true
def name, do: "money"
@impl true
def apply(cents, []) when is_integer(cents) do
{:ok, "$" <> :erlang.float_to_binary(cents / 100, decimals: 2)}
end
end
config :alembic, custom_filters: [MyApp.Filters.Money]
{{ price_cents | money }} {# => "$19.99" #}
Dependency policy
Alembic has zero runtime dependencies. Development tooling
(ex_doc, credo, dialyxir, benchee) is runtime: false and never
becomes part of the dependency graph of an application that depends on
Alembic.
Development
mix deps.get # install dependencies
mix test # run the test suite (unit + integration + doctests)
mix test --cover # with coverage report
mix format --check-formatted
mix credo --strict # static analysis
mix dialyzer # type checking
mix docs # generate documentation
mix run bench/*.exs # run a benchmark script (see BENCHMARKS.md)
mix hex.build # build the Hex package locally
Contributing
Issues and pull requests are welcome. Before opening a PR, please make sure
mix test, mix credo --strict, and mix dialyzer all pass.
License
MIT — see LICENSE.