View Source Liquid Compatibility
Alembic implements a Liquid-compatible subset of the template language. This document lists what's supported, where Alembic intentionally deviates from upstream Liquid, what's entirely out of scope for this MVP, and what Alembic adds beyond Liquid.
Note on methodology: this session had no network access to fetch and port
Shopify's actual github.com/Shopify/liquid test-suite fixtures. Coverage
of the same categories the official suite exercises (variables, if/for,
assign, filters, whitespace control, comments, raw blocks) is instead
hand-written in test/integration/liquid_compat_test.exs.
Supported Liquid features
- Output tags —
{{ user.name }}, dot-path and bracket (user["name"]) variable access, both interchangeable. Filters — full pipe chain syntax (
{{ x | a | b: 1, 2 }}); seeAlembic.Filtersfor the complete catalog (string, array, number, misc).- Control flow —
{% if %}/{% elsif %}/{% else %}/{% endif %},{% for %}/{% else %}/{% endfor %}with fullforloopmetadata (index,index0,rindex,rindex0,first,last,length). - Operators —
==,!=,>,<,>=,<=,contains,and,or,not, withnot > and > orprecedence (seedocs/grammar.md§5.4). - Assignment —
{% assign var = expr %}, visible to every node evaluated after it, including across{% for %}/{% if %}boundaries. - Whitespace control —
{{-,-}},{%-,-%}in any combination. - Comments —
{% comment %} ... {% endcomment %}, content fully discarded, including anything that looks like Liquid syntax inside it. - Raw blocks —
{% raw %} ... {% endraw %}, content preserved verbatim as plain text. - Template inheritance —
{% extends %}/{% block %}/{% endblock %}, multi-level chains,{{ block.super }}, circular- and max-depth detection. - Includes —
{% include "partial.html" %}and{% include "partial.html" with key: val, key2: val2 %}, sharing the including template's scope (classicincludesemantics, not an isolatedrender). - Liquid truthiness — only
nilandfalseare falsy;0,"", and[]are all truthy, matching Liquid (not Elixir's own truthiness rules).
Intentional deviations
| Area | Alembic | Upstream Liquid | Why |
|---|---|---|---|
join with no argument | separator "" | separator " " | Issue 1.4.3's own task list specified "" explicitly |
url_encode / url_decode | URI.encode_www_form/1 (space → +) | same (+ for space) | Matches Liquid; URI.encode/1 (percent-encodes space as %20) would not have |
ceil / floor | return integers | return integers | Matches Liquid; Elixir's own Float.ceil/1 returns a float, so this required an explicit trunc/1 |
date filter | Elixir Calendar.strftime/2 format strings | Ruby strftime format strings | No Ruby-compatible formatter available without a dependency; the two format-string dialects are similar but not identical |
| Output tag base expression | must be a bare variable path (optionally filtered) | any expression, including literals | Alembic.AST.output_node's type ({:output, path(), [filter()]}) was fixed in Milestone 1.1, before the parser existed; {{ "literal" | filter }} returns {:error, {:unsupported_output_expression, _}} |
| Cache hit/miss telemetry | Logger.debug/1 | n/a | :telemetry is a separate Hex package; issue 1.1.1's zero-runtime-deps policy (ex_doc only) rules it out |
slice filter | strings only | strings and arrays | Alembic.Filters' slice clauses always run the input through coerce_to_string/1; array slicing was never implemented |
Unsupported features (out of MVP scope)
{% tablerow %}tag{% paginate %}tag (and the wider Shopify pagination object)- Liquid's built-in request/shop/theme objects (
request,shop,theme, etc.) — Alembic has no notion of these; all context comes from theassignsmap passed torender/3 {% render %}(isolated-scope include) — only classic{% include %}(shared scope) exists{% cycle %},{% increment %},{% decrement %}tags- Liquid's
{% liquid %}shorthand block syntax - Ranges as iterables (
{% for i in (1..5) %}) —{% for %}only accepts an array-valued expression, not an inline range literal - Multi-argument
{% assign %}expressions beyond a single filter chain
Extension features (Alembic adds beyond Liquid)
base64_encode/base64_decodefiltersAlembic.Filterbehaviour for registering custom filters, globally viaconfig :alembic, custom_filters: [MyApp.Filters.Money]or per call viarender/3'scustom_filters:option (the latter takes precedence on a name collision)strict: truerender option — errors on any undefined variable instead of silently rendering"", useful for catching typos during development- ETS-backed compiled-template cache with mtime-based invalidation,
sweep/0for pruning stale entries, and acache: falseper-call opt to bypass it - Path-traversal protection built into the file loader (
{:path_traversal_detected, _})