support blocktrans
This commit is contained in:
1 parent
516318051f
commit
f93938c277
7 files changed
+57133
-56770
No files matched your search
@@ -130,15 +130,19 @@ is accepted: these names are common enough that shadowing them is the mistake.
|
|||||||
|
|
||||||
### External scanner (`src/scanner.c`)
|
### External scanner (`src/scanner.c`)
|
||||||
|
|
||||||
Eleven external tokens, for the constraints a context-free grammar cannot express:
|
Twelve external tokens, for the constraints a context-free grammar cannot express:
|
||||||
|
|
||||||
- `push_block`/`push_partial`/`push_verbatim` and the matching `pop_*` — name matching for
|
- `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
|
`{% 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
|
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`).
|
never a second candidate name to try. Serialized as `<kind><name> ` per entry (`tag_kind_chars`).
|
||||||
- `verbatim_content`, `comment_content` — raw text up to the matching close tag.
|
- `verbatim_content`, `comment_content` — raw text up to the matching close tag.
|
||||||
- `_bt_open`, `_bt_option` — which options a `{% blocktranslate %}` has already been given, so that a
|
- `_tag_open`, `_bt_option`, `_kwarg_name` — what a tag has already been given, so that a repeated
|
||||||
repeated one is refused. Both are zero width and hidden, so no tree changes. The state is one byte of
|
argument is refused: `_bt_option` for `{% blocktranslate %}`'s five options, `_kwarg_name` for the
|
||||||
|
`name=value` arguments of a tag registered with `simple_tag`, whose names are open-ended and so are kept
|
||||||
|
as names rather than as a mask. `_kwarg_name` returns nothing unless the name is followed by `=`, which
|
||||||
|
is also what tells an argument apart from the `as name` clause or the end of the tag. `_tag_open` clears
|
||||||
|
both, and every rule that uses either has one. Both are zero width and hidden, so no tree changes. The state is one byte of
|
||||||
flags, not a stack, because the tag cannot nest: its body admits no tags. It lives outside the
|
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
|
`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.
|
status character, so a GLR stack split copies it like everything else.
|
||||||
@@ -154,9 +158,11 @@ Block-name matching does — it decides which close tag closes what.
|
|||||||
and the reason is worth keeping straight: its five options are a fixed set, so uniqueness _is_ expressible —
|
and the reason is worth keeping straight: its five options are a fixed set, so uniqueness _is_ expressible —
|
||||||
`arrangements` expresses exactly that — and the scanner is standing in for a grammar that would be correct
|
`arrangements` expresses exactly that — and the scanner is standing in for a grammar that would be correct
|
||||||
but takes minutes to build. Where the set is open, as with `{% querystring %}`'s `**kwargs`, no grammar
|
but takes minutes to build. Where the set is open, as with `{% querystring %}`'s `**kwargs`, no grammar
|
||||||
can express it — but the scanner still could, by remembering the names a tag has been given rather than a
|
can express it at all, and the scanner remembers the names the tag has been given instead of a mask of
|
||||||
fixed mask of them. That is not done, and it is what the `{% querystring page=2 page=3 %}` divergence below
|
known ones. Which tags get that guard is itself a differential question: only `parse_bits` refuses a
|
||||||
is waiting on.
|
repeat, so `{% with a=1 a=2 %}`, `{% include "t" with a=1 a=2 %}` and `{% blocktranslate with a=1 a=2 %}`
|
||||||
|
are all accepted by Django — `token_kwargs` builds a dict and the later value simply wins — and must not
|
||||||
|
be guarded.
|
||||||
|
|
||||||
Zero-width guards carry one hazard: `recover_with_missing` can supply one without the scanner running. The
|
Zero-width guards carry one hazard: `recover_with_missing` can supply one without the scanner running. The
|
||||||
`if (valid_symbols[MatcherError]) return false;` near the top of `scan` is what stops a guard being scanned
|
`if (valid_symbols[MatcherError]) return false;` near the top of `scan` is what stops a guard being scanned
|
||||||
@@ -210,8 +216,6 @@ 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.
|
||||||
- `{% querystring page=2 page=3 %}` (a repeated `**kwargs` key) is a Django error but is not expressible in
|
|
||||||
a context-free grammar. Repeats _are_ rejected wherever the signature names its arguments.
|
|
||||||
- `{% csp_nonce_attr "a" media="b" %}` is accepted, matching Django, which only rejects it at render time.
|
- `{% csp_nonce_attr "a" media="b" %}` is accepted, matching Django, which only rejects it at render time.
|
||||||
- A misspelled tag or filter cannot be caught: `{{ x|lenght }}` is indistinguishable from a filter some
|
- A misspelled tag or filter cannot be caught: `{{ x|lenght }}` is indistinguishable from a filter some
|
||||||
library registered. Cross-referencing names against `{% load %}` needs the project's Python, so it
|
library registered. Cross-referencing names against `{% load %}` needs the project's Python, so it
|
||||||
|
|||||||
+10
-4
@@ -137,7 +137,12 @@ function simpleTag($, tag, args = true, kwargs = true) {
|
|||||||
parts.push(optional(rule));
|
parts.push(optional(rule));
|
||||||
}
|
}
|
||||||
if (kwargs === true) {
|
if (kwargs === true) {
|
||||||
parts.push(repeat(part(seq($.identifier, "=", $.filtered_value))));
|
// parse_bits refuses a keyword argument the tag already holds, whatever
|
||||||
|
// **kwargs it takes, so the scanner keeps the names rather than the grammar
|
||||||
|
parts.unshift($._tag_open);
|
||||||
|
parts.push(
|
||||||
|
repeat(part(seq($._kwarg_name, $.identifier, "=", $.filtered_value))),
|
||||||
|
);
|
||||||
} else if (kwargs) {
|
} else if (kwargs) {
|
||||||
// a known signature: any of the names it accepts, in any order, none twice
|
// a known signature: any of the names it accepts, in any order, none twice
|
||||||
const named = Object.entries(kwargs).map(([name, value]) =>
|
const named = Object.entries(kwargs).map(([name, value]) =>
|
||||||
@@ -302,8 +307,9 @@ const django = grammar({
|
|||||||
$.push_verbatim,
|
$.push_verbatim,
|
||||||
$.verbatim_content,
|
$.verbatim_content,
|
||||||
$.comment_content,
|
$.comment_content,
|
||||||
$._bt_open,
|
$._tag_open,
|
||||||
$._bt_option,
|
$._bt_option,
|
||||||
|
$._kwarg_name,
|
||||||
],
|
],
|
||||||
reserved: {
|
reserved: {
|
||||||
global: ($) => ["not", "if", "in", "is", "as", "for", "from"],
|
global: ($) => ["not", "if", "in", "is", "as", "for", "from"],
|
||||||
@@ -492,14 +498,14 @@ const django = grammar({
|
|||||||
...["blocktrans", "blocktranslate"].map((name) =>
|
...["blocktrans", "blocktranslate"].map((name) =>
|
||||||
choice(
|
choice(
|
||||||
seq(
|
seq(
|
||||||
block(name, $._bt_open, repeat($._translate_option)),
|
block(name, $._tag_open, repeat($._translate_option)),
|
||||||
optional($._translate_body),
|
optional($._translate_body),
|
||||||
block("end" + name),
|
block("end" + name),
|
||||||
),
|
),
|
||||||
seq(
|
seq(
|
||||||
block(
|
block(
|
||||||
name,
|
name,
|
||||||
$._bt_open,
|
$._tag_open,
|
||||||
repeat($._translate_option),
|
repeat($._translate_option),
|
||||||
$._translate_count,
|
$._translate_count,
|
||||||
repeat($._translate_option),
|
repeat($._translate_option),
|
||||||
|
|||||||
+25
-5
@@ -1383,7 +1383,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "_bt_open"
|
"name": "_tag_open"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "REPEAT",
|
"type": "REPEAT",
|
||||||
@@ -1501,7 +1501,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "_bt_open"
|
"name": "_tag_open"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "REPEAT",
|
"type": "REPEAT",
|
||||||
@@ -1684,7 +1684,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "_bt_open"
|
"name": "_tag_open"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "REPEAT",
|
"type": "REPEAT",
|
||||||
@@ -1802,7 +1802,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "_bt_open"
|
"name": "_tag_open"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "REPEAT",
|
"type": "REPEAT",
|
||||||
@@ -2688,6 +2688,10 @@
|
|||||||
"context_name": "tag_name"
|
"context_name": "tag_name"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_tag_open"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "REPEAT",
|
"type": "REPEAT",
|
||||||
"content": {
|
"content": {
|
||||||
@@ -2716,6 +2720,10 @@
|
|||||||
{
|
{
|
||||||
"type": "SEQ",
|
"type": "SEQ",
|
||||||
"members": [
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_kwarg_name"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "identifier"
|
"name": "identifier"
|
||||||
@@ -5664,6 +5672,10 @@
|
|||||||
"value": "querystring"
|
"value": "querystring"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_tag_open"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "REPEAT",
|
"type": "REPEAT",
|
||||||
"content": {
|
"content": {
|
||||||
@@ -5692,6 +5704,10 @@
|
|||||||
{
|
{
|
||||||
"type": "SEQ",
|
"type": "SEQ",
|
||||||
"members": [
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_kwarg_name"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "identifier"
|
"name": "identifier"
|
||||||
@@ -7869,11 +7885,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "_bt_open"
|
"name": "_tag_open"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "_bt_option"
|
"name": "_bt_option"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_kwarg_name"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inline": [],
|
"inline": [],
|
||||||
|
|||||||
+56879
-56749
File diff suppressed because it is too large.
Load diff
+104
-4
@@ -21,11 +21,14 @@ enum TokenType {
|
|||||||
VerbatimContent,
|
VerbatimContent,
|
||||||
/* raw text inside a comment block, up through (but excluding) the endcomment tag */
|
/* raw text inside a comment block, up through (but excluding) the endcomment tag */
|
||||||
CommentContent,
|
CommentContent,
|
||||||
/* zero width, at the start of a blocktranslate tag: forget the options seen */
|
/* zero width, at the start of a tag: forget what the tag before it held */
|
||||||
TranslateOpen,
|
TagOpen,
|
||||||
/* zero width, before an option of a blocktranslate tag: record it, and fail
|
/* zero width, before an option of a blocktranslate tag: record it, and fail
|
||||||
* if that option has already been written once */
|
* if that option has already been written once */
|
||||||
TranslateOption,
|
TranslateOption,
|
||||||
|
/* zero width, before the name of a "name=value" argument: record the name,
|
||||||
|
* and fail if the tag has already been given one by that name */
|
||||||
|
KwargName,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* The options blocktranslate takes, in the order of the bits recording them.
|
/* The options blocktranslate takes, in the order of the bits recording them.
|
||||||
@@ -66,6 +69,11 @@ struct Scanner {
|
|||||||
* translate_options. Kept out of the union below, whose members share their
|
* translate_options. Kept out of the union below, whose members share their
|
||||||
* storage with each other. */
|
* storage with each other. */
|
||||||
uint8_t translate_seen;
|
uint8_t translate_seen;
|
||||||
|
/* the "name=value" argument names the tag being read has been given, each
|
||||||
|
* followed by NAME_SEP. parse_bits refuses a repeated keyword argument, and
|
||||||
|
* the set of names is open, so this remembers the names rather than a mask
|
||||||
|
* of known ones. Kept out of the union, as translate_seen is. */
|
||||||
|
Name seen_kwargs;
|
||||||
union {
|
union {
|
||||||
Stack stack;
|
Stack stack;
|
||||||
char error [ERROR_SIZE];
|
char error [ERROR_SIZE];
|
||||||
@@ -82,6 +90,7 @@ static enum TokenType pop_token_for_kind(TagKind kind) {
|
|||||||
|
|
||||||
static void reset_scanner(struct Scanner *const scanner) {
|
static void reset_scanner(struct Scanner *const scanner) {
|
||||||
scanner->translate_seen = 0;
|
scanner->translate_seen = 0;
|
||||||
|
array_delete(&scanner->seen_kwargs);
|
||||||
if (scanner->has_error) {
|
if (scanner->has_error) {
|
||||||
scanner->error[0] = '\0';
|
scanner->error[0] = '\0';
|
||||||
scanner->has_error = false;
|
scanner->has_error = false;
|
||||||
@@ -160,6 +169,17 @@ write_serialization_error:;
|
|||||||
iter += write_code(scanner->has_error + '0', iter);
|
iter += write_code(scanner->has_error + '0', iter);
|
||||||
// five options fit in five bits, which stays inside printable ASCII
|
// five options fit in five bits, which stays inside printable ASCII
|
||||||
iter += write_code(scanner->translate_seen + '0', iter);
|
iter += write_code(scanner->translate_seen + '0', iter);
|
||||||
|
for (unsigned i = 0; i < scanner->seen_kwargs.size; ++i) {
|
||||||
|
int32_t code = *array_get(&scanner->seen_kwargs, i);
|
||||||
|
unsigned write_amt = write_code(code, iter);
|
||||||
|
if (write_amt == 0) {
|
||||||
|
scanner_error(scanner, "bad code from keyword argument name");
|
||||||
|
goto write_serialization_error;
|
||||||
|
}
|
||||||
|
iter += write_amt;
|
||||||
|
}
|
||||||
|
// a name holds no newline, so this ends the list without escaping anything
|
||||||
|
iter += write_code('\n', iter);
|
||||||
if (scanner->has_error) {
|
if (scanner->has_error) {
|
||||||
for (int i = 0; i < ERROR_SIZE; ++i) {
|
for (int i = 0; i < ERROR_SIZE; ++i) {
|
||||||
char value = scanner->error[i];
|
char value = scanner->error[i];
|
||||||
@@ -266,6 +286,29 @@ void tree_sitter_django_external_scanner_deserialize(
|
|||||||
}
|
}
|
||||||
scanner->translate_seen = (uint8_t) (seen - '0');
|
scanner->translate_seen = (uint8_t) (seen - '0');
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int32_t item;
|
||||||
|
bytes_read = read_code(buffer, &item);
|
||||||
|
if (!bytes_read) {
|
||||||
|
scanner_error(scanner, "received invalid utf8 byte");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
consumed += bytes_read;
|
||||||
|
if (consumed > length) {
|
||||||
|
scanner_error(scanner, "keyword argument names truncated by length");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buffer += bytes_read;
|
||||||
|
if (item == '\n') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (item != NAME_SEP && !check_name_char(item)) {
|
||||||
|
scanner_error(scanner, "invalid character in keyword argument name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
array_push(&scanner->seen_kwargs, item);
|
||||||
|
}
|
||||||
|
|
||||||
switch (code) {
|
switch (code) {
|
||||||
default:
|
default:
|
||||||
scanner_error(scanner, "received unrecognized scanner status");
|
scanner_error(scanner, "received unrecognized scanner status");
|
||||||
@@ -418,6 +461,54 @@ static int read_translate_option(TSLexer *const lexer) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Reads the name of a "name=value" argument, and reports whether the tag can
|
||||||
|
* still take it. A name not followed by "=" is not one of these arguments at
|
||||||
|
* all, which is what tells this apart from the "as name" clause or the end of
|
||||||
|
* the tag; the name is only kept once the "=" has been seen.
|
||||||
|
*
|
||||||
|
* The names are held end to end, each followed by NAME_SEP, which no name
|
||||||
|
* holds. The candidate is appended first and dropped again if it turns out to
|
||||||
|
* be a repeat, so nothing is kept unless the token is returned. */
|
||||||
|
static bool scan_kwarg_name(struct Scanner *const scanner, TSLexer *const lexer) {
|
||||||
|
if (!check_name_start_char(lexer->lookahead)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const unsigned start = scanner->seen_kwargs.size;
|
||||||
|
while (check_name_char(lexer->lookahead)) {
|
||||||
|
array_push(&scanner->seen_kwargs, lexer->lookahead);
|
||||||
|
lexer->advance(lexer, false);
|
||||||
|
}
|
||||||
|
const unsigned size = scanner->seen_kwargs.size - start;
|
||||||
|
if (lexer->lookahead != '=') {
|
||||||
|
scanner->seen_kwargs.size = start;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (unsigned i = 0; i < start;) {
|
||||||
|
unsigned end = i;
|
||||||
|
while (end < start && *array_get(&scanner->seen_kwargs, end) != NAME_SEP) {
|
||||||
|
++end;
|
||||||
|
}
|
||||||
|
if (end - i == size) {
|
||||||
|
bool same = true;
|
||||||
|
for (unsigned j = 0; j < size; ++j) {
|
||||||
|
if (*array_get(&scanner->seen_kwargs, i + j)
|
||||||
|
!= *array_get(&scanner->seen_kwargs, start + j)) {
|
||||||
|
same = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (same) {
|
||||||
|
// the tag already holds an argument by this name
|
||||||
|
scanner->seen_kwargs.size = start;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = end + 1;
|
||||||
|
}
|
||||||
|
array_push(&scanner->seen_kwargs, NAME_SEP);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const char endverbatim_chars[] = "endverbatim";
|
const char endverbatim_chars[] = "endverbatim";
|
||||||
|
|
||||||
/* Called right after consuming "{%"; checks whether what follows closes the
|
/* Called right after consuming "{%"; checks whether what follows closes the
|
||||||
@@ -582,10 +673,19 @@ bool tree_sitter_django_external_scanner_scan(
|
|||||||
* read. They belong above the loop below, which skips the separator the
|
* read. They belong above the loop below, which skips the separator the
|
||||||
* grammar still has to match, and below the MatcherError check, which is what
|
* grammar still has to match, and below the MatcherError check, which is what
|
||||||
* keeps them from being scanned during error recovery. */
|
* keeps them from being scanned during error recovery. */
|
||||||
if (valid_symbols[TranslateOpen]) {
|
if (valid_symbols[TagOpen]) {
|
||||||
lexer->mark_end(lexer);
|
lexer->mark_end(lexer);
|
||||||
scanner->translate_seen = 0;
|
scanner->translate_seen = 0;
|
||||||
lexer->result_symbol = TranslateOpen;
|
array_delete(&scanner->seen_kwargs);
|
||||||
|
lexer->result_symbol = TagOpen;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (valid_symbols[KwargName]) {
|
||||||
|
lexer->mark_end(lexer);
|
||||||
|
if (!scan_kwarg_name(scanner, lexer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lexer->result_symbol = KwargName;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (valid_symbols[TranslateOption]) {
|
if (valid_symbols[TranslateOption]) {
|
||||||
|
|||||||
@@ -284,3 +284,64 @@ A Name Reserved For A Tag Is Still An Argument
|
|||||||
(variable_attribute
|
(variable_attribute
|
||||||
(identifier)))))
|
(identifier)))))
|
||||||
(content))
|
(content))
|
||||||
|
|
||||||
|
==================
|
||||||
|
Custom Tag With A Keyword Argument Twice
|
||||||
|
:error
|
||||||
|
==================
|
||||||
|
|
||||||
|
{% mytag a=1 a=2 %}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
==================
|
||||||
|
Custom Tag With Keyword Arguments And As
|
||||||
|
==================
|
||||||
|
|
||||||
|
{% mytag a=1 b=2 as out %}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
(template
|
||||||
|
(content)
|
||||||
|
(custom_tag
|
||||||
|
tag: (identifier)
|
||||||
|
(identifier)
|
||||||
|
(filtered_value
|
||||||
|
(value
|
||||||
|
(literal
|
||||||
|
(number))))
|
||||||
|
(identifier)
|
||||||
|
(filtered_value
|
||||||
|
(value
|
||||||
|
(literal
|
||||||
|
(number))))
|
||||||
|
variable: (identifier))
|
||||||
|
(content))
|
||||||
|
|
||||||
|
==================
|
||||||
|
Custom Tag Repeating A Keyword Of The Tag Before It
|
||||||
|
==================
|
||||||
|
|
||||||
|
{% mytag page=1 %}{% mytag page=2 %}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
(template
|
||||||
|
(content)
|
||||||
|
(custom_tag
|
||||||
|
tag: (identifier)
|
||||||
|
(identifier)
|
||||||
|
(filtered_value
|
||||||
|
(value
|
||||||
|
(literal
|
||||||
|
(number)))))
|
||||||
|
(custom_tag
|
||||||
|
tag: (identifier)
|
||||||
|
(identifier)
|
||||||
|
(filtered_value
|
||||||
|
(value
|
||||||
|
(literal
|
||||||
|
(number)))))
|
||||||
|
(content))
|
||||||
@@ -74,3 +74,45 @@ Querystring With Expression Argument And As
|
|||||||
(number))))
|
(number))))
|
||||||
variable: (identifier))
|
variable: (identifier))
|
||||||
(content))
|
(content))
|
||||||
|
|
||||||
|
==================
|
||||||
|
Querystring With Two Keys
|
||||||
|
==================
|
||||||
|
|
||||||
|
{% querystring page=2 size=3 %}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
(template
|
||||||
|
(content)
|
||||||
|
(query_string
|
||||||
|
(identifier)
|
||||||
|
(filtered_value
|
||||||
|
(value
|
||||||
|
(literal
|
||||||
|
(number))))
|
||||||
|
(identifier)
|
||||||
|
(filtered_value
|
||||||
|
(value
|
||||||
|
(literal
|
||||||
|
(number)))))
|
||||||
|
(content))
|
||||||
|
|
||||||
|
==================
|
||||||
|
Querystring With A Key Twice
|
||||||
|
:error
|
||||||
|
==================
|
||||||
|
|
||||||
|
{% querystring page=2 page=3 %}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
==================
|
||||||
|
Querystring With A Key Repeated Later
|
||||||
|
:error
|
||||||
|
==================
|
||||||
|
|
||||||
|
{% querystring a=1 b=2 a=3 %}
|
||||||
|
|
||||||
|
---
|
||||||
Reference in new issue
Block a user