added more utility rules and updated example
This commit is contained in:
1 parent
036928d628
commit
cb8c9f9c0b
12 files changed
+34942
-32273
No files matched your search
+184
-77
@@ -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),
|
||||
),
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user