Compare commits

...
3 Commits
Author SHA1 Message Date
danhanson 9701fab04a add divergences section to README.md and fixed reversed and noop arguments 2026-09-10 15:56:16 -05:00
danhanson 913ca69e6c add True, False, None 2026-09-10 15:00:39 -05:00
danhanson c0a2e0099c add conditionals query 2026-09-10 14:38:59 -05:00
14 changed files with 42589 additions and 39282 deletions

No files matched your search

+28 -6
View File
@@ -61,6 +61,15 @@ that place it:
order but never twice. Used by `translate` and `include`; keyword arguments are guarded by the scanner order but never twice. Used by `translate` and `include`; keyword arguments are guarded by the scanner
instead. Grows as `Σ C(n,k)·k!` (65 for four names), which is instead. Grows as `Σ C(n,k)·k!` (65 for four names), which is
why it is only used for signatures that name their arguments. why it is only used for signatures that name their arguments.
- `wordAsValueRules(word)` / `wordAsValue($, word, continued)` — a tag's own keyword written where a value
goes. Django tells its flag from a variable by comparing the whole bit, so `reversed` is the flag of
`{% for %}` while `reversed.x` and `reversed|last` are lookups; `continued` selects that second group.
Needed because keyword extraction hands the parser the keyword wherever both it and `identifier` are
valid, so the reading in which the word names a variable has to be built back out of it. The rules are
hidden and aliased at the use site, one per level (`variable_attribute`, `value`, `filtered_value`),
because an alias renames the node its own rule makes and **cannot nest one inside another** — writing
it as nested inline `alias(alias(...))` calls silently yields a flat, partly zero-width tree that still
generates and still parses. Check the tree, not just the accept.
Consequences worth knowing before editing a rule: Consequences worth knowing before editing a rule:
@@ -229,6 +238,19 @@ too if it were spelled the same way. `queries/exports.scm` names it for a tool t
bind in the scope around the block, which a locals query cannot do: it places a definition in the innermost bind in the scope around the block, which a locals query cannot do: it places a definition in the innermost
scope containing it and offers no way out again. scope containing it and offers no way out again.
`queries/conditionals.scm` is data for a tool too: `@conditional.group` / `@conditional.branch` /
`@conditional.default` say which bodies render on only some passes, so a checker can tell that
`{% if a %}{% now "Y" as n %}{% endif %}{{ n }}` may reach `{{ n }}` with nothing bound. A locals query
cannot: it matches a reference to the definition whose scope contains it and has no notion of a path not
taken. A group is exhaustive iff it has a `@conditional.default`, so each pattern captures the group
alongside one of its parts and a tool needs no list of which clause belongs to which group. Adding a tag
group whose body may be skipped means adding it there — `{% cache %}` is the non-obvious one, since a
cache hit skips the body outright and `CacheNode` pushes no context.
A branch that is also a `@local.scope` confines its bindings whichever way the branch goes, which is why
`{% for %}` is inert for that check: `ForNode` renders `nodelist_empty` **inside** the same
`context.push()` as the loop body, so `empty_clause` is a scope alongside `for_clause`.
## Testing ## Testing
Corpus tests live in `test/corpus/*.txt`, one file per tag. Corpus tests live in `test/corpus/*.txt`, one file per tag.
@@ -264,6 +286,12 @@ documentation or memory. Both checks are worth repeating whenever tags, filters
- Django has no number token: `[\w.]+` is one lexeme and `int()`/`float()` decides whether it is a literal, - Django has no number token: `[\w.]+` is one lexeme and `int()`/`float()` decides whether it is a literal,
so `1a`, `1e`, `0x1f`, `1.2.3`, `1__0` and `1.` are variable _lookups_ there and errors here. so `1a`, `1e`, `0x1f`, `1.2.3`, `1__0` and `1.` are variable _lookups_ there and errors here.
- Django has no boolean or `None` token either. `True`, `False` and `None` are lookups, which resolve
because `BaseContext._reset_dicts` seeds every context with `{"True": True, "False": False, "None": None}`.
They are `boolean` and `none` literals here, so that a tool reading `queries/locals.scm` does not count
them as references to something the template never defines. Shadowing still parses — `{% with True=1 %}`
binds an `identifier`, because the keyword is not valid where a binding name is — but `{{ True.x }}` is
an error here and an ordinary two-part lookup in Django, which renders `string_if_invalid`.
- A `with` binding written twice (`{% with a=1 a=2 %}`, and the same in `{% include %}` and - A `with` binding written twice (`{% with a=1 a=2 %}`, and the same in `{% include %}` and
`{% blocktranslate %}`) is an error here and not in Django, where `token_kwargs` builds a dict and the `{% blocktranslate %}`) is an error here and not in Django, where `token_kwargs` builds a dict and the
later value wins. It has no use, so it is treated as the mistake it almost certainly is. later value wins. It has no use, so it is treated as the mistake it almost certainly is.
@@ -286,11 +314,5 @@ documentation or memory. Both checks are worth repeating whenever tags, filters
`{% cache 500 sidebar.name %}` names a fragment there, `{% cache 500 using="c" %}` names a fragment `{% cache 500 sidebar.name %}` names a fragment there, `{% cache 500 using="c" %}` names a fragment
called `using="c"` rather than choosing a cache, and `{% cache 500 sidebar using= %}` takes an empty called `using="c"` rather than choosing a cache, and `{% cache 500 sidebar using= %}` takes an empty
name. All three are errors here. name. All three are errors here.
- `do_for` reads `reversed` off the end of the tag before it looks for `in`, so `{% for x in reversed %}`
is an error there and a loop over a variable named `reversed` here. Writing the flag as well
(`{% for x in reversed reversed %}`) is accepted by both, which is why the name is not simply reserved
in that position.
- `do_translate` refuses `as` and `noop` as the value of its `context` option, so
`{% trans "hi" context noop %}` is an error there and a context named by the variable `noop` here.
- `partial`/`partialdef` are Django builtins as of Django 6; `elif`/`else`/`empty` are modelled as parts of - `partial`/`partialdef` are Django builtins as of Django 6; `elif`/`else`/`empty` are modelled as parts of
their enclosing tag rather than as separate tags. their enclosing tag rather than as separate tags.
+25
View File
@@ -11,3 +11,28 @@ npm run parser-test # run the corpus tests in test/corpus
npm run parser-build # build the WASM binary npm run parser-build # build the WASM binary
npm run playground # interactively explore the grammar npm run playground # interactively explore the grammar
``` ```
## Divergences from Django
The grammar aims to accept what Django accepts, with one deliberate exception. Where Django takes an
input without complaint but the input is almost certainly a mistake — it parses, and then silently
renders nothing or quietly ignores half of what was written — the grammar reports an error instead.
Every case below is input Django itself accepts:
- **Number-shaped names.** Django has no number token: a word is a literal if `int()` or `float()` parses
it and a variable lookup otherwise, so `0x1f`, `1a`, `1.`, `1.2.3` and `1__0` look up a variable of that
name and render nothing.
- **An attribute of `True`, `False` or `None`.** Every Django context is seeded with those three names, so
`{{ True.x }}` is an ordinary two-part lookup that can never resolve.
- **A keyword argument written twice.** `{% with a=1 a=2 %}`, and the same in `{% include %}` and
`{% blocktranslate %}`, where Django builds a dict and lets the last value win.
- **Anything after a `{% static %}` path.** `StaticNode.handle_token` looks for `as` two words from the end
and ignores the rest, so `{% static "a" junk %}` and `{% static "a" as url junk %}` render as if the
extra words were not there.
- **A `{% cache %}` argument meant as something other than the fragment name.** Django takes the raw word
in that position, so `{% cache 500 using="c" %}` names a fragment called `using="c"` rather than choosing
a cache, and `{% cache 500 sidebar.name %}` names one with a dot in it.
In the other direction the grammar is more permissive only where it cannot know better: the names a
`{% load %}` brings in live in Python, so `{{ x|lenght }}` cannot be told from a filter some library
registered, and an unknown tag's arguments are assumed to have the shape `simple_tag` gives them.
+77 -6
View File
@@ -64,6 +64,56 @@ function asVariable($) {
return part("as", field("variable", $.identifier)); return part("as", field("variable", $.identifier));
} }
/**
* A tag's own keyword written where a value goes, with the tree an ordinary
* lookup would have so that a tool sees no difference. Where a keyword and an
* identifier are both valid, tree-sitter hands the parser the keyword, so the
* reading in which the word names a variable has to be built back out of it —
* and out of hidden rules, because an alias only renames the node its rule
* makes and cannot nest one inside another.
*
* Django tells such a keyword from a variable by comparing the whole bit, so
* "reversed" is its flag while "reversed.x" and "reversed|last" are lookups.
* That second group is what `continued` selects.
*
* @param {string} word
* @returns {Record<string, RuleBuilder<string>>}
*/
function wordAsValueRules(word) {
return {
[`_${word}_lookup`]: ($) => alias(word, $.identifier),
[`_${word}_dotted_lookup`]: ($) =>
seq(alias(word, $.identifier), ".", $.attribute),
[`_${word}_value`]: ($) =>
alias($[`_${word}_lookup`], $.variable_attribute),
[`_${word}_dotted_value`]: ($) =>
alias($[`_${word}_dotted_lookup`], $.variable_attribute),
[`_${word}_bare`]: ($) => alias($[`_${word}_value`], $.value),
[`_${word}_continued`]: ($) =>
choice(
seq(
alias($[`_${word}_dotted_value`], $.value),
optional(seq("|", $.filter_expression)),
),
seq(alias($[`_${word}_value`], $.value), "|", $.filter_expression),
),
};
}
/**
* The rules wordAsValueRules() built, where a value goes.
* @param {GrammarSymbols<string>} $
* @param {string} word
* @param {boolean} continued whether a "." or a "|" has to follow the word
* @returns {RuleOrLiteral}
*/
function wordAsValue($, word, continued) {
return alias(
$[`_${word}_${continued ? "continued" : "bare"}`],
$.filtered_value,
);
}
/** /**
* @param {RuleOrLiteral[]} chosen * @param {RuleOrLiteral[]} chosen
* @param {RuleOrLiteral[]} rest * @param {RuleOrLiteral[]} rest
@@ -342,7 +392,8 @@ const django = grammar({
seq($.value, optional(seq("|", $.filter_expression))), seq($.value, optional(seq("|", $.filter_expression))),
filter_expression: ($) => seq($.filter, repeat(seq("|", $.filter))), filter_expression: ($) => seq($.filter, repeat(seq("|", $.filter))),
value: ($) => choice($.literal, $.variable_attribute), value: ($) => choice($.literal, $.variable_attribute),
literal: ($) => choice($.number, $.string, $.translated_string), literal: ($) =>
choice($.number, $.string, $.translated_string, $.boolean, $.none),
// digits, with single underscores allowed between them, as Python's // digits, with single underscores allowed between them, as Python's
// int() and float() accept ("1_000") and Django's Variable relies on // int() and float() accept ("1_000") and Django's Variable relies on
number: ($) => number: ($) =>
@@ -354,6 +405,12 @@ const django = grammar({
* string: "_( 'a' )" is a syntax error there, not a translated literal. * string: "_( 'a' )" is a syntax error there, not a translated literal.
*/ */
translated_string: ($) => seq("_(", $.string, ")"), translated_string: ($) => seq("_(", $.string, ")"),
// the flags of "for" and "trans", spelled as the variable names they
// also are; see wordAsValueRules()
...wordAsValueRules("reversed"),
...wordAsValueRules("noop"),
boolean: ($) => choice("True", "False"),
none: ($) => "None",
attribute: ($) => /[a-zA-Z0-9][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9][a-zA-Z0-9_]*)*/, attribute: ($) => /[a-zA-Z0-9][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9][a-zA-Z0-9_]*)*/,
variable_attribute: ($) => variable_attribute: ($) =>
seq($.identifier, optional(seq(".", $.attribute))), seq($.identifier, optional(seq(".", $.attribute))),
@@ -648,10 +705,19 @@ const django = grammar({
), ),
), ),
part("in"), part("in"),
part($.filtered_value), // do_for reads the flag off the end and takes the sequence from the
// do_for reads this off the end and takes the sequence from the bit // bit before it, so the flag comes last or not at all. A bare
// before it, so it comes last or not at all // "reversed" in the sequence position leaves nothing to iterate,
optional(part("reversed")), // naming a sequence only when a lookup or filter continues it or
// when the flag follows it as well
choice(
seq(part($.filtered_value), optional(part("reversed"))),
seq(
part(wordAsValue($, "reversed", true)),
optional(part("reversed")),
),
seq(part(wordAsValue($, "reversed", false)), part("reversed")),
),
), ),
optional($.template), optional($.template),
), ),
@@ -811,7 +877,12 @@ const django = grammar({
optional( optional(
arrangements([ arrangements([
part("noop"), part("noop"),
part("context", $.filtered_value), // do_translate refuses "as" and "noop" as the context, comparing
// the whole bit, so a lookup that only begins with one is fine
part(
"context",
choice($.filtered_value, wordAsValue($, "noop", true)),
),
asVariable($), asVariable($),
]), ]),
), ),
+76
View File
@@ -0,0 +1,76 @@
; Which parts of a template render conditionally, for a tool checking that a
; variable is defined on every path that reaches a use of it.
;
; Django binds an "as" target straight into the context, and a tag that pushes
; no context leaves that binding visible after its group closes. So
;
; {% if a %}{% now "Y" as n %}{% endif %}{{ n }}
;
; renders the year when a is truthy and string_if_invalid ("" by default) when
; it is not — a silent empty string, not an error. queries/locals.scm cannot
; see that: a locals query matches a reference to the definition whose scope
; contains it and has no notion of a path that may not be taken.
;
; The halves a tool joins, by grouping matches on the @conditional.group node:
;
; @conditional.group the whole construct
; @conditional.branch one alternative; at most one of a group's branches
; renders on any given pass
; @conditional.default the branch that renders when no other one does
;
; A group is exhaustive iff one of its matches carries a @conditional.default.
; If it is not, some pass through it binds nothing, so every definition inside
; is conditional. If it is, a definition escapes only when every branch makes
; it. Each capture names both halves of the pair, so no ancestor walk is needed
; and no clause has to be attributed to a group by name.
;
; Branches nest, so the same test applies again to a group inside a branch.
;
; A branch that is also a @local.scope in queries/locals.scm confines its
; bindings whether or not it renders; the two queries compose and the scope
; decides first.
; if/elif are alternatives tested in order; else runs when none matched. None
; of the three pushes a context, so this is the case the check exists for.
(if_group
[
(if_clause)
(elif_clause)
] @conditional.branch) @conditional.group
(if_group
(else_clause) @conditional.default) @conditional.group
; ifchanged renders its body only when the watched value differs from the
; previous pass, and its else otherwise. It pushes no context either.
(ifchanged_group
(ifchanged_clause) @conditional.branch) @conditional.group
(ifchanged_group
(else_clause) @conditional.default) @conditional.group
; The loop body runs once per item and empty runs when there are none, so the
; two partition every render. Both are inside ForNode's context.push() — the
; empty branch included, which is easy to miss — so locals.scm scopes both and
; nothing escapes either way. Named here because the construct does branch, and
; because a tool reading only this file should not have to infer that it does.
(for_group
(for_clause) @conditional.branch) @conditional.group
(for_group
(empty_clause) @conditional.default) @conditional.group
; A cache body is skipped entirely on a cache hit, and CacheNode pushes no
; context, so a binding made inside it reaches the rest of the template on the
; first render and is gone on the next. One branch and no default: never
; exhaustive, which is the right answer.
(cache_group
(cache_clause) @conditional.branch) @conditional.group
; Not here, deliberately: block and partialdef bodies may also fail to render —
; a child template overrides the block, and a partial renders only where
; {% partial %} names it — but both clauses are @local.scope in locals.scm, so
; nothing they bind escapes to be checked. Bodies that always render once
; (with, filter, autoescape, spaceless, language, localize, localtime,
; timezone) are not conditional at all, and comment, verbatim and
; blocktranslate bodies hold no tags to bind anything.
+2
View File
@@ -41,6 +41,8 @@ variable: (identifier) @variable.parameter
(number) @number (number) @number
(string) @string (string) @string
(boolean) @constant.builtin
(none) @constant.builtin
(library (library
(identifier) @module) (identifier) @module)
+7
View File
@@ -11,6 +11,13 @@ variable: (identifier) @local.definition
(block_clause) @local.scope (block_clause) @local.scope
(partialdef_clause) @local.scope (partialdef_clause) @local.scope
; ForNode renders nodelist_empty inside the same context.push() as the loop
; body, so "{% empty %}" is part of the loop's scope even though it is a
; separate clause: "{% for x in ys %}a{% empty %}{% now "Y" as n %}{% endfor %}{{ n }}"
; is empty. queries/conditionals.scm names both clauses as branches of the
; same group and relies on this.
(empty_clause) @local.scope
; blocktranslate pushes a context for its "with" and "count" bindings. Its ; blocktranslate pushes a context for its "with" and "count" bindings. Its
; asvar target is not one of them: it is written in the same tag but assigned ; asvar target is not one of them: it is written in the same tag but assigned
; outside it, and carries an "asvar" field rather than "variable" so that this ; outside it, and carries an "asvar" field rather than "variable" so that this
+549 -40
View File
@@ -122,6 +122,14 @@
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "translated_string" "name": "translated_string"
},
{
"type": "SYMBOL",
"name": "boolean"
},
{
"type": "SYMBOL",
"name": "none"
} }
] ]
}, },
@@ -150,6 +158,263 @@
} }
] ]
}, },
"_reversed_lookup": {
"type": "ALIAS",
"content": {
"type": "STRING",
"value": "reversed"
},
"named": true,
"value": "identifier"
},
"_reversed_dotted_lookup": {
"type": "SEQ",
"members": [
{
"type": "ALIAS",
"content": {
"type": "STRING",
"value": "reversed"
},
"named": true,
"value": "identifier"
},
{
"type": "STRING",
"value": "."
},
{
"type": "SYMBOL",
"name": "attribute"
}
]
},
"_reversed_value": {
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_reversed_lookup"
},
"named": true,
"value": "variable_attribute"
},
"_reversed_dotted_value": {
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_reversed_dotted_lookup"
},
"named": true,
"value": "variable_attribute"
},
"_reversed_bare": {
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_reversed_value"
},
"named": true,
"value": "value"
},
"_reversed_continued": {
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_reversed_dotted_value"
},
"named": true,
"value": "value"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "|"
},
{
"type": "SYMBOL",
"name": "filter_expression"
}
]
},
{
"type": "BLANK"
}
]
}
]
},
{
"type": "SEQ",
"members": [
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_reversed_value"
},
"named": true,
"value": "value"
},
{
"type": "STRING",
"value": "|"
},
{
"type": "SYMBOL",
"name": "filter_expression"
}
]
}
]
},
"_noop_lookup": {
"type": "ALIAS",
"content": {
"type": "STRING",
"value": "noop"
},
"named": true,
"value": "identifier"
},
"_noop_dotted_lookup": {
"type": "SEQ",
"members": [
{
"type": "ALIAS",
"content": {
"type": "STRING",
"value": "noop"
},
"named": true,
"value": "identifier"
},
{
"type": "STRING",
"value": "."
},
{
"type": "SYMBOL",
"name": "attribute"
}
]
},
"_noop_value": {
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_lookup"
},
"named": true,
"value": "variable_attribute"
},
"_noop_dotted_value": {
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_dotted_lookup"
},
"named": true,
"value": "variable_attribute"
},
"_noop_bare": {
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_value"
},
"named": true,
"value": "value"
},
"_noop_continued": {
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_dotted_value"
},
"named": true,
"value": "value"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "|"
},
{
"type": "SYMBOL",
"name": "filter_expression"
}
]
},
{
"type": "BLANK"
}
]
}
]
},
{
"type": "SEQ",
"members": [
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_value"
},
"named": true,
"value": "value"
},
{
"type": "STRING",
"value": "|"
},
{
"type": "SYMBOL",
"name": "filter_expression"
}
]
}
]
},
"boolean": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "True"
},
{
"type": "STRING",
"value": "False"
}
]
},
"none": {
"type": "STRING",
"value": "None"
},
"attribute": { "attribute": {
"type": "PATTERN", "type": "PATTERN",
"value": "[a-zA-Z0-9][a-zA-Z0-9_]*(?:\\.[a-zA-Z0-9][a-zA-Z0-9_]*)*" "value": "[a-zA-Z0-9][a-zA-Z0-9_]*(?:\\.[a-zA-Z0-9][a-zA-Z0-9_]*)*"
@@ -3401,19 +3666,6 @@
} }
] ]
}, },
{
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "SYMBOL",
"name": "filtered_value"
}
]
},
{ {
"type": "CHOICE", "type": "CHOICE",
"members": [ "members": [
@@ -3421,17 +3673,120 @@
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
{ {
"type": "PATTERN", "type": "SEQ",
"value": "[ \\t\\r\\n]+" "members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "SYMBOL",
"name": "filtered_value"
}
]
}, },
{ {
"type": "STRING", "type": "CHOICE",
"value": "reversed" "members": [
{
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "STRING",
"value": "reversed"
}
]
},
{
"type": "BLANK"
}
]
} }
] ]
}, },
{ {
"type": "BLANK" "type": "SEQ",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_reversed_continued"
},
"named": true,
"value": "filtered_value"
}
]
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "STRING",
"value": "reversed"
}
]
},
{
"type": "BLANK"
}
]
}
]
},
{
"type": "SEQ",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_reversed_bare"
},
"named": true,
"value": "filtered_value"
}
]
},
{
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "STRING",
"value": "reversed"
}
]
}
]
} }
] ]
}, },
@@ -6709,8 +7064,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
@@ -6753,8 +7122,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
@@ -6905,8 +7288,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
@@ -6933,8 +7330,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
@@ -6962,8 +7373,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
@@ -7006,8 +7431,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
@@ -7080,8 +7519,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
@@ -7141,8 +7594,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
@@ -7336,8 +7803,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
@@ -7397,8 +7878,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
@@ -7458,8 +7953,22 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "CHOICE",
"name": "filtered_value" "members": [
{
"type": "SYMBOL",
"name": "filtered_value"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_noop_continued"
},
"named": true,
"value": "filtered_value"
}
]
} }
] ]
} }
+25
View File
@@ -396,6 +396,11 @@
] ]
} }
}, },
{
"type": "boolean",
"named": true,
"fields": {}
},
{ {
"type": "cache_clause", "type": "cache_clause",
"named": true, "named": true,
@@ -1699,6 +1704,14 @@
"multiple": false, "multiple": false,
"required": true, "required": true,
"types": [ "types": [
{
"type": "boolean",
"named": true
},
{
"type": "none",
"named": true
},
{ {
"type": "number", "type": "number",
"named": true "named": true
@@ -2650,6 +2663,14 @@
"type": ">=", "type": ">=",
"named": false "named": false
}, },
{
"type": "False",
"named": false
},
{
"type": "True",
"named": false
},
{ {
"type": "_(", "type": "_(",
"named": false "named": false
@@ -3074,6 +3095,10 @@
"type": "media", "type": "media",
"named": false "named": false
}, },
{
"type": "none",
"named": true
},
{ {
"type": "noop", "type": "noop",
"named": false "named": false
+41512 -39230
View File
File diff suppressed because it is too large. Load diff
+86
View File
@@ -455,3 +455,89 @@ For With Reversed Before In
{% for x reversed in y %}a{% endfor %} {% for x reversed in y %}a{% endfor %}
--- ---
==================
For Over A Variable Named Reversed
==================
{% for x in reversed reversed %}a{% endfor %}
---
(template
(content)
(for_group
(for_clause
variable: (identifier)
(filtered_value
(value
(variable_attribute
(identifier))))
(template
(content))))
(content))
==================
For Over An Attribute Of A Variable Named Reversed
==================
{% for x in reversed.items %}a{% endfor %}
---
(template
(content)
(for_group
(for_clause
variable: (identifier)
(filtered_value
(value
(variable_attribute
(identifier)
(attribute))))
(template
(content))))
(content))
==================
For Over A Filtered Variable Named Reversed
==================
{% for x in reversed|last %}a{% endfor %}
---
(template
(content)
(for_group
(for_clause
variable: (identifier)
(filtered_value
(value
(variable_attribute
(identifier)))
(filter_expression
(filter)))
(template
(content))))
(content))
==================
For With Reversed As Its Whole Sequence
:error
==================
{% for x in reversed %}a{% endfor %}
---
==================
For With Reversed Three Times
:error
==================
{% for x in reversed reversed reversed %}a{% endfor %}
---
+55
View File
@@ -488,3 +488,58 @@ Language Name Filter With An Argument
{{ "de"|language_name:1 }} {{ "de"|language_name:1 }}
--- ---
==================
Translate With A Context Named After Noop
:error
==================
{% trans "hi" context noop %}
---
==================
Translate With A Context From An Attribute Of Noop
==================
{% trans "hi" context noop.x %}
---
(template
(content)
(translate
(filtered_value
(value
(literal
(string))))
(filtered_value
(value
(variable_attribute
(identifier)
(attribute)))))
(content))
==================
Translate With A Filtered Context Named After Noop
==================
{% trans "hi" context noop|upper %}
---
(template
(content)
(translate
(filtered_value
(value
(literal
(string))))
(filtered_value
(value
(variable_attribute
(identifier)))
(filter_expression
(filter))))
(content))
+26
View File
@@ -156,3 +156,29 @@ If Operator Adjacent To Value
{% if 1and 2 %}yes{% endif %} {% if 1and 2 %}yes{% endif %}
--- ---
==================
If Is Not None
==================
{% if status is not None %}yes{% endif %}
---
(template
(content)
(if_group
(if_clause
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))
(binaryOperator)
(filtered_value
(value
(literal
(none)))))
(template
(content))))
(content))
+100
View File
@@ -361,3 +361,103 @@ Template Variable With Spaced Filter
(filter_expression (filter_expression
(filter)))) (filter))))
(content)) (content))
==================
Template Variable True
==================
{{ True }}
---
(template
(content)
(template_variable
(filtered_value
(value
(literal
(boolean)))))
(content))
==================
Template Variable False
==================
{{ False }}
---
(template
(content)
(template_variable
(filtered_value
(value
(literal
(boolean)))))
(content))
==================
Template Variable None
==================
{{ None }}
---
(template
(content)
(template_variable
(filtered_value
(value
(literal
(none)))))
(content))
==================
Template Variable Boolean As Filter Argument
==================
{{ value|default:True }}
---
(template
(content)
(template_variable
(filtered_value
(value
(variable_attribute
(identifier)))
(filter_expression
(filter
argument: (value
(literal
(boolean)))))))
(content))
==================
Template Variable Name Beginning With A Literal
==================
{{ Truest }}
---
(template
(content)
(template_variable
(filtered_value
(value
(variable_attribute
(identifier)))))
(content))
==================
Template Variable Attribute Of A Literal
:error
==================
{{ None.x }}
---
+21
View File
@@ -111,3 +111,24 @@ With Repeating A Binding Of The Tag Before It
(template (template
(content)))) (content))))
(content)) (content))
==================
With A Binding Named After A Literal
==================
{% with True=1 %}a{% endwith %}
---
(template
(content)
(with_group
(with_clause
variable: (identifier)
(filtered_value
(value
(literal
(number))))
(template
(content))))
(content))