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.
@@ -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
+ }