added more utility rules and updated example

This commit is contained in:
danhanson committed 2026-09-14 15:33:17 -05:00
1 parent 036928d628
commit cb8c9f9c0b
12 files changed
+34911 -32242

No files matched your search

+90 -29
View File
@@ -46,7 +46,8 @@ argument, so `{% cycle 1as x %}` is the single token `1as` — an error, not `1`
can never be made mandatory, so the separator is an explicit token and every rule is built from helpers
that place it:
- `SEP` — `/[ \t\r\n]+/`. Newlines included: Django matches tags with `re.DOTALL`, so tags may span lines.
- `SEP` — `sym("_sep")`, the hidden rule `_sep: /[ \t\r\n]+/`, which a grammar extending this one writes as
`$._sep`. Newlines included: Django matches tags with `re.DOTALL`, so tags may span lines.
- `part(...)` — one tag part, taking the whitespace that separates it from the part before.
- `joined(first, ...rest)` — parts separated from _each other_, with no leading separator (`not in`).
- `block(tag, ...args)` — `{%`, optional separator, tag name, args, optional separator, `%}`; tags may
@@ -90,9 +91,16 @@ Consequences worth knowing before editing a rule:
Every group is split so that each tag that opens a body owns a node of its own:
```js
if_clause: ($) => seq(block("if", part($.predicate)), optional($.template)),
if_block: ($) => startBlock("if", part($.predicate)),
if_clause: ($) => seq($.if_block, optional($.template)),
endif_block: ($) => endBlock("endif"),
if_group: ($) =>
seq($.if_clause, repeat($.elif_clause), optional($.else_clause), block("endif")),
seq(
$.if_clause,
repeat($.elif_clause),
optional($.else_clause),
choice($.endif_block, alias($._missing_block, $.missing_endif_block)),
),
```
**A clause is complete without the closing tag, and that is the point.** While a template is being edited
@@ -111,7 +119,20 @@ Stripping `_block` gives the name; a new tag, or a tag group's new part, follows
A group's last child is its end block or, where the end tag is missing, a zero-width marker named for it
(`missing_endif_block`; see the scanner section). So an unterminated group still reduces to a group node,
and only a stray tag or a body the grammar cannot read produces an `ERROR`.
and a group's tag written where no open group can hold it is an `unexpected_block` (see the load section
below), so only a body the grammar cannot read produces an `ERROR`.
Every tag is built from ten inline utility rules — `simpleBlockOpen`/`simpleBlockClose`,
`startBlockOpen`/`startBlockClose`,
`followBlockOpen`/`followBlockClose`, `repeatBlockOpen`/`repeatBlockClose`, `endBlockOpen`/`endBlockClose` —
through the helpers `block` (a tag outside any group, from `simpleBlockOpen`/`simpleBlockClose`), `startBlock`, `followBlock` (a middle tag a group
holds at most once: `else`, `empty`, `plural`), `repeatBlock` (one it may repeat: `elif`) and `endBlock`.
They are inline rules rather than JS helpers so that a grammar extending this one can use them. **Every
tag must begin with one of the opens**, one an extending grammar adds included: the token they read the
tag's name with is valid wherever a tag's name is and the scanner always returns it, so a tag written from a bare `"{%"` never matches: its name is read as a `custom_tag_block` instead,
with no error to show for it. Choose `followBlock` or `repeatBlock` by what the grammar allows, because the
scanner trusts it: a tag marked once-only that can in fact repeat is an `unexpected_block` from its
second use on, and one marked repeatable that cannot is never flagged when repeated.
The names themselves need no hand-maintained list either: in the generated `src/node-types.json`, a group's
`children` are its clause types and its end block, and a clause's children include its opening block, so
@@ -141,7 +162,8 @@ silently discarded the parse in which the body continues, so every tag inside a
Sharing the clauses also made the table markedly smaller — 7,200 states and a 515KB library became 5,150
and 383KB — for the same reason hoisting `blocktranslate`'s pieces into hidden rules did: the automaton
stops duplicating the body states in every group's inline context. Giving every `{% %}` its own `_block`
rule shrank it again, from 1,999 states to 1,818, by the same mechanism.
rule shrank it again, from 1,999 states to 1,818, by the same mechanism. The group stack's tokens (see the
scanner section) cost 247, to 3,304.
### Filter pipes differ by context
@@ -170,6 +192,20 @@ listed in `NAMES_INSIDE_A_TAG_GROUP` and applied through the `tag_name` reserved
wraps, so wrapping the whole rule would un-reserve `as` and reject `{% mytag endif %}`. Adding a tag group
with a new part or end tag means adding its name there too.
**The same names are what `unexpected_block` accepts.** Once reserved, a group's tag written where no open
group can hold it — `{% endif %}` with nothing open, `{% else %}` in a `{% for %}` — used to be an `ERROR`,
and error recovery placed it badly: every tag starts with `{%` and the read token, which the parser has
already accepted when the name turns out to be wrong, so recovery rewinds to just after them and folds the
stray tag into whichever tag comes next, which then holds the `ERROR`. `unexpected_block` takes any of
`NAMES_INSIDE_A_TAG_GROUP` with loose arguments, so there is nothing to recover; `prec.dynamic(-1)` keeps it
from displacing a tag the group does take, and `queries/errors.scm` reports it as
`@error.unexpected_block`. It added 31 states and no `conflicts` entry.
Where several tags share a name the group holds only once — `{% if %}{% else %}{% else %}{% else %}` — reading
any of them as the stray parses, so the ties are broken by dynamic precedence: `unexpected_block` is -1 when
`_group_held` confirms the innermost group already holds the name and -2 otherwise, so each tag flagged
before its time costs one more, and the first stays the clause however many follow.
**Builtin names must stay keyword-extractable, or the fallbacks swallow them.** `word: $.identifier` turns
each builtin's name into its own token, which the lexer prefers wherever it is valid, so a builtin commits
to its own alternative and its argument rules still apply. This is why a filter's name and its `:` are
@@ -213,12 +249,12 @@ README nothing checks at all. The library-contents differential below, run again
### External scanner (`src/scanner.c`)
Fifteen external tokens, for the constraints a context-free grammar cannot express:
Eighteen external tokens, for the constraints a context-free grammar cannot express:
- `push_block`/`push_partial`/`push_verbatim` and the matching `pop_*` — name matching for
`{% block a %}…{% endblock a %}`. They share **one** stack of `{kind, name}` entries, not one stack per
kind: the grammar already guarantees nesting, so only the innermost open tag can be closed, and there is
never a second candidate name to try. Serialized as `<kind><name> ` per entry (`tag_kind_chars`).
- `push_block`/`push_partial`/`push_verbatim` and the matching `pop_*` — the names in
`{% block a %}…{% endblock a %}`. A push token reads the name into `pending_name`, which the group takes
when `_group_open_tag_push` pushes it; a pop token checks the written name, or its absence, against the
innermost open group, the only one the grammar lets it close, and `_group_close` then pops it.
- `verbatim_content`, `comment_content` — raw text up to the matching close tag.
- `_tag_open`, `_bt_option`, `_kwarg_name` — what a tag has already been given, so that a repeated
argument is refused: `_bt_option` for `{% blocktranslate %}`'s five options, `_kwarg_name` for `name=value`
@@ -230,28 +266,48 @@ Fifteen external tokens, for the constraints a context-free grammar cannot expre
flags, not a stack, because the tag cannot nest: its body admits no tags. It lives outside the
`stack`/`error` union, is cleared in both branches of `reset_scanner`, and is serialized after the
status character, so a GLR stack split copies it like everything else.
- `_group_open`, `_group_close` — zero width, before the name of a tag that opens or closes a tag group
(`groupBlock`). The scanner reads the name ahead of the grammar and pushes or pops a nameless entry, so
the stack holds every open group and not only the three whose names it matches; `block`, `partialdef`
and `verbatim` still push and pop through their own tokens, and are the only kinds with a pop token.
`{% plural %}` goes through `_group_close` too: reading the `count` option (`_bt_option`) turns a
blocktranslate entry into its counted kind, which waits for `plural` before its end tag. `tag_groups`
holds each kind's opening name and the names that belong to it, and `group_names` **must stay in sync
with `NAMES_INSIDE_A_TAG_GROUP`**.
- `_group_open_tag_read`, `_group_open_tag_push`, `_group_follow`, `_group_repeat`, `_group_close`, `_group_held` —
the stack
of open groups, which the scanner keeps **without knowing any group by name**, so a grammar extending this
one gets it for its own groups. The read token sits right after `{%` in every tag, always returns, and
reads the tag's name into `read_word`. The other four sit right before `%}` and check that it follows:
push opens a group expecting `"end"` + `read_word` (every Django end tag is spelled that way), follow
records a middle tag the innermost group may hold only once, repeat records nothing, and close pops the
innermost group once `read_word` is what it expects. `_group_held` belongs to `unexpected_block` (see the load section) and returns only when
the innermost group already holds a tag named `read_word`. Reading `count` (`_bt_option`) marks the group about
to be pushed as waiting for `plural`, which its follow clears. An entry is
`{expected, name, middles, awaits_plural}`, serialized as `expected:name:middles:flag` and a newline per
entry; entries hold whole names, so serializing checks `TREE_SITTER_SERIALIZATION_BUFFER_SIZE` and goes to
the error state rather than overrun it. Push and follow before `%}` cost 247 states over the table-driven
scanner (3,057 to 3,304), because argument-final states stop being shared with tags where the token is not
valid; after `%}`, where each tag's states are its own, saved only 15.
- `_missing_block` — the marker where a group's required tag block is missing, aliased at each use to
`missing_endX_block` or `missing_plural_block` (`MISSING_BLOCKS`). It is placed only at the end of
input; before a tag whose name is reserved to some other group, which therefore cannot belong to this
body; or before an `endblock`/`endpartialdef` naming a block further out than the innermost. It closes
the innermost group on the stack as the end tag would, the group reduces with no `ERROR`, and
`queries/errors.scm` is what reports it. **It is one token aliased per group, never a token per
group**: tree-sitter does not merge parse states whose valid external tokens differ, so a token per
group cloned every body state for each kind of group and took the table from 1,818 states to 17,664
(it is 3,057 as built). That trade is also why the scanner keeps a stack of every group instead of
being told by the token which group it is in. A supertype would be the natural way to match every
marker, but the generator silently drops one whose members are aliases, so `errors.scm` lists them.
`missing_endX_block` or `missing_plural_block` (`MISSING_BLOCKS`). It is placed only at the end of input;
before the end tag of a group enclosing the innermost one; before an `endblock`/`endpartialdef` naming a
block further out; or before a second `plural`, since a blocktranslate body holds no tag that could take it.
Any other tag is taken to be the innermost group's, as Django's parser takes it — a middle tag it has not
held yet, and a second of one it holds only once, which is an `unexpected_block` inside it. Marking the
group closed before a second `else` was tried and dropped: inside `{% block %}`, where most template code
sits, there is always an enclosing group to hand the `else` to, so the inner group's own end tag became a
stray instead. A counted blocktranslate is missing its
plural before anything but `plural`. The marker closes the group as its end tag would (or records the
plural), the group reduces with no `ERROR`, and `queries/errors.scm` reports it. Tags in a raw body are
not read: the marker tells the body is raw from `verbatim_content`/`comment_content` being valid, and only
the end of input counts there. **It is one token aliased per group, never a token per group**: tree-sitter
does not merge parse states whose valid external tokens differ, so a token per group cloned every body
state for each kind of group and took the table from 1,818 states to 17,664. A supertype would be the
natural way to match every marker, but the generator silently drops one whose members are aliases, so
`errors.scm` lists them.
What a stack that knows no group cannot tell: a middle tag of an enclosing group that the innermost group
has never held — `{% if a %}{% for x in y %}{% else %}` — is an `ERROR`, not a marker. Learning each
group's middle tags as they appear was tried first and put false markers on valid nesting such as
`{% if %}…{% else %}{% if %}…{% else %}`, because the outer group, having held an `else`, claimed the
inner one's.
- `matcher_error` — used by no rule; returned only if the scanner reaches an error state.
`check_space()` defines whitespace for the scanner and **must stay in sync with `SEP`**, because the
`check_space()` defines whitespace for the scanner and **must stay in sync with `_sep`**, because the
scanner skips whitespace itself while matching names.
Prefer the grammar over the scanner. A useful test: does the constraint change how the input _parses_?
@@ -278,6 +334,11 @@ matching. `_missing_block` is the one exception, and is safe only because it ret
of input or at `{%`, where `content` cannot match. It is scanned above the raw-text scanners, which find
nothing at the end of an empty body.
The group tokens were checked against that hazard with malformed groups — a missing predicate, junk before
`%}`, in an end tag, in `{% else %}` and in `{% plural %}`, and a mismatched block name. Recovery never
inserted one of them as `MISSING`, and the stack never fell out of step with the parser: later groups still
got the right markers. Junk inside an end tag does leave the whole file an `ERROR`.
### Queries
`queries/highlights.scm` lists tag and filter names explicitly, so adding either to the grammar means
+54 -20
View File
@@ -68,6 +68,8 @@ Five of the files in `queries/` are query files an editor loads:
a template is being written — and the tree reports no error of its own, so this query is how to find
it. A marker is placed only where the scanner can tell: at the end of input, before an end tag that
belongs to an enclosing group, or before an `{% endblock %}` naming a block further out.
A group's tag written where no open group can hold it, such as `{% else %}` inside a `{% for %}`, is an
`unexpected_block`, captured as `@error.unexpected_block`.
The other three are data for a tool rather than queries an editor runs. Each captures the two halves of a
relation a tree-sitter query cannot express, and the tool does the join:
@@ -137,7 +139,7 @@ What the fallbacks cannot do:
registered. Cross-referencing names against `{% load %}` needs the project's Python, so it belongs in a
linter; `queries/libraries.scm` and the `load_block` rule supply what one needs.
- **Names that only exist inside a tag group are reserved**, so `{% endif %}` or `{% empty %}` standing on
its own is an error rather than a call to a custom tag of that name.
its own is an `unexpected_block`, reported by `errors.scm`, rather than a call to a custom tag of that name.
### Adding rules for your own tags and filters
@@ -149,27 +151,48 @@ alternative of `filter`.
```js
import django from "tree-sitter-django/grammar";
const SEP = /[ \t\r\n]+/;
export default grammar(django, {
name: "djangox",
conflicts: ($, previous) => [...previous, [$.loop_clause]],
rules: {
tag_block_group: ($, previous) =>
choice(previous, $.map_block, $.loop_group),
// {% map items over rows %}
tag_block_group: ($, previous) => choice(previous, $.map_block),
map_block: ($) =>
seq(
"{%",
optional(SEP),
"map",
SEP,
$.simpleBlockOpen,
field("tag", "map"),
$._sep,
$.filtered_value,
SEP,
$._sep,
"over",
SEP,
$._sep,
$.filtered_value,
optional(SEP),
"%}",
$.simpleBlockClose,
),
// {% loop x %}…{% endloop %}
loop_block: ($) =>
seq(
$.startBlockOpen,
field("tag", "loop"),
$._sep,
$.identifier,
$.startBlockClose,
),
loop_clause: ($) => seq($.loop_block, optional($.template)),
endloop_block: ($) =>
seq($.endBlockOpen, field("tag", "endloop"), $.endBlockClose),
loop_group: ($) =>
seq(
$.loop_clause,
choice(
$.endloop_block,
alias($._missing_block, $.missing_endloop_block),
),
),
// {{ value|shout:"!" }}
@@ -182,23 +205,34 @@ export default grammar(django, {
});
```
Six things to know:
Seven things to know:
- **Whitespace is explicit.** `extras` is empty, because Django splits a tag's contents before parsing any
argument, so every separator has to be written out. The `part`/`block`/`simpleTag` helpers the base
grammar is built from are private to it, hence the `SEP` above.
grammar is built from are private to it, but the separator is the hidden rule `$._sep`, and it and the
tag utilities (`simpleBlockOpen` and the rest) are rules, so an extending grammar uses them as they are.
- **Every tag begins with one of the opens**: `$.simpleBlockOpen`, with `$.simpleBlockClose` at its end, for a
tag outside any group such as `map_block`, and `startBlockOpen`, `followBlockOpen`, `repeatBlockOpen` or
`endBlockOpen` for the tags of a group. Its
zero-width token hands the tag's name to the scanner and is taken wherever a tag can start, so a tag
written from a bare `"{%"` never matches: `{% map items over rows %}` would quietly parse as a
`custom_tag_block` instead.
- **A new name wins over the fallback.** `shout` becomes a token of its own, so it commits to your
alternative and your arity applies — `{{ x|shout }}` is now an error, while names you have not modelled
still fall through to `(filter name: (identifier))`.
- **A tag with a body is split the way the base grammar's are**, so every `{% %}` is a node of its own:
a `map_block` for the opening tag, a `map_clause` of that block and the body, an `endmap_block`, and a
`map_group` of the clause and the end block. The clause needs a `conflicts` entry, as every clause in
the base grammar does: `conflicts: ($, previous) => [...previous, [$.map_clause]]`. The generator
offers an associativity instead; taking it silently discards the parse in which the body continues.
a `loop_block` ending in `$.startBlockClose`, a `loop_clause` of that block and the body, an
`endloop_block` built from `$.endBlockOpen` and `$.endBlockClose`, and a `loop_group` of the clause and the
end block. A middle tag uses `followBlockOpen`/`followBlockClose` if the group holds it at most once, and
`repeatBlockOpen`/`repeatBlockClose` if it may repeat; the scanner trusts that choice. The clause needs a
`conflicts` entry, as every clause in the base grammar does. The generator offers an associativity
instead; taking it silently discards the parse in which the body continues.
- **End tags are not reserved**, a stray `{% endmap %}` parses as a `custom_tag_block` rather than an error — `reserved` takes no
`previous`, so the `tag_name` list cannot be added to a name at a time. Instead you may drop the `custom_tag_block` rule to avoid parsing end tags as custom tags.
- **Your own tag groups get no missing-tag markers.** Which groups the scanner tracks is fixed in its
tables, so an unterminated `{% map %}` is an `ERROR`.
- **Your groups get missing-tag markers.** The scanner knows no group by name — a group is closed by `end`
followed by its opening tag's name — so the `alias($._missing_block, $.missing_endloop_block)` choice
above is all it takes: an unclosed `{% loop x %}`, or one left open inside an `{% if %}` that
`{% endif %}` closes, holds a `missing_endloop_block`. Add the name to your copy of `errors.scm`.
- **The external scanner has to be re-exported under your grammar's name**, or block-name matching and the
repeated-argument guards will not link. Give your grammar a `src/scanner.c` that renames the base's five entry points to the
ones your generated `parser.c` calls, and includes it:
+184 -77
View File
@@ -10,8 +10,12 @@
* A newline separates parts like any other whitespace, since Django matches a
* tag with re.DOTALL and splits its contents on whitespace of any kind. The
* scanner skips the same characters when it matches a name of its own.
*
* The pattern is the hidden rule _sep, which SEP refers to, rather than a regex
* written inline, so that a grammar extending this one can write $._sep and
* share the one token instead of repeating the pattern.
*/
const SEP = /[ \t\r\n]+/;
const SEP = sym("_sep");
/**
* Parts of a tag, each separated from the one before it by whitespace.
@@ -44,37 +48,87 @@ function part(first, ...rest) {
*/
function block(tag, ...args) {
return seq(
"{%",
optional(SEP),
sym("simpleBlockOpen"),
field("tag", tag),
...args,
optional(SEP),
"%}",
sym("simpleBlockClose"),
);
}
/**
* A tag that opens or closes a tag group. The scanner keeps a stack of the
* groups that are open, which is what lets it tell a missing end tag from one
* that belongs to the group, so `marker` is a zero-width token placed before the
* tag's name: the scanner reads the name ahead of the grammar to push or pop the
* group. block, partialdef and verbatim push and pop through the tokens that
* match their names instead.
* The tags of a tag group. The scanner keeps a stack of the groups that are
* open, which is what lets it tell a missing end tag from one that belongs to
* the group, and it knows no group by name: every tag begins with
* one of the opens (simpleBlockOpen for a tag outside any group), whose zero-width token reads the tag's name to
* the scanner, and the end of the tag says what that name means.
*
* - startBlock: a tag that opens a group, which pushes one expecting "end"
* followed by the tag's name.
* - followBlock: a middle tag a group may hold only once, such as "else". The
* first belongs to the innermost group; a second, once that group already
* holds one, cannot be its own, so the innermost group is taken to be missing
* its end tag and the tag goes to a group around it.
* - repeatBlock: a middle tag a group may hold any number of times, such as
* "elif", which always belongs to the innermost group.
* - endBlock: the tag that closes the innermost group, which pops it.
*
* The ten utilities are inline rules rather than helpers, so that a grammar
* extending this one can build its own groups from them; every tag it adds has
* to begin with one of the opens, since the token they read with is valid
* wherever a tag's name is and always wins.
*
* @param {Rule} marker
* @param {RuleOrLiteral} tag
* @param {RuleOrLiteral[]} args
* @returns {SeqRule}
*/
function groupBlock(marker, tag, ...args) {
function startBlock(tag, ...args) {
return seq(
"{%",
optional(SEP),
marker,
sym("startBlockOpen"),
field("tag", tag),
...args,
optional(SEP),
"%}",
sym("startBlockClose"),
);
}
/**
* @param {RuleOrLiteral} tag
* @param {RuleOrLiteral[]} args
* @returns {SeqRule}
*/
function followBlock(tag, ...args) {
return seq(
sym("followBlockOpen"),
field("tag", tag),
...args,
sym("followBlockClose"),
);
}
/**
* @param {RuleOrLiteral} tag
* @param {RuleOrLiteral[]} args
* @returns {SeqRule}
*/
function repeatBlock(tag, ...args) {
return seq(
sym("repeatBlockOpen"),
field("tag", tag),
...args,
sym("repeatBlockClose"),
);
}
/**
* @param {RuleOrLiteral} tag
* @param {RuleOrLiteral[]} args
* @returns {SeqRule}
*/
function endBlock(tag, ...args) {
return seq(
sym("endBlockOpen"),
field("tag", tag),
...args,
sym("endBlockClose"),
);
}
@@ -242,10 +296,9 @@ function simpleTag($, tag, args = true, kwargs = true) {
function blocktransRules(tag) {
return {
[`_${tag}_block`]: ($) =>
groupBlock($._group_open, tag, $._tag_open, repeat($._translate_option)),
startBlock(tag, $._tag_open, repeat($._translate_option)),
[`_${tag}_count_block`]: ($) =>
groupBlock(
$._group_open,
startBlock(
tag,
$._tag_open,
repeat($._translate_option),
@@ -267,7 +320,7 @@ function blocktransRules(tag) {
),
),
),
[`end${tag}_block`]: ($) => groupBlock($._group_close, `end${tag}`),
[`end${tag}_block`]: ($) => endBlock(`end${tag}`),
};
}
@@ -460,13 +513,24 @@ const django = grammar({
[$.spaceless_clause],
[$.timezone_clause],
[$.with_clause],
[$.predicate],
[$.binaryOperator],
[$.library, $.load_block],
[$._translate_option],
[$._filtered_value_spaced],
[$._filter_expression_spaced],
],
// the tags of a group; see startBlock
inline: ($) => [
$.simpleBlockOpen,
$.simpleBlockClose,
$.startBlockOpen,
$.startBlockClose,
$.followBlockOpen,
$.followBlockClose,
$.repeatBlockOpen,
$.repeatBlockClose,
$.endBlockOpen,
$.endBlockClose,
],
supertypes: ($) => [$.template_node, $.tag_block_group],
externals: ($) => [
$.matcher_error,
@@ -482,8 +546,12 @@ const django = grammar({
$._bt_option,
$._kwarg_name,
$._missing_block,
$._group_open,
$._group_open_tag_read,
$._group_open_tag_push,
$._group_follow,
$._group_repeat,
$._group_close,
$._group_held,
],
reserved: {
global: ($) => ["not", "if", "in", "is", "as", "for", "from"],
@@ -491,7 +559,20 @@ const django = grammar({
},
rules: {
template: ($) => repeat1(choice($.template_node, $.content)),
// the tags of a group; see startBlock
simpleBlockOpen: ($) => seq("{%", $._group_open_tag_read, optional(SEP)),
simpleBlockClose: ($) => seq(optional(SEP), "%}"),
startBlockOpen: ($) => seq("{%", $._group_open_tag_read, optional(SEP)),
startBlockClose: ($) => seq($._group_open_tag_push, optional(SEP), "%}"),
followBlockOpen: ($) => seq("{%", $._group_open_tag_read, optional(SEP)),
followBlockClose: ($) => seq($._group_follow, optional(SEP), "%}"),
repeatBlockOpen: ($) => seq("{%", $._group_open_tag_read, optional(SEP)),
repeatBlockClose: ($) => seq($._group_repeat, optional(SEP), "%}"),
endBlockOpen: ($) => seq("{%", $._group_open_tag_read, optional(SEP)),
endBlockClose: ($) => seq($._group_close, optional(SEP), "%}"),
content: ($) => /(?:[^\{]|\{[^\{#%}])+/,
// the separator between the parts of a tag; see SEP
_sep: ($) => /[ \t\r\n]+/,
template_node: ($) =>
choice($.tag_block_group, $.template_variable, $.template_comment),
filtered_value: ($) =>
@@ -627,6 +708,7 @@ const django = grammar({
$.templatetag_block,
$.timezone_group,
$.translate_block,
$.unexpected_block,
$.url_block,
$.verbatim_group,
$.widthratio_block,
@@ -676,9 +758,9 @@ const django = grammar({
optional(seq(":", field("argument", $.value))),
),
autoescape_block: ($) =>
groupBlock($._group_open, "autoescape", part(choice("on", "off"))),
startBlock("autoescape", part(choice("on", "off"))),
autoescape_clause: ($) => seq($.autoescape_block, optional($.template)),
endautoescape_block: ($) => groupBlock($._group_close, "endautoescape"),
endautoescape_block: ($) => endBlock("endautoescape"),
autoescape_group: ($) =>
seq(
$.autoescape_clause,
@@ -688,9 +770,9 @@ const django = grammar({
),
),
// the scanner takes the whitespace before a name it has to match itself
block_block: ($) => block("block", field("name", $.push_block)),
block_block: ($) => startBlock("block", field("name", $.push_block)),
block_clause: ($) => seq($.block_block, optional($.template)),
endblock_block: ($) => block("endblock", $.pop_block),
endblock_block: ($) => endBlock("endblock", $.pop_block),
block_group: ($) =>
seq(
$.block_clause,
@@ -699,7 +781,7 @@ const django = grammar({
alias($._missing_block, $.missing_endblock_block),
),
),
plural_block: ($) => groupBlock($._group_close, "plural"),
plural_block: ($) => followBlock("plural"),
plural_clause: ($) => seq($.plural_block, optional($._translate_body)),
...blocktransRules("blocktrans"),
...blocktransRules("blocktranslate"),
@@ -765,8 +847,7 @@ const django = grammar({
),
),
cache_block: ($) =>
groupBlock(
$._group_open,
startBlock(
"cache",
part($.filtered_value),
part(choice($.identifier, $.string)),
@@ -774,7 +855,7 @@ const django = grammar({
optional(part(seq("using=", $.filtered_value))),
),
cache_clause: ($) => seq($.cache_block, optional($.template)),
endcache_block: ($) => groupBlock($._group_close, "endcache"),
endcache_block: ($) => endBlock("endcache"),
cache_group: ($) =>
seq(
$.cache_clause,
@@ -783,10 +864,9 @@ const django = grammar({
alias($._missing_block, $.missing_endcache_block),
),
),
comment_block: ($) =>
groupBlock($._group_open, "comment", optional(part($.string))),
comment_block: ($) => startBlock("comment", optional(part($.string))),
comment_clause: ($) => seq($.comment_block, optional($.comment_content)),
endcomment_block: ($) => groupBlock($._group_close, "endcomment"),
endcomment_block: ($) => endBlock("endcomment"),
comment_group: ($) =>
seq(
$.comment_clause,
@@ -823,13 +903,12 @@ const django = grammar({
// than as split arguments, so pipes here take whitespace just as they do
// between "{{" and "}}"
filter_block: ($) =>
groupBlock(
$._group_open,
startBlock(
"filter",
part(alias($._filter_expression_spaced, $.filter_expression)),
),
filter_clause: ($) => seq($.filter_block, optional($.template)),
endfilter_block: ($) => groupBlock($._group_close, "endfilter"),
endfilter_block: ($) => endBlock("endfilter"),
filter_group: ($) =>
seq(
$.filter_clause,
@@ -845,8 +924,7 @@ const django = grammar({
optional(asVariable($)),
),
for_block: ($) =>
groupBlock(
$._group_open,
startBlock(
"for",
part(field("variable", $.identifier)),
repeat(
@@ -873,9 +951,9 @@ const django = grammar({
),
),
for_clause: ($) => seq($.for_block, optional($.template)),
empty_block: ($) => block("empty"),
empty_block: ($) => followBlock("empty"),
empty_clause: ($) => seq($.empty_block, optional($.template)),
endfor_block: ($) => groupBlock($._group_close, "endfor"),
endfor_block: ($) => endBlock("endfor"),
for_group: ($) =>
seq(
$.for_clause,
@@ -911,14 +989,14 @@ const django = grammar({
block("get_media_prefix", optional(asVariable($))),
get_static_prefix_block: ($) =>
block("get_static_prefix", optional(asVariable($))),
if_block: ($) => groupBlock($._group_open, "if", part($.predicate)),
if_block: ($) => startBlock("if", part($.predicate)),
if_clause: ($) => seq($.if_block, optional($.template)),
elif_block: ($) => block("elif", part($.predicate)),
elif_block: ($) => repeatBlock("elif", part($.predicate)),
elif_clause: ($) => seq($.elif_block, optional($.template)),
/** Shared by if_group and ifchanged_group, which spell it the same way. */
else_block: ($) => block("else"),
else_block: ($) => followBlock("else"),
else_clause: ($) => seq($.else_block, optional($.template)),
endif_block: ($) => groupBlock($._group_close, "endif"),
endif_block: ($) => endBlock("endif"),
if_group: ($) =>
seq(
$.if_clause,
@@ -927,9 +1005,9 @@ const django = grammar({
choice($.endif_block, alias($._missing_block, $.missing_endif_block)),
),
ifchanged_block: ($) =>
groupBlock($._group_open, "ifchanged", repeat(part($.filtered_value))),
startBlock("ifchanged", repeat(part($.filtered_value))),
ifchanged_clause: ($) => seq($.ifchanged_block, optional($.template)),
endifchanged_block: ($) => groupBlock($._group_close, "endifchanged"),
endifchanged_block: ($) => endBlock("endifchanged"),
ifchanged_group: ($) =>
seq(
$.ifchanged_clause,
@@ -955,10 +1033,9 @@ const django = grammar({
),
),
/** django.templatetags.i18n. The language tag takes the one argument. */
language_block: ($) =>
groupBlock($._group_open, "language", part($.filtered_value)),
language_block: ($) => startBlock("language", part($.filtered_value)),
language_clause: ($) => seq($.language_block, optional($.template)),
endlanguage_block: ($) => groupBlock($._group_close, "endlanguage"),
endlanguage_block: ($) => endBlock("endlanguage"),
language_group: ($) =>
seq(
$.language_clause,
@@ -981,13 +1058,9 @@ const django = grammar({
* rejects anything but "on" or "off", including a second word.
*/
localize_block: ($) =>
groupBlock(
$._group_open,
"localize",
optional(part(choice("on", "off"))),
),
startBlock("localize", optional(part(choice("on", "off")))),
localize_clause: ($) => seq($.localize_block, optional($.template)),
endlocalize_block: ($) => groupBlock($._group_close, "endlocalize"),
endlocalize_block: ($) => endBlock("endlocalize"),
localize_group: ($) =>
seq(
$.localize_clause,
@@ -998,13 +1071,9 @@ const django = grammar({
),
/** django.templatetags.tz, taking "on" or "off" as localize does. */
localtime_block: ($) =>
groupBlock(
$._group_open,
"localtime",
optional(part(choice("on", "off"))),
),
startBlock("localtime", optional(part(choice("on", "off")))),
localtime_clause: ($) => seq($.localtime_block, optional($.template)),
endlocaltime_block: ($) => groupBlock($._group_close, "endlocaltime"),
endlocaltime_block: ($) => endBlock("endlocaltime"),
localtime_group: ($) =>
seq(
$.localtime_clause,
@@ -1023,9 +1092,9 @@ const django = grammar({
now_block: ($) => block("now", part($.string), optional(asVariable($))),
partial_block: ($) => block("partial", part($.identifier)),
partialdef_block: ($) =>
block("partialdef", $.push_partial, optional(part("inline"))),
startBlock("partialdef", $.push_partial, optional(part("inline"))),
partialdef_clause: ($) => seq($.partialdef_block, optional($.template)),
endpartialdef_block: ($) => block("endpartialdef", $.pop_partial),
endpartialdef_block: ($) => endBlock("endpartialdef", $.pop_partial),
partialdef_group: ($) =>
seq(
$.partialdef_clause,
@@ -1045,9 +1114,9 @@ const django = grammar({
optional(asVariable($)),
),
resetcycle_block: ($) => block("resetcycle", optional(part($.identifier))),
spaceless_block: ($) => groupBlock($._group_open, "spaceless"),
spaceless_block: ($) => startBlock("spaceless"),
spaceless_clause: ($) => seq($.spaceless_block, optional($.template)),
endspaceless_block: ($) => groupBlock($._group_close, "endspaceless"),
endspaceless_block: ($) => endBlock("endspaceless"),
spaceless_group: ($) =>
seq(
$.spaceless_clause,
@@ -1075,10 +1144,9 @@ const django = grammar({
),
),
/** django.templatetags.tz. timezone_tag takes the one argument. */
timezone_block: ($) =>
groupBlock($._group_open, "timezone", part($.filtered_value)),
timezone_block: ($) => startBlock("timezone", part($.filtered_value)),
timezone_clause: ($) => seq($.timezone_block, optional($.template)),
endtimezone_block: ($) => groupBlock($._group_close, "endtimezone"),
endtimezone_block: ($) => endBlock("endtimezone"),
timezone_group: ($) =>
seq(
$.timezone_clause,
@@ -1109,6 +1177,43 @@ const django = grammar({
]),
),
),
/**
* A tag of some group, written where no open group can hold it: an end tag
* with nothing open, or a middle tag the innermost group does not take. It
* is a node of its own rather than an ERROR so that error recovery has
* nothing to repair: recovery rewinds to just after the "{%" every tag
* starts with, and folds the stray tag into whichever tag comes next.
* Dynamic precedence keeps it from displacing a tag its group does take,
* and queries/errors.scm reports it.
*
* Of several tags with a name the group may hold only once, such as a
* second and a third "else", the later ones are the strays. Reading the
* first as the stray instead parses just as well, so _group_held, which the
* scanner returns only for a name the innermost group already holds, puts
* the later reading at -1 against -2 for each tag flagged before its time.
*/
unexpected_block: ($) =>
choice(
prec.dynamic(
-1,
seq(
sym("simpleBlockOpen"),
field("tag", choice(...NAMES_INSIDE_A_TAG_GROUP)),
repeat(seq(SEP, /[^\s%]+/)),
$._group_held,
sym("simpleBlockClose"),
),
),
prec.dynamic(
-2,
seq(
sym("simpleBlockOpen"),
field("tag", choice(...NAMES_INSIDE_A_TAG_GROUP)),
repeat(seq(SEP, /[^\s%]+/)),
sym("simpleBlockClose"),
),
),
),
url_block: ($) =>
block(
"url",
@@ -1121,9 +1226,9 @@ const django = grammar({
),
optional(asVariable($)),
),
verbatim_block: ($) => block("verbatim", $.push_verbatim),
verbatim_block: ($) => startBlock("verbatim", $.push_verbatim),
verbatim_clause: ($) => seq($.verbatim_block, optional($.verbatim_content)),
endverbatim_block: ($) => block("endverbatim", $.pop_verbatim),
endverbatim_block: ($) => endBlock("endverbatim", $.pop_verbatim),
verbatim_group: ($) =>
seq(
$.verbatim_clause,
@@ -1140,14 +1245,16 @@ const django = grammar({
part($.filtered_value),
optional(asVariable($)),
),
with_block: ($) =>
groupBlock($._group_open, "with", $._tag_open, $._tag_kwargs),
with_block: ($) => startBlock("with", $._tag_open, $._tag_kwargs),
with_clause: ($) => seq($.with_block, optional($.template)),
endwith_block: ($) => groupBlock($._group_close, "endwith"),
endwith_block: ($) => endBlock("endwith"),
with_group: ($) =>
seq(
$.with_clause,
choice($.endwith_block, alias($._missing_block, $.missing_endwith_block)),
choice(
$.endwith_block,
alias($._missing_block, $.missing_endwith_block),
),
),
},
});
+2
View File
@@ -39,3 +39,5 @@
(missing_endverbatim_block)
(missing_endwith_block)
(missing_plural_block)] @error.missing_block
(unexpected_block) @error.unexpected_block
+1154 -2422
View File
File diff suppressed because it is too large. Load diff
+104
View File
@@ -167,6 +167,10 @@
"type": "translate_block",
"named": true
},
{
"type": "unexpected_block",
"named": true
},
{
"type": "url_block",
"named": true
@@ -3050,6 +3054,106 @@
]
}
},
{
"type": "unexpected_block",
"named": true,
"fields": {
"tag": {
"multiple": false,
"required": true,
"types": [
{
"type": "elif",
"named": false
},
{
"type": "else",
"named": false
},
{
"type": "empty",
"named": false
},
{
"type": "endautoescape",
"named": false
},
{
"type": "endblock",
"named": false
},
{
"type": "endblocktrans",
"named": false
},
{
"type": "endblocktranslate",
"named": false
},
{
"type": "endcache",
"named": false
},
{
"type": "endcomment",
"named": false
},
{
"type": "endfilter",
"named": false
},
{
"type": "endfor",
"named": false
},
{
"type": "endif",
"named": false
},
{
"type": "endifchanged",
"named": false
},
{
"type": "endlanguage",
"named": false
},
{
"type": "endlocalize",
"named": false
},
{
"type": "endlocaltime",
"named": false
},
{
"type": "endpartialdef",
"named": false
},
{
"type": "endspaceless",
"named": false
},
{
"type": "endtimezone",
"named": false
},
{
"type": "endverbatim",
"named": false
},
{
"type": "endwith",
"named": false
},
{
"type": "plural",
"named": false
}
]
}
}
},
{
"type": "url_block",
"named": true,
+32298 -29126
View File
File diff suppressed because it is too large. Load diff
+474 -534
View File
File diff suppressed because it is too large. Load diff
+19 -22
View File
@@ -335,7 +335,6 @@ Blocktrans Closed By The Other Spelling
---
==================
Blocktranslate Closed By The Other Spelling
:error
@@ -346,7 +345,6 @@ Blocktranslate Closed By The Other Spelling
---
==================
Blocktrans With Count And No Plural
==================
@@ -370,8 +368,6 @@ Blocktrans With Count And No Plural
(endblocktrans_block))
(content))
==================
Blocktrans With Plural And No Count
:error
@@ -382,7 +378,6 @@ Blocktrans With Plural And No Count
---
==================
Blocktrans Counting Two Bindings
:error
@@ -393,7 +388,6 @@ Blocktrans Counting Two Bindings
---
==================
Blocktrans With Nothing To Bind
:error
@@ -404,7 +398,6 @@ Blocktrans With Nothing To Bind
---
==================
Blocktrans With An Unknown Option
:error
@@ -415,7 +408,6 @@ Blocktrans With An Unknown Option
---
==================
Blocktrans With Asvar And No Name
:error
@@ -426,7 +418,6 @@ Blocktrans With Asvar And No Name
---
==================
Blocktrans Holding A Tag
:error
@@ -437,7 +428,6 @@ Blocktrans Holding A Tag
---
==================
Blocktrans Holding A Comment
:error
@@ -448,7 +438,6 @@ Blocktrans Holding A Comment
---
==================
Blocktrans Holding Another Blocktrans
:error
@@ -459,17 +448,33 @@ Blocktrans Holding Another Blocktrans
---
==================
Blocktrans With Two Plurals
:error
==================
{% blocktrans count n=x %}one{% plural %}many{% plural %}x{% endblocktrans %}
---
(template
(content)
(blocktranslate_group
(blocktrans_clause
(blocktrans_block
(identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(content)
(plural_clause
(plural_block)
(content)))
(missing_endblocktrans_block))
(unexpected_block)
(content)
(unexpected_block)
(content))
==================
Blocktrans With An Argument On Plural
@@ -481,7 +486,6 @@ Blocktrans With An Argument On Plural
---
==================
Blocktrans With Trimmed Twice
:error
@@ -492,7 +496,6 @@ Blocktrans With Trimmed Twice
---
==================
Blocktrans With With Twice
:error
@@ -503,7 +506,6 @@ Blocktrans With With Twice
---
==================
Blocktrans With Context Twice
:error
@@ -514,7 +516,6 @@ Blocktrans With Context Twice
---
==================
Blocktrans With Asvar Twice
:error
@@ -525,7 +526,6 @@ Blocktrans With Asvar Twice
---
==================
Blocktrans With Count Twice
:error
@@ -536,7 +536,6 @@ Blocktrans With Count Twice
---
==================
Blocktranslate With Trimmed Twice
:error
@@ -547,7 +546,6 @@ Blocktranslate With Trimmed Twice
---
==================
Blocktrans Repeating An Option Of The Tag Before It
==================
@@ -579,4 +577,3 @@ Blocktrans With A Binding Twice
---
+9 -2
View File
@@ -230,25 +230,32 @@ Builtin Filter With An Argument It Does Not Take
---
==================
Custom Tag Does Not Cover An End Tag
:error
==================
{% endif %}
---
(template
(content)
(unexpected_block)
(content))
==================
Custom Tag Does Not Cover A Part Of A Tag Group
:error
==================
{% else %}
---
(template
(content)
(unexpected_block)
(content))
==================
A Name Reserved For A Tag Is Still A Variable
+27 -2
View File
@@ -173,7 +173,6 @@ c
==================
Ifchanged With Two Elses
:error
==================
{% ifchanged %}
@@ -186,10 +185,24 @@ c
---
(template
(content)
(ifchanged_group
(ifchanged_clause
(ifchanged_block)
(template
(content)))
(else_clause
(else_block)
(template
(content)
(unexpected_block)
(content)))
(endifchanged_block))
(content))
==================
Ifchanged With An Argument On Else
:error
==================
{% ifchanged %}
@@ -199,3 +212,15 @@ b
{% endifchanged %}
---
(template
(content)
(ifchanged_group
(ifchanged_clause
(ifchanged_block)
(template
(content)
(unexpected_block)
(content)))
(endifchanged_block))
(content))
+496 -8
View File
@@ -164,7 +164,7 @@ Missing Endif Before An Outer End Tag
(content))
==================
Missing Endfor Before An Outer Else
Unseen Outer Else Is Not A Missing Tag
==================
{% if a %}{% for x in y %}{% else %}b{% endif %}
@@ -189,12 +189,11 @@ Missing Endfor Before An Outer Else
(filtered_value
(value
(variable_attribute
(identifier))))))
(missing_endfor_block))))
(else_clause
(else_block)
(identifier)))))
(template
(unexpected_block)
(content)))
(missing_endfor_block))))
(endif_block))
(content))
@@ -462,35 +461,524 @@ Missing Endif After A Custom End Tag
(content)))
(missing_endif_block)))
==================
Second Else After A Nested Else Is Unexpected
==================
{% if a %}{% if b %}x{% else %}y{% else %}z{% endif %}
---
(template
(content)
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(else_clause
(else_block)
(template
(content)
(unexpected_block)
(content)))
(endif_block))
(content)))
(missing_endif_block)))
==================
Second Empty After A Nested Empty Is Unexpected
==================
{% for x in y %}{% for z in w %}a{% empty %}b{% empty %}c{% endfor %}
---
(template
(content)
(for_group
(for_clause
(for_block
(identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(for_group
(for_clause
(for_block
(identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(content)))
(empty_clause
(empty_block)
(template
(content)
(unexpected_block)
(content)))
(endfor_block))
(content)))
(missing_endfor_block)))
==================
Else Of An If Nested In An Else
==================
{% if a %}x{% else %}{% if b %}y{% else %}z{% endif %}{% endif %}
---
(template
(content)
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(else_clause
(else_block)
(template
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(else_clause
(else_block)
(template
(content)))
(endif_block))))
(endif_block))
(content))
==================
Elif Of An If Nested In An Elif
==================
{% if a %}x{% elif b %}{% if c %}y{% elif d %}z{% endif %}{% endif %}
---
(template
(content)
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(elif_clause
(elif_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(elif_clause
(elif_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(endif_block))))
(endif_block))
(content))
==================
Empty Of A For Nested In An Empty
==================
{% for x in y %}a{% empty %}{% for z in w %}b{% empty %}c{% endfor %}{% endfor %}
---
(template
(content)
(for_group
(for_clause
(for_block
(identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(content)))
(empty_clause
(empty_block)
(template
(for_group
(for_clause
(for_block
(identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(content)))
(empty_clause
(empty_block)
(template
(content)))
(endfor_block))))
(endfor_block))
(content))
==================
Else Of An Ifchanged Nested In An Else Of An If
==================
{% if a %}x{% else %}{% ifchanged %}y{% else %}z{% endifchanged %}{% endif %}
---
(template
(content)
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(else_clause
(else_block)
(template
(ifchanged_group
(ifchanged_clause
(ifchanged_block)
(template
(content)))
(else_clause
(else_block)
(template
(content)))
(endifchanged_block))))
(endif_block))
(content))
==================
Else Of An If Nested In An Else Of An Ifchanged
==================
{% ifchanged %}a{% else %}{% if b %}y{% else %}z{% endif %}{% endifchanged %}
---
(template
(content)
(ifchanged_group
(ifchanged_clause
(ifchanged_block)
(template
(content)))
(else_clause
(else_block)
(template
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(else_clause
(else_block)
(template
(content)))
(endif_block))))
(endifchanged_block))
(content))
==================
Third Else Is Unexpected Too
==================
{% if a %}w{% else %}x{% else %}y{% else %}z{% endif %}
---
(template
(content)
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(else_clause
(else_block)
(template
(content)
(unexpected_block)
(content)
(unexpected_block)
(content)))
(endif_block))
(content))
==================
Second And Third Empty Are Unexpected
==================
{% for x in y %}a{% empty %}b{% empty %}c{% empty %}d{% endfor %}
---
(template
(content)
(for_group
(for_clause
(for_block
(identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(content)))
(empty_clause
(empty_block)
(template
(content)
(unexpected_block)
(content)
(unexpected_block)
(content)))
(endfor_block))
(content))
==================
Second And Third Else Of An Ifchanged Are Unexpected
==================
{% ifchanged %}a{% else %}b{% else %}c{% else %}d{% endifchanged %}
---
(template
(content)
(ifchanged_group
(ifchanged_clause
(ifchanged_block)
(template
(content)))
(else_clause
(else_block)
(template
(content)
(unexpected_block)
(content)
(unexpected_block)
(content)))
(endifchanged_block))
(content))
==================
Second Else Inside A Block Is Unexpected
==================
{% block a %}{% if b %}x{% else %}y{% else %}z{% endif %}{% endblock %}
---
(template
(content)
(block_group
(block_clause
(block_block
(push_block))
(template
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(else_clause
(else_block)
(template
(content)
(unexpected_block)
(content)))
(endif_block))))
(endblock_block
(pop_block)))
(content))
==================
Second Else Of An If In A For Is Unexpected
==================
{% for x in y %}{% if a %}b{% else %}c{% else %}d{% endif %}{% endfor %}
---
(template
(content)
(for_group
(for_clause
(for_block
(identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(else_clause
(else_block)
(template
(content)
(unexpected_block)
(content)))
(endif_block))))
(endfor_block))
(content))
==================
Stray End Tag After A Missing One
:error
==================
{% if a %}{% endfor %}
---
(template
(content)
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(unexpected_block)
(content)))
(missing_endif_block)))
==================
Endblock Naming No Open Block
:error
==================
{% block a %}{% block b %}{% endblock c %}
---
(template
(content)
(block_group
(block_clause
(block_block
(push_block))
(template
(block_group
(block_clause
(block_block
(push_block))
(template
(unexpected_block)
(content)))
(missing_endblock_block))))
(missing_endblock_block)))
==================
Second Else Is Not A Missing Tag
:error
==================
{% if a %}{% else %}{% else %}{% endif %}
---
(template
(content)
(if_group
(if_clause
(if_block
(predicate
(filtered_value
(value
(variable_attribute
(identifier)))))))
(else_clause
(else_block)
(template
(unexpected_block)))
(endif_block))
(content))
==================
Plural Without Count Is Not A Missing Tag