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,{% assign %}, comparison and logical operators. 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. - Partials —
{% include %}with optional variable passing. - 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.
Alembic and Grimoire
Alembic is developed as an independent library, intended as the template engine powering Grimoire, a static site generator. During local development, Grimoire can reference Alembic as a path dependency:
defp deps do
[
{:alembic, path: "../alembic"}
]
end
This separation keeps template compilation and rendering independent from the static site generator's file discovery, content processing, routing, and output generation:
Grimoire
│
├── discovers source files
├── loads content and metadata
├── selects templates
│
└── calls Alembic
├── compile/2
├── render/3
└── render_file/3
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.