add Django 6 support and simpleTag

This commit is contained in:
danhanson committed 2026-09-06 20:40:39 -05:00
1 parent e357dc17ad
commit fc36d7d7b2
11 files changed
+25759 -23427

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
django = "*"
[dev-packages]
[requires]
python_version = "3.13"
Generated
+46
View File
@@ -0,0 +1,46 @@
{
"_meta": {
"hash": {
"sha256": "1d824dfb7e1efaa95eefc090ec048bd1e901b57997b98d5b8319268f04f72176"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.13"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"asgiref": {
"hashes": [
"sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340",
"sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094"
],
"markers": "python_version >= '3.10'",
"version": "==3.12.1"
},
"django": {
"hashes": [
"sha256:585fb82bf15053c42cf52e67c2f1b54a032dca384759315fd6678ab1870d1d72",
"sha256:7ab93536d8e677aa14554c56426290c69480bf2ee68d7513d4a22f57d6bfeb84"
],
"index": "pypi",
"markers": "python_version >= '3.12'",
"version": "==6.1.1"
},
"sqlparse": {
"hashes": [
"sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9",
"sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f"
],
"markers": "python_version >= '3.10'",
"version": "==0.6.0"
}
},
"develop": {}
}
+108 -18
View File
@@ -54,6 +54,101 @@ function block(tag, ...args) {
); );
} }
/**
* The "as name" clause that binds a tag's result in the context. Django spells
* it the same way in every tag that takes one, so the grammar names the target
* the same way too, matching the bindings in "for" and "with".
* @param {GrammarSymbols<string>} $
* @returns {SeqRule}
*/
function asVariable($) {
return part("as", field("variable", $.identifier));
}
/**
* @param {RuleOrLiteral[]} chosen
* @param {RuleOrLiteral[]} rest
* @param {RuleOrLiteral[]} spellings
*/
function walk(chosen, rest, spellings) {
if (chosen.length > 0) {
spellings.push(chosen.length === 1 ? chosen[0] : seq(...chosen));
}
rest.forEach((item, i) =>
walk(
[...chosen, item],
rest.filter((_, j) => j !== i),
spellings,
),
);
};
/**
* Every way of writing some of `items` in any order, using each at most once,
* which is how a tag's keyword arguments reach parse_bits(). There is no
* interleaving operator to say that directly, so the orderings are spelled
* out; a signature names a handful of arguments at most, and one that takes
* more takes them as **kwargs instead.
* @param {RuleOrLiteral[]} items
* @returns {ChoiceRule|RuleOrLiteral}
*/
function arrangements(items) {
if (items.length >= 5) {
throw new Error(`Too many items for arrangement: ${items.length}`);
}
/**
* @type {RuleOrLiteral[]}
*/
const spellings = [];
walk([], items, spellings);
return spellings.length === 1 ? spellings[0] : choice(...spellings);
}
/**
* A tag registered with Django's simple_tag helper: positional filter
* expressions first, then "name=value" keyword arguments, then the "as name"
* clause that Library.simple_tag gives every such tag. The helper takes the
* whitespace between the parts itself, so a caller only describes what the
* tag's Python signature accepts.
*
* Django checks the rest of the signature when it parses the tag, so what is
* spelled here is what parse_bits() would allow: too many arguments, an
* unknown keyword or a repeated one are all syntax errors there.
*
* @param {GrammarSymbols<string>} $
* @param {boolean|number} args how many positional arguments the signature
* takes: true for any number (*args), a count for at most that many, or
* false for none
* @param {boolean|Record<string, RuleOrLiteral>} kwargs true for any
* "name=value" pair (**kwargs), false for none, or the accepted names mapped
* to their value rules, each of which may be given at most once
* @param {string} tag
* @returns {SeqRule}
*/
function simpleTag($, tag, args = true, kwargs = true) {
const parts = [];
if (args === true) {
parts.push(repeat(part($.filtered_value)));
} else if (typeof args === "number" && args > 0) {
// at most `args` of them, so each further one nests inside the last
let rule = part($.filtered_value);
for (let i = 1; i < args; ++i) {
rule = seq(part($.filtered_value), optional(rule));
}
parts.push(optional(rule));
}
if (kwargs === true) {
parts.push(repeat(part(seq($.identifier, "=", $.filtered_value))));
} else if (kwargs) {
// a known signature: any of the names it accepts, in any order, none twice
const named = Object.entries(kwargs).map(([name, value]) =>
part(seq(name, "=", value)),
);
parts.push(optional(arrangements(named)));
}
return block(tag, ...parts, optional(asVariable($)));
}
/** Django's string constant, which takes any escape but a line break. */ /** Django's string constant, which takes any escape but a line break. */
const STRING = /"(?:[^"\\]|\\[^\n])*"|'(?:[^'\\]|\\[^\n])*'/; const STRING = /"(?:[^"\\]|\\[^\n])*"|'(?:[^'\\]|\\[^\n])*'/;
@@ -171,6 +266,7 @@ const django = grammar({
$.autoescape_group, $.autoescape_group,
$.block_group, $.block_group,
$.comment_group, $.comment_group,
$.csp_nonce_attr,
$.csrf_token, $.csrf_token,
$.cycle, $.cycle,
$.debug, $.debug,
@@ -211,6 +307,7 @@ const django = grammar({
seq("divisibleby:", $.value), seq("divisibleby:", $.value),
"escape", "escape",
"escapejs", "escapejs",
"escapeseq",
"filesizeformat", "filesizeformat",
"first", "first",
choice(seq("floatformat:", $.value), "floatformat"), choice(seq("floatformat:", $.value), "floatformat"),
@@ -221,7 +318,6 @@ const django = grammar({
choice(seq("json_script:", $.value), "json_script"), choice(seq("json_script:", $.value), "json_script"),
"last", "last",
"length", "length",
seq("length_is:", $.value),
"linebreaks", "linebreaks",
"linebreaksbr", "linebreaksbr",
"linenumbers", "linenumbers",
@@ -275,6 +371,9 @@ const django = grammar({
optional($.comment_content), optional($.comment_content),
block("endcomment"), block("endcomment"),
), ),
// csp_nonce_attr(context, media=None)
csp_nonce_attr: ($) =>
simpleTag($, "csp_nonce_attr", 1, { media: $.filtered_value }),
csrf_token: ($) => block("csrf_token"), csrf_token: ($) => block("csrf_token"),
cycle: ($) => cycle: ($) =>
block( block(
@@ -282,7 +381,7 @@ const django = grammar({
repeat1(part($.filtered_value)), repeat1(part($.filtered_value)),
optional( optional(
seq( seq(
part("as", field("variable", $.identifier)), asVariable($),
// only the as-form takes the flag // only the as-form takes the flag
optional(part("silent")), optional(part("silent")),
), ),
@@ -306,7 +405,7 @@ const django = grammar({
block( block(
"firstof", "firstof",
repeat1(part($.filtered_value)), repeat1(part($.filtered_value)),
optional(part("as", $.identifier)), optional(asVariable($)),
), ),
for_scope: ($) => for_scope: ($) =>
prec.left( prec.left(
@@ -365,12 +464,7 @@ const django = grammar({
part(choice("w", "p", "b")), part(choice("w", "p", "b")),
optional(part("random")), optional(part("random")),
), ),
now: ($) => now: ($) => block("now", part($.string), optional(asVariable($))),
block(
"now",
part($.string),
optional(part("as", field("name", $.identifier))),
),
partial: ($) => block("partial", part($.identifier)), partial: ($) => block("partial", part($.identifier)),
partialdef_group: ($) => partialdef_group: ($) =>
seq( seq(
@@ -378,19 +472,15 @@ const django = grammar({
optional($.template), optional($.template),
block("endpartialdef", $.pop_partial), block("endpartialdef", $.pop_partial),
), ),
query_string: ($) => // querystring(context, *args, **kwargs)
block( query_string: ($) => simpleTag($, "querystring"),
"querystring",
repeat(part($.identifier)),
repeat(part(seq($.identifier, "=", $.filtered_value))),
),
regroup: ($) => regroup: ($) =>
block( block(
"regroup", "regroup",
part($.filtered_value), part($.filtered_value),
part("by"), part("by"),
part($.attribute), part($.attribute),
optional(part("as", field("variable", $.identifier))), optional(asVariable($)),
), ),
reset_cycle: ($) => block("resetcycle", optional(part($.identifier))), reset_cycle: ($) => block("resetcycle", optional(part($.identifier))),
spaceless_group: ($) => spaceless_group: ($) =>
@@ -421,7 +511,7 @@ const django = grammar({
repeat1(part(seq($.identifier, "=", $.filtered_value))), repeat1(part(seq($.identifier, "=", $.filtered_value))),
), ),
), ),
optional(part("as", field("variable", $.identifier))), optional(asVariable($)),
), ),
verbatim_group: ($) => verbatim_group: ($) =>
seq( seq(
@@ -435,7 +525,7 @@ const django = grammar({
part($.filtered_value), part($.filtered_value),
part($.filtered_value), part($.filtered_value),
part($.filtered_value), part($.filtered_value),
optional(part("as", field("variable", $.identifier))), optional(asVariable($)),
), ),
with_group: ($) => with_group: ($) =>
seq( seq(
+2 -1
View File
@@ -51,6 +51,7 @@ tag: [
("endblock") ("endblock")
("comment") ("comment")
("endcomment") ("endcomment")
("csp_nonce_attr")
("csrf_token") ("csrf_token")
("cycle") ("cycle")
("debug") ("debug")
@@ -105,6 +106,7 @@ tag: [
("divisibleby:") ("divisibleby:")
("escape") ("escape")
("escapejs") ("escapejs")
("escapeseq")
("filesizeformat") ("filesizeformat")
("first") ("first")
("floatformat") ("floatformat")
@@ -117,7 +119,6 @@ tag: [
("json_script:") ("json_script:")
("last") ("last")
("length") ("length")
("length_is:")
("linebreaks") ("linebreaks")
("linebreaksbr") ("linebreaksbr")
("linenumbers") ("linenumbers")
+192 -17
View File
@@ -546,6 +546,10 @@
"type": "SYMBOL", "type": "SYMBOL",
"name": "comment_group" "name": "comment_group"
}, },
{
"type": "SYMBOL",
"name": "csp_nonce_attr"
},
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "csrf_token" "name": "csrf_token"
@@ -789,6 +793,10 @@
"type": "STRING", "type": "STRING",
"value": "escapejs" "value": "escapejs"
}, },
{
"type": "STRING",
"value": "escapeseq"
},
{ {
"type": "STRING", "type": "STRING",
"value": "filesizeformat" "value": "filesizeformat"
@@ -883,19 +891,6 @@
"type": "STRING", "type": "STRING",
"value": "length" "value": "length"
}, },
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "length_is:"
},
{
"type": "SYMBOL",
"name": "value"
}
]
},
{ {
"type": "STRING", "type": "STRING",
"value": "linebreaks" "value": "linebreaks"
@@ -1604,6 +1599,144 @@
} }
] ]
}, },
"csp_nonce_attr": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "{%"
},
{
"type": "CHOICE",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "BLANK"
}
]
},
{
"type": "FIELD",
"name": "tag",
"content": {
"type": "STRING",
"value": "csp_nonce_attr"
}
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "SYMBOL",
"name": "filtered_value"
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "media"
},
{
"type": "STRING",
"value": "="
},
{
"type": "SYMBOL",
"name": "filtered_value"
}
]
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "as"
},
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "FIELD",
"name": "variable",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
]
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "CHOICE",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": "%}"
}
]
},
"csrf_token": { "csrf_token": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@@ -2067,8 +2200,12 @@
"value": "[ \\t\\r\\n]+" "value": "[ \\t\\r\\n]+"
}, },
{ {
"type": "SYMBOL", "type": "FIELD",
"name": "identifier" "name": "variable",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
} }
] ]
} }
@@ -3134,7 +3271,7 @@
}, },
{ {
"type": "FIELD", "type": "FIELD",
"name": "name", "name": "variable",
"content": { "content": {
"type": "SYMBOL", "type": "SYMBOL",
"name": "identifier" "name": "identifier"
@@ -3399,7 +3536,7 @@
}, },
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "identifier" "name": "filtered_value"
} }
] ]
} }
@@ -3433,6 +3570,44 @@
] ]
} }
}, },
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "as"
},
{
"type": "PATTERN",
"value": "[ \\t\\r\\n]+"
},
{
"type": "FIELD",
"name": "variable",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
]
}
]
},
{
"type": "BLANK"
}
]
},
{ {
"type": "CHOICE", "type": "CHOICE",
"members": [ "members": [
+82 -18
View File
@@ -15,6 +15,10 @@
"type": "comment_group", "type": "comment_group",
"named": true "named": true
}, },
{
"type": "csp_nonce_attr",
"named": true
},
{ {
"type": "csrf_token", "type": "csrf_token",
"named": true "named": true
@@ -244,6 +248,42 @@
] ]
} }
}, },
{
"type": "csp_nonce_attr",
"named": true,
"fields": {
"tag": {
"multiple": false,
"required": true,
"types": [
{
"type": "csp_nonce_attr",
"named": false
}
]
},
"variable": {
"multiple": false,
"required": false,
"types": [
{
"type": "identifier",
"named": true
}
]
}
},
"children": {
"multiple": true,
"required": false,
"types": [
{
"type": "filtered_value",
"named": true
}
]
}
},
{ {
"type": "csrf_token", "type": "csrf_token",
"named": true, "named": true,
@@ -434,6 +474,16 @@
"named": false "named": false
} }
] ]
},
"variable": {
"multiple": false,
"required": false,
"types": [
{
"type": "identifier",
"named": true
}
]
} }
}, },
"children": { "children": {
@@ -443,10 +493,6 @@
{ {
"type": "filtered_value", "type": "filtered_value",
"named": true "named": true
},
{
"type": "identifier",
"named": true
} }
] ]
} }
@@ -725,16 +771,6 @@
"type": "now", "type": "now",
"named": true, "named": true,
"fields": { "fields": {
"name": {
"multiple": false,
"required": false,
"types": [
{
"type": "identifier",
"named": true
}
]
},
"tag": { "tag": {
"multiple": false, "multiple": false,
"required": true, "required": true,
@@ -744,6 +780,16 @@
"named": false "named": false
} }
] ]
},
"variable": {
"multiple": false,
"required": false,
"types": [
{
"type": "identifier",
"named": true
}
]
} }
}, },
"children": { "children": {
@@ -853,6 +899,16 @@
"named": false "named": false
} }
] ]
},
"variable": {
"multiple": false,
"required": false,
"types": [
{
"type": "identifier",
"named": true
}
]
} }
}, },
"children": { "children": {
@@ -1361,6 +1417,10 @@
"type": "content", "type": "content",
"named": true "named": true
}, },
{
"type": "csp_nonce_attr",
"named": false
},
{ {
"type": "csrf_token", "type": "csrf_token",
"named": false "named": false
@@ -1469,6 +1529,10 @@
"type": "escapejs", "type": "escapejs",
"named": false "named": false
}, },
{
"type": "escapeseq",
"named": false
},
{ {
"type": "extends", "type": "extends",
"named": false "named": false
@@ -1565,10 +1629,6 @@
"type": "length", "type": "length",
"named": false "named": false
}, },
{
"type": "length_is:",
"named": false
},
{ {
"type": "linebreaks", "type": "linebreaks",
"named": false "named": false
@@ -1601,6 +1661,10 @@
"type": "make_list", "type": "make_list",
"named": false "named": false
}, },
{
"type": "media",
"named": false
},
{ {
"type": "not", "type": "not",
"named": false "named": false
+25179 -23371
View File
File diff suppressed because it is too large. Load diff
+81
View File
@@ -0,0 +1,81 @@
==================
Csp Nonce Attr
==================
{% csp_nonce_attr %}
---
(template
(content)
(csp_nonce_attr)
(content))
==================
Csp Nonce Attr With Argument And As
==================
{% csp_nonce_attr "script" as nonce %}
---
(template
(content)
(csp_nonce_attr
(filtered_value
(value
(literal
(string))))
variable: (identifier))
(content))
==================
Csp Nonce Attr With Media Keyword
==================
{% csp_nonce_attr media="script" %}
---
(template
(content)
(csp_nonce_attr
(filtered_value
(value
(literal
(string)))))
(content))
==================
Csp Nonce Attr Takes One Argument
:error
==================
{% csp_nonce_attr "a" "b" %}
---
(ERROR)
==================
Csp Nonce Attr Rejects Unknown Keyword
:error
==================
{% csp_nonce_attr other="b" %}
---
(ERROR)
==================
Csp Nonce Attr Rejects Repeated Keyword
:error
==================
{% csp_nonce_attr media=1 media=2 %}
---
(ERROR)
+1 -1
View File
@@ -42,5 +42,5 @@ Firstof With As
(value (value
(variable_attribute (variable_attribute
(identifier)))) (identifier))))
(identifier)) variable: (identifier))
(content)) (content))
+28
View File
@@ -0,0 +1,28 @@
==================
Now Basic
==================
{% now "Y-m-d" %}
---
(template
(content)
(now
(string))
(content))
==================
Now With As
==================
{% now "Y" as today %}
---
(template
(content)
(now
(string)
variable: (identifier))
(content))
+28 -1
View File
@@ -40,10 +40,37 @@ Querystring With Identifier And Key Value
(template (template
(content) (content)
(query_string (query_string
(identifier) (filtered_value
(value
(variable_attribute
(identifier))))
(identifier) (identifier)
(filtered_value (filtered_value
(value (value
(literal (literal
(number))))) (number)))))
(content)) (content))
==================
Querystring With Expression Argument And As
==================
{% querystring request.GET page=2 as qs %}
---
(template
(content)
(query_string
(filtered_value
(value
(variable_attribute
(identifier)
(attribute))))
(identifier)
(filtered_value
(value
(literal
(number))))
variable: (identifier))
(content))