tree-sitter-xml-django 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tree_sitter_xml_django-0.4.2/LICENSE +28 -0
- tree_sitter_xml_django-0.4.2/PKG-INFO +201 -0
- tree_sitter_xml_django-0.4.2/README.md +182 -0
- tree_sitter_xml_django-0.4.2/bindings/python/tree_sitter_xml_django/__init__.py +43 -0
- tree_sitter_xml_django-0.4.2/bindings/python/tree_sitter_xml_django/__init__.pyi +17 -0
- tree_sitter_xml_django-0.4.2/bindings/python/tree_sitter_xml_django/binding.c +35 -0
- tree_sitter_xml_django-0.4.2/bindings/python/tree_sitter_xml_django/py.typed +0 -0
- tree_sitter_xml_django-0.4.2/bindings/python/tree_sitter_xml_django.egg-info/PKG-INFO +201 -0
- tree_sitter_xml_django-0.4.2/bindings/python/tree_sitter_xml_django.egg-info/SOURCES.txt +15 -0
- tree_sitter_xml_django-0.4.2/bindings/python/tree_sitter_xml_django.egg-info/dependency_links.txt +1 -0
- tree_sitter_xml_django-0.4.2/bindings/python/tree_sitter_xml_django.egg-info/not-zip-safe +1 -0
- tree_sitter_xml_django-0.4.2/bindings/python/tree_sitter_xml_django.egg-info/requires.txt +3 -0
- tree_sitter_xml_django-0.4.2/bindings/python/tree_sitter_xml_django.egg-info/top_level.txt +2 -0
- tree_sitter_xml_django-0.4.2/pyproject.toml +36 -0
- tree_sitter_xml_django-0.4.2/setup.cfg +4 -0
- tree_sitter_xml_django-0.4.2/setup.py +77 -0
- tree_sitter_xml_django-0.4.2/src/parser.c +56798 -0
- tree_sitter_xml_django-0.4.2/src/tree_sitter/alloc.h +54 -0
- tree_sitter_xml_django-0.4.2/src/tree_sitter/array.h +336 -0
- tree_sitter_xml_django-0.4.2/src/tree_sitter/parser.h +286 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Kraken Technologies Ltd
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
16
|
+
may be used to endorse or promote products derived from this software
|
|
17
|
+
without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tree-sitter-xml-django
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: Django-XML template language grammar for tree-sitter
|
|
5
|
+
Author-email: Laurent Putz <laurent.putz@kraken.tech>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/kraken-tech/tree-sitter-xml-django
|
|
8
|
+
Keywords: incremental,parsing,tree-sitter,xml-django
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
11
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Provides-Extra: core
|
|
17
|
+
Requires-Dist: tree-sitter~=0.26; extra == "core"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# tree-sitter-xml-django
|
|
21
|
+
|
|
22
|
+
Django-XML template language grammar for [tree-sitter](https://tree-sitter.github.io/tree-sitter/).
|
|
23
|
+
|
|
24
|
+
Supports `.xml` and `.rml` files using Django template syntax embedded in XML.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Node.js (for grammar generation)
|
|
29
|
+
- Python 3.x (for Python bindings)
|
|
30
|
+
- [tree-sitter CLI](https://github.com/tree-sitter/tree-sitter)
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Python
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import tree_sitter_xml_django as ts_xml_django
|
|
38
|
+
from tree_sitter import Language, Parser
|
|
39
|
+
|
|
40
|
+
language = Language(ts_xml_django.language())
|
|
41
|
+
parser = Parser(language)
|
|
42
|
+
|
|
43
|
+
tree = parser.parse(b"<root>{% if condition %}<child/>{% endif %}</root>")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Contributing
|
|
47
|
+
|
|
48
|
+
### First-time setup
|
|
49
|
+
|
|
50
|
+
Requires Node.js, [uv](https://docs.astral.sh/uv/), and Python 3.10+.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Install Node.js and Python dependencies
|
|
54
|
+
make dev
|
|
55
|
+
|
|
56
|
+
# Install pre-commit hooks
|
|
57
|
+
uvx pre-commit install --install-hooks
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`make dev` runs `npm install` (tree-sitter CLI) and `uv sync` (Python dev dependencies) in one step.
|
|
61
|
+
|
|
62
|
+
#### Common tasks
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `make generate` | Re-generate `src/parser.c` from `grammar.js` |
|
|
67
|
+
| `make test` | Run tree-sitter corpus tests and Python tests |
|
|
68
|
+
| `make package` | Build the Python distribution package |
|
|
69
|
+
|
|
70
|
+
### Releasing a new version
|
|
71
|
+
|
|
72
|
+
To publish a new version of the package to PyPI:
|
|
73
|
+
|
|
74
|
+
1. Bump the `version` field in `pyproject.toml` (follows [semver](https://semver.org/))
|
|
75
|
+
2. Merge the change to `main`
|
|
76
|
+
|
|
77
|
+
CI will detect that the version has no corresponding git tag, publish the package to PyPI, and then push a `vX.Y.Z` tag to GitHub automatically. No manual tagging or release steps are needed.
|
|
78
|
+
|
|
79
|
+
If a merge to `main` does not include a version bump, the publish step is skipped silently.
|
|
80
|
+
|
|
81
|
+
### The external scanner at src/scanner.c
|
|
82
|
+
|
|
83
|
+
Using the parser requires a binding in your target language that calls the generated file `src/parser.c`.
|
|
84
|
+
For more complex "lookahead" rules, `src/scanner.c` provides a collection of methods that expose tokens
|
|
85
|
+
referenced in `parser.c`. It is worth noting that `parser.c` cannot be edited directly without breaking
|
|
86
|
+
the synchronisation between `grammar.js` and `parser.c`, but `scanner.c` _can_ be edited so long as the
|
|
87
|
+
method names don't change. The CI will only detect differences between the generated `parser.c` file and
|
|
88
|
+
its grammar rules in `grammar.js`, and from the perspective of the parser, a change to the method logic
|
|
89
|
+
doesn't change what it sees - only the method name that it calls.
|
|
90
|
+
|
|
91
|
+
### Paired vs unpaired Django statements
|
|
92
|
+
|
|
93
|
+
Django template tags fall into two categories, and the grammar treats them very differently.
|
|
94
|
+
|
|
95
|
+
**Unpaired statements** (e.g. `{% load %}`, `{% url %}`, `{% csrf_token %}`) stand alone — no
|
|
96
|
+
body, no closing tag. Because they require no context about what came before or after, the
|
|
97
|
+
grammar handles them with a single catch-all rule (`dj_unpaired_statement`) that matches any
|
|
98
|
+
`{% identifier %}` not claimed by a more specific rule. The tag name is deliberately not
|
|
99
|
+
enumerated; knowing it adds nothing to the parse.
|
|
100
|
+
|
|
101
|
+
**Paired statements** (e.g. `{% block %}...{% endblock %}`, `{% if %}...{% endif %}`) enclose a
|
|
102
|
+
body and require a matching closing tag. The grammar must know the tag name at parse time in
|
|
103
|
+
order to look for the correct `{% endXXX %}` closing token — this is why paired tags are handled
|
|
104
|
+
by named rules or an explicit whitelist rather than a catch-all.
|
|
105
|
+
|
|
106
|
+
This distinction explains what happens with an **unknown paired tag**: because the parser has no
|
|
107
|
+
rule telling it to look for a closing `{% endcustom %}`, the opening tag falls through to the
|
|
108
|
+
catch-all and is recorded as a `dj_unpaired_statement`. The closing `{% endcustom %}` is then
|
|
109
|
+
parsed as a second, separate `dj_unpaired_statement`. See
|
|
110
|
+
[Unknown paired Django tags](#unknown-paired-django-tags) under Limitations for the planned fix.
|
|
111
|
+
|
|
112
|
+
N.B: It's important to note that the `{% [tag] %} ... {% end[tag] %}` convention is just that,
|
|
113
|
+
a convention not a rule of Django syntax. Any proposed fix would need to be lenient on paired
|
|
114
|
+
statements that do not follow this convention.
|
|
115
|
+
|
|
116
|
+
### Not (yet) implemented
|
|
117
|
+
|
|
118
|
+
Query files (typically found at `/queries/*.scm`) are not implemented in this grammar.
|
|
119
|
+
The only use case for this library at the time of writing is programmatic
|
|
120
|
+
tree manipulation, not editor integration. Implement these if the grammar is ever adopted
|
|
121
|
+
for editor use (syntax highlighting, language injection, symbol navigation).
|
|
122
|
+
|
|
123
|
+
## Limitations
|
|
124
|
+
|
|
125
|
+
### XML tags spanning Django conditional branches
|
|
126
|
+
|
|
127
|
+
Django templates sometimes use conditional blocks to select between variants of
|
|
128
|
+
an XML structure, relying on the template engine to produce valid XML at render
|
|
129
|
+
time even though the static source is not well-formed XML. Any XML tag that
|
|
130
|
+
opens or closes across a Django branch boundary — in either direction — will
|
|
131
|
+
produce incorrect parse trees:
|
|
132
|
+
|
|
133
|
+
```django
|
|
134
|
+
{# open before block, close inside branch — ERROR nodes #}
|
|
135
|
+
<keepTogether>
|
|
136
|
+
{% if necf_state %}
|
|
137
|
+
</keepTogether>
|
|
138
|
+
{% elif vic_state %}
|
|
139
|
+
</keepTogether>
|
|
140
|
+
{% endif %}
|
|
141
|
+
|
|
142
|
+
{# open inside branches, close after block — wrong tree, no ERROR nodes #}
|
|
143
|
+
{% if wide %}
|
|
144
|
+
<keepTogether>
|
|
145
|
+
{% else %}
|
|
146
|
+
<keepTogether>
|
|
147
|
+
{% endif %}
|
|
148
|
+
</keepTogether>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The second case appears to parse without `ERROR` nodes when the pattern sits at
|
|
152
|
+
the document root, but in practice these templates have a wrapping parent
|
|
153
|
+
element. Inside XML element content (`_node`), bare `end_tag` is not allowed,
|
|
154
|
+
so the closing tag is silently consumed as the close of the parent element
|
|
155
|
+
instead, producing a structurally wrong tree.
|
|
156
|
+
|
|
157
|
+
Adding bare `start_tag`/`end_tag` alternatives to regular XML content causes
|
|
158
|
+
GLR ambiguity: the parser begins treating every ordinary opening tag as a bare
|
|
159
|
+
node and orphans its close tag, breaking large amounts of valid XML.
|
|
160
|
+
|
|
161
|
+
The one exception is when **both** the mismatched open and close tags land
|
|
162
|
+
inside Django statement bodies (e.g. inside a `{% for %}` or `{% block %}`
|
|
163
|
+
body), because that context already allows bare tags:
|
|
164
|
+
|
|
165
|
+
```django
|
|
166
|
+
{% for item in items %}
|
|
167
|
+
{% if wide %}<keepTogether>{% else %}<keepTogether>{% endif %}
|
|
168
|
+
<para>{{ item }}</para>
|
|
169
|
+
</keepTogether> {# inside the for body — parses without error #}
|
|
170
|
+
{% endfor %}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**If a fix becomes necessary:** the principled path is an **external scanner**
|
|
174
|
+
(C code in `src/scanner.c`) that maintains a stack of open XML element names.
|
|
175
|
+
When it encounters a `</tag>` whose name is not on the stack it emits a
|
|
176
|
+
distinct `_orphan_end_tag` token rather than a normal `end_tag`, allowing the
|
|
177
|
+
grammar to admit it in `_node` context without competing with legitimate close
|
|
178
|
+
tags. This is non-trivial — the scanner needs to coordinate with Django block
|
|
179
|
+
boundaries — but it avoids the GLR ambiguity that makes the pure-grammar
|
|
180
|
+
approach unworkable. The same scanner update would also be a natural place to
|
|
181
|
+
address [unknown paired Django tags](#unknown-paired-django-tags).
|
|
182
|
+
|
|
183
|
+
### Unknown paired Django tags
|
|
184
|
+
|
|
185
|
+
The grammar handles known built-in paired tags by name (`autoescape`, `block`,
|
|
186
|
+
`filter`, `for`, `if`, `comment`, etc.). Any tag not in that list — including
|
|
187
|
+
third-party or project-specific paired tags — is parsed as two separate
|
|
188
|
+
`dj_unpaired_statement` nodes rather than a single `dj_paired_statement` with
|
|
189
|
+
a body.
|
|
190
|
+
|
|
191
|
+
**If a fix becomes necessary:** the same external scanner approach described
|
|
192
|
+
above for XML tags applies here. The scanner would maintain a second stack of
|
|
193
|
+
open Django tag names; when it encounters `{% endXXX %}`, it checks whether
|
|
194
|
+
`XXX` is on the stack and, if so, emits a specialised close token that the
|
|
195
|
+
grammar uses to close the corresponding `dj_paired_statement`. This removes
|
|
196
|
+
the need for a tag whitelist and handles arbitrary custom and third-party tags.
|
|
197
|
+
Both stacks could be implemented together in a single scanner update.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
[BSD 3-Clause](LICENSE)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# tree-sitter-xml-django
|
|
2
|
+
|
|
3
|
+
Django-XML template language grammar for [tree-sitter](https://tree-sitter.github.io/tree-sitter/).
|
|
4
|
+
|
|
5
|
+
Supports `.xml` and `.rml` files using Django template syntax embedded in XML.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js (for grammar generation)
|
|
10
|
+
- Python 3.x (for Python bindings)
|
|
11
|
+
- [tree-sitter CLI](https://github.com/tree-sitter/tree-sitter)
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Python
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import tree_sitter_xml_django as ts_xml_django
|
|
19
|
+
from tree_sitter import Language, Parser
|
|
20
|
+
|
|
21
|
+
language = Language(ts_xml_django.language())
|
|
22
|
+
parser = Parser(language)
|
|
23
|
+
|
|
24
|
+
tree = parser.parse(b"<root>{% if condition %}<child/>{% endif %}</root>")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Contributing
|
|
28
|
+
|
|
29
|
+
### First-time setup
|
|
30
|
+
|
|
31
|
+
Requires Node.js, [uv](https://docs.astral.sh/uv/), and Python 3.10+.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Install Node.js and Python dependencies
|
|
35
|
+
make dev
|
|
36
|
+
|
|
37
|
+
# Install pre-commit hooks
|
|
38
|
+
uvx pre-commit install --install-hooks
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`make dev` runs `npm install` (tree-sitter CLI) and `uv sync` (Python dev dependencies) in one step.
|
|
42
|
+
|
|
43
|
+
#### Common tasks
|
|
44
|
+
|
|
45
|
+
| Command | Description |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `make generate` | Re-generate `src/parser.c` from `grammar.js` |
|
|
48
|
+
| `make test` | Run tree-sitter corpus tests and Python tests |
|
|
49
|
+
| `make package` | Build the Python distribution package |
|
|
50
|
+
|
|
51
|
+
### Releasing a new version
|
|
52
|
+
|
|
53
|
+
To publish a new version of the package to PyPI:
|
|
54
|
+
|
|
55
|
+
1. Bump the `version` field in `pyproject.toml` (follows [semver](https://semver.org/))
|
|
56
|
+
2. Merge the change to `main`
|
|
57
|
+
|
|
58
|
+
CI will detect that the version has no corresponding git tag, publish the package to PyPI, and then push a `vX.Y.Z` tag to GitHub automatically. No manual tagging or release steps are needed.
|
|
59
|
+
|
|
60
|
+
If a merge to `main` does not include a version bump, the publish step is skipped silently.
|
|
61
|
+
|
|
62
|
+
### The external scanner at src/scanner.c
|
|
63
|
+
|
|
64
|
+
Using the parser requires a binding in your target language that calls the generated file `src/parser.c`.
|
|
65
|
+
For more complex "lookahead" rules, `src/scanner.c` provides a collection of methods that expose tokens
|
|
66
|
+
referenced in `parser.c`. It is worth noting that `parser.c` cannot be edited directly without breaking
|
|
67
|
+
the synchronisation between `grammar.js` and `parser.c`, but `scanner.c` _can_ be edited so long as the
|
|
68
|
+
method names don't change. The CI will only detect differences between the generated `parser.c` file and
|
|
69
|
+
its grammar rules in `grammar.js`, and from the perspective of the parser, a change to the method logic
|
|
70
|
+
doesn't change what it sees - only the method name that it calls.
|
|
71
|
+
|
|
72
|
+
### Paired vs unpaired Django statements
|
|
73
|
+
|
|
74
|
+
Django template tags fall into two categories, and the grammar treats them very differently.
|
|
75
|
+
|
|
76
|
+
**Unpaired statements** (e.g. `{% load %}`, `{% url %}`, `{% csrf_token %}`) stand alone — no
|
|
77
|
+
body, no closing tag. Because they require no context about what came before or after, the
|
|
78
|
+
grammar handles them with a single catch-all rule (`dj_unpaired_statement`) that matches any
|
|
79
|
+
`{% identifier %}` not claimed by a more specific rule. The tag name is deliberately not
|
|
80
|
+
enumerated; knowing it adds nothing to the parse.
|
|
81
|
+
|
|
82
|
+
**Paired statements** (e.g. `{% block %}...{% endblock %}`, `{% if %}...{% endif %}`) enclose a
|
|
83
|
+
body and require a matching closing tag. The grammar must know the tag name at parse time in
|
|
84
|
+
order to look for the correct `{% endXXX %}` closing token — this is why paired tags are handled
|
|
85
|
+
by named rules or an explicit whitelist rather than a catch-all.
|
|
86
|
+
|
|
87
|
+
This distinction explains what happens with an **unknown paired tag**: because the parser has no
|
|
88
|
+
rule telling it to look for a closing `{% endcustom %}`, the opening tag falls through to the
|
|
89
|
+
catch-all and is recorded as a `dj_unpaired_statement`. The closing `{% endcustom %}` is then
|
|
90
|
+
parsed as a second, separate `dj_unpaired_statement`. See
|
|
91
|
+
[Unknown paired Django tags](#unknown-paired-django-tags) under Limitations for the planned fix.
|
|
92
|
+
|
|
93
|
+
N.B: It's important to note that the `{% [tag] %} ... {% end[tag] %}` convention is just that,
|
|
94
|
+
a convention not a rule of Django syntax. Any proposed fix would need to be lenient on paired
|
|
95
|
+
statements that do not follow this convention.
|
|
96
|
+
|
|
97
|
+
### Not (yet) implemented
|
|
98
|
+
|
|
99
|
+
Query files (typically found at `/queries/*.scm`) are not implemented in this grammar.
|
|
100
|
+
The only use case for this library at the time of writing is programmatic
|
|
101
|
+
tree manipulation, not editor integration. Implement these if the grammar is ever adopted
|
|
102
|
+
for editor use (syntax highlighting, language injection, symbol navigation).
|
|
103
|
+
|
|
104
|
+
## Limitations
|
|
105
|
+
|
|
106
|
+
### XML tags spanning Django conditional branches
|
|
107
|
+
|
|
108
|
+
Django templates sometimes use conditional blocks to select between variants of
|
|
109
|
+
an XML structure, relying on the template engine to produce valid XML at render
|
|
110
|
+
time even though the static source is not well-formed XML. Any XML tag that
|
|
111
|
+
opens or closes across a Django branch boundary — in either direction — will
|
|
112
|
+
produce incorrect parse trees:
|
|
113
|
+
|
|
114
|
+
```django
|
|
115
|
+
{# open before block, close inside branch — ERROR nodes #}
|
|
116
|
+
<keepTogether>
|
|
117
|
+
{% if necf_state %}
|
|
118
|
+
</keepTogether>
|
|
119
|
+
{% elif vic_state %}
|
|
120
|
+
</keepTogether>
|
|
121
|
+
{% endif %}
|
|
122
|
+
|
|
123
|
+
{# open inside branches, close after block — wrong tree, no ERROR nodes #}
|
|
124
|
+
{% if wide %}
|
|
125
|
+
<keepTogether>
|
|
126
|
+
{% else %}
|
|
127
|
+
<keepTogether>
|
|
128
|
+
{% endif %}
|
|
129
|
+
</keepTogether>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The second case appears to parse without `ERROR` nodes when the pattern sits at
|
|
133
|
+
the document root, but in practice these templates have a wrapping parent
|
|
134
|
+
element. Inside XML element content (`_node`), bare `end_tag` is not allowed,
|
|
135
|
+
so the closing tag is silently consumed as the close of the parent element
|
|
136
|
+
instead, producing a structurally wrong tree.
|
|
137
|
+
|
|
138
|
+
Adding bare `start_tag`/`end_tag` alternatives to regular XML content causes
|
|
139
|
+
GLR ambiguity: the parser begins treating every ordinary opening tag as a bare
|
|
140
|
+
node and orphans its close tag, breaking large amounts of valid XML.
|
|
141
|
+
|
|
142
|
+
The one exception is when **both** the mismatched open and close tags land
|
|
143
|
+
inside Django statement bodies (e.g. inside a `{% for %}` or `{% block %}`
|
|
144
|
+
body), because that context already allows bare tags:
|
|
145
|
+
|
|
146
|
+
```django
|
|
147
|
+
{% for item in items %}
|
|
148
|
+
{% if wide %}<keepTogether>{% else %}<keepTogether>{% endif %}
|
|
149
|
+
<para>{{ item }}</para>
|
|
150
|
+
</keepTogether> {# inside the for body — parses without error #}
|
|
151
|
+
{% endfor %}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**If a fix becomes necessary:** the principled path is an **external scanner**
|
|
155
|
+
(C code in `src/scanner.c`) that maintains a stack of open XML element names.
|
|
156
|
+
When it encounters a `</tag>` whose name is not on the stack it emits a
|
|
157
|
+
distinct `_orphan_end_tag` token rather than a normal `end_tag`, allowing the
|
|
158
|
+
grammar to admit it in `_node` context without competing with legitimate close
|
|
159
|
+
tags. This is non-trivial — the scanner needs to coordinate with Django block
|
|
160
|
+
boundaries — but it avoids the GLR ambiguity that makes the pure-grammar
|
|
161
|
+
approach unworkable. The same scanner update would also be a natural place to
|
|
162
|
+
address [unknown paired Django tags](#unknown-paired-django-tags).
|
|
163
|
+
|
|
164
|
+
### Unknown paired Django tags
|
|
165
|
+
|
|
166
|
+
The grammar handles known built-in paired tags by name (`autoescape`, `block`,
|
|
167
|
+
`filter`, `for`, `if`, `comment`, etc.). Any tag not in that list — including
|
|
168
|
+
third-party or project-specific paired tags — is parsed as two separate
|
|
169
|
+
`dj_unpaired_statement` nodes rather than a single `dj_paired_statement` with
|
|
170
|
+
a body.
|
|
171
|
+
|
|
172
|
+
**If a fix becomes necessary:** the same external scanner approach described
|
|
173
|
+
above for XML tags applies here. The scanner would maintain a second stack of
|
|
174
|
+
open Django tag names; when it encounters `{% endXXX %}`, it checks whether
|
|
175
|
+
`XXX` is on the stack and, if so, emits a specialised close token that the
|
|
176
|
+
grammar uses to close the corresponding `dj_paired_statement`. This removes
|
|
177
|
+
the need for a tag whitelist and handles arbitrary custom and third-party tags.
|
|
178
|
+
Both stacks could be implemented together in a single scanner update.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
[BSD 3-Clause](LICENSE)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Django-XML template language grammar for tree-sitter"""
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files as _files
|
|
4
|
+
|
|
5
|
+
from ._binding import language
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_query(name, file):
|
|
9
|
+
try:
|
|
10
|
+
query = _files(f"{__package__}") / file
|
|
11
|
+
globals()[name] = query.read_text()
|
|
12
|
+
except FileNotFoundError:
|
|
13
|
+
globals()[name] = None
|
|
14
|
+
return globals()[name]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def __getattr__(name):
|
|
18
|
+
if name == "HIGHLIGHTS_QUERY":
|
|
19
|
+
return _get_query("HIGHLIGHTS_QUERY", "queries/highlights.scm")
|
|
20
|
+
if name == "INJECTIONS_QUERY":
|
|
21
|
+
return _get_query("INJECTIONS_QUERY", "queries/injections.scm")
|
|
22
|
+
if name == "LOCALS_QUERY":
|
|
23
|
+
return _get_query("LOCALS_QUERY", "queries/locals.scm")
|
|
24
|
+
if name == "TAGS_QUERY":
|
|
25
|
+
return _get_query("TAGS_QUERY", "queries/tags.scm")
|
|
26
|
+
|
|
27
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"language",
|
|
32
|
+
"HIGHLIGHTS_QUERY",
|
|
33
|
+
"INJECTIONS_QUERY",
|
|
34
|
+
"LOCALS_QUERY",
|
|
35
|
+
"TAGS_QUERY",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def __dir__():
|
|
40
|
+
return sorted(__all__ + [
|
|
41
|
+
"__all__", "__builtins__", "__cached__", "__doc__", "__file__",
|
|
42
|
+
"__loader__", "__name__", "__package__", "__path__", "__spec__",
|
|
43
|
+
])
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
from typing_extensions import CapsuleType
|
|
3
|
+
|
|
4
|
+
HIGHLIGHTS_QUERY: Final[str] | None
|
|
5
|
+
"""The syntax highlighting query for this grammar."""
|
|
6
|
+
|
|
7
|
+
INJECTIONS_QUERY: Final[str] | None
|
|
8
|
+
"""The language injection query for this grammar."""
|
|
9
|
+
|
|
10
|
+
LOCALS_QUERY: Final[str] | None
|
|
11
|
+
"""The local variable query for this grammar."""
|
|
12
|
+
|
|
13
|
+
TAGS_QUERY: Final[str] | None
|
|
14
|
+
"""The symbol tagging query for this grammar."""
|
|
15
|
+
|
|
16
|
+
def language() -> CapsuleType:
|
|
17
|
+
"""The tree-sitter language function for this grammar."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#include <Python.h>
|
|
2
|
+
|
|
3
|
+
typedef struct TSLanguage TSLanguage;
|
|
4
|
+
|
|
5
|
+
TSLanguage *tree_sitter_xml_django(void);
|
|
6
|
+
|
|
7
|
+
static PyObject* _binding_language(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) {
|
|
8
|
+
return PyCapsule_New(tree_sitter_xml_django(), "tree_sitter.Language", NULL);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static struct PyModuleDef_Slot slots[] = {
|
|
12
|
+
#ifdef Py_GIL_DISABLED
|
|
13
|
+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
|
|
14
|
+
#endif
|
|
15
|
+
{0, NULL}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
static PyMethodDef methods[] = {
|
|
19
|
+
{"language", _binding_language, METH_NOARGS,
|
|
20
|
+
"Get the tree-sitter language for this grammar."},
|
|
21
|
+
{NULL, NULL, 0, NULL}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static struct PyModuleDef module = {
|
|
25
|
+
.m_base = PyModuleDef_HEAD_INIT,
|
|
26
|
+
.m_name = "_binding",
|
|
27
|
+
.m_doc = NULL,
|
|
28
|
+
.m_size = 0,
|
|
29
|
+
.m_methods = methods,
|
|
30
|
+
.m_slots = slots,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
PyMODINIT_FUNC PyInit__binding(void) {
|
|
34
|
+
return PyModuleDef_Init(&module);
|
|
35
|
+
}
|
|
File without changes
|