View Source Changelog
All notable changes to this project are documented in this file, following Keep a Changelog.
[0.1.0] - 2026-08-29
Added
- Lexer (
Alembic.Lexer) — recursive binary-pattern-matching tokenizer; text/output/tag tokens, each carrying aline/colposition (Alembic.Token.position/1); whitespace control ({{-,-}},{%-,-%});{% comment %}and{% raw %}blocks; UTF-8 correct; structured errors with line/column positions. - Parser (
Alembic.Parser,Alembic.Parser.Expression) — recursive descent parser producingAlembic.AST.t(); full expression grammar (variable paths, literals, comparison/logical operators withnot > and > orprecedence, filter chains);{% if %}/{% elsif %}/{% else %},{% for %}/{% else %},{% assign %},{% extends %}/{% block %},{% include %}.parse/1reports the first error found;{:unexpected_token, token, position}and{:missing_end_tag, tag_name, position}(the position of the opening tag, since a missing close often means there's no closing tag anywhere to point at) both carry a location.parse_all/1instead keeps going after an error — best-effort skip-to-next-tag-or-output recovery — to report every independent problem found in one pass, returning{:ok, ast}or{:error, [reason(), ...]}. - Evaluator (
Alembic.Evaluator) — tree-walking interpreter with iolist output accumulation; Liquid truthiness (0/""/[]truthy, onlynil/falsefalsy);{% assign %}visible to every later node at the same level, including across{% for %}/{% if %}boundaries and across an{% include %}boundary; optionalstrict: truemode erroring on undefined variables. - Context (
Alembic.Context) — scoped symbol table;push_scope/2/pop_scope/1; dot/bracket path resolution across maps, keyword lists, and list indices;forloopmetadata. - Filters (
Alembic.Filters,Alembic.Filter) — full built-in string, array, number, and misc filter catalog, each with type-coercion behavior documented inAlembic.Filters' moduledoc (a reference table, since the individual filter clauses are private and can't carry their own@doc); custom filter registration globally viaconfig :alembic, custom_filters: [...]or per call viarender/3'scustom_filters:option (per-call modules take precedence on a name collision). - Template inheritance (
Alembic.Inheritance) — multi-level{% extends %}chains,{{ block.super }}, circular- and max-depth-inheritance detection. - File loader (
Alembic.Loader) — multi-root resolution, automatic extension appending, path-traversal protection. - Cache (
Alembic.Cache) — ETS-backed compiled-template cache keyed by{path, mtime}; concurrent lock-free reads;sweep/0for pruning stale entries;cache: falseper-call bypass; the ETS table is recreated automatically if the cache's GenServer is ever restarted by its supervisor. - Public API (
Alembic) —compile/2,render/3,render_string/3,render_file/3, and!variants wired through the full pipeline;Alembic.TemplateError,Alembic.CompileError,Alembic.RenderError. docs/grammar.md— formal EBNF grammar, LL(1) analysis, worked parse tree, and documented grammar ambiguity resolutions.COMPATIBILITY.md— supported/deviating/unsupported/extension features relative to upstream Liquid (including thatsliceoperates on strings only, not arrays).BENCHMARKS.md— lexer, full-pipeline, cache hit/miss, iolist-vs-concat, and filter-chain benchmarks, including a documented finding that{% include %}partials are recompiled on every render, uncached, independent of the outer template's cache status.
Notes
- Zero runtime dependencies —
ex_doc,credo,dialyxir, andbencheeare alldev/test-only. - Test suite: 448 tests + 62 doctests, 91.15% coverage,
mix credo --strictandmix dialyzerboth clean.