View Source Alembic.Parser (alembic v0.1.0)
Recursive descent parser: turns the flat token list produced by
Alembic.Lexer into a typed Alembic.AST.t().
Each parse_* function is a direct implementation of a production rule in
docs/grammar.md. Expression content (inside {{ }} and after tag
keywords) is delegated to Alembic.Parser.Expression.
Alembic.Token.t() carries a line/col position (see
Alembic.Token.position/1). Two error reasons below surface one:
:unexpected_token names a concrete leftover token, so it uses that
token's own position; :missing_end_tag uses the position of the
opening tag that's missing its close (e.g. the {% if %} itself), not
a closing-tag position, since there's often no closing tag at all to
point at — running out of input entirely is the common case, not finding
the wrong one. The remaining error reasons here (:malformed_for,
:invalid_expression, etc.) operate on already-extracted tag/expression
content rather than a token, so they don't carry a position.
Whitespace control is resolved here, not in the Evaluator
Alembic.AST node types have no field for strip_left/strip_right —
adding one would mean threading strip intent through every node shape and
back out again at render time. Instead, parse/1 resolves whitespace
control immediately, as a pass over the raw token list before any node is
built: an output/tag token's strip_left/strip_right flag trims the
adjacent :text token in place. By the time parse_template/1 builds the
AST, the whitespace is already gone — the tree the Evaluator walks needs no
strip metadata at all.
Summary
Functions
Turns a token list (from Alembic.Lexer.tokenize/1) into an
Alembic.AST.t().
Like parse/1, but tries to keep going after an error instead of
stopping at the first one, so a single call can report every independent
problem in a template rather than making the caller fix-and-reparse one
error at a time. {:ok, ast} only when there are zero errors;
{:error, [reason(), ...]} otherwise, one entry per error found, in the
order encountered.
Types
@type reason() :: {:unexpected_token, Alembic.Token.t(), Alembic.Token.position()} | {:missing_end_tag, String.t(), Alembic.Token.position()} | :extends_not_first | {:unsupported_output_expression, String.t()} | {:invalid_expression, String.t(), Alembic.Parser.Expression.reason()} | {:malformed_for, String.t()} | {:malformed_assign, term()} | {:malformed_extends, term()} | {:malformed_include, term()} | {:unexpected_tag, String.t()}
Functions
@spec parse([Alembic.Token.t()]) :: {:ok, Alembic.AST.t()} | {:error, reason()}
Turns a token list (from Alembic.Lexer.tokenize/1) into an
Alembic.AST.t().
Examples
iex> {:ok, tokens} = Alembic.Lexer.tokenize("{% if x %}hi{% endif %}")
iex> Alembic.Parser.parse(tokens)
{:ok, [{:if, {:variable, ["x"]}, [{:text, "hi"}], [], nil}]}
iex> {:ok, tokens} = Alembic.Lexer.tokenize("{% if x %}no close")
iex> Alembic.Parser.parse(tokens)
{:error, {:missing_end_tag, "endif", %{line: 1, col: 1}}}
@spec parse_all([Alembic.Token.t()]) :: {:ok, Alembic.AST.t()} | {:error, [reason(), ...]}
Like parse/1, but tries to keep going after an error instead of
stopping at the first one, so a single call can report every independent
problem in a template rather than making the caller fix-and-reparse one
error at a time. {:ok, ast} only when there are zero errors;
{:error, [reason(), ...]} otherwise, one entry per error found, in the
order encountered.
Recovery is best-effort, not a guarantee of precise error isolation: on
an error, it skips forward until the next {% tag %} or {{ output }}
token (or end of input) and resumes from there. A single malformed
construct — e.g. an {% if %} with a genuinely broken tag nested inside
it — can therefore surface as more than one reported error: the real one,
plus a spurious one from resuming mid-construct. Treat the result as "at
least these problems exist," not as N precisely-located, independent
errors. parse/1 remains the right choice whenever only the first error
matters (that's still every existing caller of this module).
Examples
iex> {:ok, tokens} = Alembic.Lexer.tokenize("{% if x %}hi{% endif %}")
iex> Alembic.Parser.parse_all(tokens)
{:ok, [{:if, {:variable, ["x"]}, [{:text, "hi"}], [], nil}]}
iex> {:ok, tokens} = Alembic.Lexer.tokenize("{% if x %}a{% endfor %}{{ y ** }}")
iex> Alembic.Parser.parse_all(tokens)
{:error,
[
{:missing_end_tag, "endif", %{line: 1, col: 1}},
{:invalid_expression, "y **", {:unknown_operator, "**"}}
]}