View Source Alembic

Hex.pm License: MIT

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 timescompile/2 and render/3 are separate steps, so a caller can parse a template once and reuse the AST. See Alembic.
  • Full control flow{% if %} / {% elsif %} / {% else %}, {% for %} with forloop metadata, {% assign %}, comparison and logical operators. See Alembic.Parser and docs/grammar.md.
  • Built-in filter library — the full Liquid string/array/number/misc filter catalog, plus custom filter registration. See Alembic.Filters and Alembic.Filter.
  • Template inheritance — multi-level {% extends %} / {% block %} chains with {{ block.super }}. See Alembic.Inheritance.
  • Partials{% include %} with optional variable passing.
  • ETS-backed compiled-template cache — automatic mtime-based invalidation, cache: false per-call bypass. See Alembic.Cache.
  • Strict modestrict: true errors 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
KeyTypeDefaultDescription
: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
:cacheboolean()trueEnable/disable the compiled-AST cache
:custom_filters[module()][]Modules implementing Alembic.Filter
:max_inheritance_depthpos_integer()10Max {% 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.