Compare commits

...
9 Commits
Author SHA1 Message Date
danhanson 2181ff3403 imported test cases
Release / Version (push) Canceled after 0s
Release / CI (push) Canceled after 0s
Release / Prebuild (macos-latest) (push) Canceled after 0s
Release / Prebuild (ubuntu-24.04-arm) (push) Canceled after 0s
Release / Prebuild (ubuntu-latest) (push) Canceled after 0s
Release / Prebuild (windows-latest) (push) Canceled after 0s
Release / Tag and stage (push) Canceled after 0s
2026-09-18 04:18:23 -05:00
danhanson b154625f1b remove macos-13
Release / Version (push) Canceled after 0s
Release / CI (push) Canceled after 0s
Release / Prebuild (macos-latest) (push) Canceled after 0s
Release / Prebuild (ubuntu-24.04-arm) (push) Canceled after 0s
Release / Prebuild (ubuntu-latest) (push) Canceled after 0s
Release / Prebuild (windows-latest) (push) Canceled after 0s
Release / Tag and stage (push) Canceled after 0s
2026-09-17 03:52:56 -05:00
danhanson 1ac669eee9 increase version
Release / Version (push) Canceled after 0s
Release / CI (push) Canceled after 0s
Release / Prebuild (macos-13) (push) Canceled after 0s
Release / Prebuild (macos-latest) (push) Canceled after 0s
Release / Prebuild (ubuntu-24.04-arm) (push) Canceled after 0s
Release / Prebuild (ubuntu-latest) (push) Canceled after 0s
Release / Prebuild (windows-latest) (push) Canceled after 0s
Release / Tag and stage (push) Canceled after 0s
2026-09-17 01:50:27 -05:00
danhanson a3de44f4ca fix windows binding
Release / Version (push) Canceled after 0s
Release / CI (push) Canceled after 0s
Release / Prebuild (macos-13) (push) Canceled after 0s
Release / Prebuild (macos-latest) (push) Canceled after 0s
Release / Prebuild (ubuntu-24.04-arm) (push) Canceled after 0s
Release / Prebuild (ubuntu-latest) (push) Canceled after 0s
Release / Prebuild (windows-latest) (push) Canceled after 0s
Release / Tag and stage (push) Canceled after 0s
2026-09-17 01:37:30 -05:00
danhanson 96e7b2a9c5 use stage in workflow
Release / Version (push) Canceled after 0s
Release / CI (push) Canceled after 0s
Release / Prebuild (macos-13) (push) Canceled after 0s
Release / Prebuild (macos-latest) (push) Canceled after 0s
Release / Prebuild (ubuntu-24.04-arm) (push) Canceled after 0s
Release / Prebuild (ubuntu-latest) (push) Canceled after 0s
Release / Prebuild (windows-latest) (push) Canceled after 0s
Release / Tag and stage (push) Canceled after 0s
2026-09-17 01:25:49 -05:00
danhanson 99123289f7 add release workflow
Release / Version (push) Canceled after 0s
Release / CI (push) Canceled after 0s
Release / Prebuild (macos-13) (push) Canceled after 0s
Release / Prebuild (macos-latest) (push) Canceled after 0s
Release / Prebuild (ubuntu-24.04-arm) (push) Canceled after 0s
Release / Prebuild (ubuntu-latest) (push) Canceled after 0s
Release / Prebuild (windows-latest) (push) Canceled after 0s
Release / Tag and publish (push) Canceled after 0s
2026-09-16 16:05:32 -05:00
danhanson 13c774dedb fix parser version to match package.json 2026-09-16 16:05:14 -05:00
danhanson 969ba61e88 remove private tag 2026-09-16 15:33:22 -05:00
danhanson 5183f6aa52 proofread readme 2026-09-16 14:59:07 -05:00
11 changed files with 836 additions and 28 deletions

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
name: CI
# Pull requests run this directly; pushes to main reach it through release.yml,
# which calls this workflow before it tags and publishes.
on:
pull_request:
workflow_call:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
parser:
name: Corpus and generated files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
- run: npm ci
- name: Corpus tests
run: npm run parser-test
- name: src/ matches grammar.js
run: |
npm run parser-generate
if ! git diff --exit-code --stat src/; then
echo "::error::src/ is out of date: run 'npm run parser-generate' and commit the result"
exit 1
fi
node:
name: Node binding (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
# windows-latest is Windows Server 2025 with Visual Studio 2026, which the
# node-gyp bundled with the runner's npm (11.5) reports as "unknown
# version undefined" and then gives up on. node-gyp learned VS 18 in
# 12.1.0 and npm 12 bundles 13.
- if: runner.os == 'Windows'
run: npm install -g npm@^12
# npm ci builds the addon through the install script (node-gyp-build)
- run: npm ci
- run: npm run binding-test
rust:
name: Rust binding
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: cargo test
python:
name: Python binding
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python -m pip install --upgrade pip
- run: python -m pip install . "tree-sitter~=0.24"
- run: python -m unittest discover -s bindings/python/tests -v
go:
name: Go binding
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
with:
go-version: stable
- run: go test ./bindings/go/
swift:
name: Swift binding
runs-on: macos-latest
steps:
- uses: actions/checkout@v5
- run: swift build
+245
View File
@@ -0,0 +1,245 @@
name: Release
# Releasing is a version bump: push a commit to main that changes the version in
# package.json, and this tags v<version>, creates the GitHub release and stages
# the npm publish. Every other push to main only runs the tests.
#
# Staging is not publishing: "npm stage publish" hands npm the tarball and nobody
# can install it until a maintainer runs "npm stage approve <stage-id>" and
# answers a 2FA prompt. That is the point — npm is deprecating the tokens that
# let a workflow publish on its own, and staging defers the proof of presence
# instead of removing it. The job summary carries the command to run.
#
# Running it by hand instead stages whatever version package.json holds, as long
# as npm has neither published nor staged it yet, and skips the tag and the
# release if a previous attempt already made them. That is how a half-finished
# release is retried.
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
version:
name: Version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.check.outputs.version }}
changed: ${{ steps.check.outputs.changed }}
tag_exists: ${{ steps.check.outputs.tag_exists }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 2
fetch-tags: true
- id: check
shell: bash
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
name="$(node -p "require('./package.json').name")"
version="$(node -p "require('./package.json').version")"
echo "version=$version" >> "$GITHUB_OUTPUT"
if git rev-parse -q --verify "refs/tags/v$version" >/dev/null; then
tag_exists=true
else
tag_exists=false
fi
echo "tag_exists=$tag_exists" >> "$GITHUB_OUTPUT"
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
# by hand: stage what package.json says, unless npm has it already.
# A version staged but not yet approved is invisible to npm view; the
# stage job looks for that one itself.
if npm view "$name@$version" version >/dev/null 2>&1; then
echo "::warning::$name@$version is already on npm; nothing to stage"
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "staging $name@$version by hand (tag exists: $tag_exists)"
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
exit 0
fi
if git cat-file -e HEAD~1:package.json 2>/dev/null; then
previous="$(git show HEAD~1:package.json | node -p "JSON.parse(require('node:fs').readFileSync(0, 'utf8')).version")"
else
previous=""
fi
if [ "$version" = "$previous" ]; then
echo "version is unchanged at $version; not releasing"
echo "changed=false" >> "$GITHUB_OUTPUT"
elif [ "$tag_exists" = "true" ]; then
echo "::warning::tag v$version already exists; not releasing"
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "releasing $previous -> $version"
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
ci:
name: CI
needs: version
uses: ./.github/workflows/ci.yml
prebuild:
name: Prebuild (${{ matrix.os }})
needs: [version, ci]
if: needs.version.outputs.changed == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# one runner per platform and architecture npm should ship a binary for;
# the arm Linux runner is free for public repositories only
os:
[
ubuntu-latest,
ubuntu-24.04-arm,
macos-latest,
windows-latest,
]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
# the Windows runner's own npm bundles a node-gyp too old to see the
# Visual Studio 2026 on that image; see ci.yml
- if: runner.os == 'Windows'
run: npm install -g npm@^12
- run: npm ci
- run: npm run prebuildify
- uses: actions/upload-artifact@v4
with:
name: prebuilds-${{ matrix.os }}
path: prebuilds/
if-no-files-found: error
publish:
name: Tag and stage
needs: [version, prebuild]
if: needs.version.outputs.changed == 'true'
runs-on: ubuntu-latest
permissions:
contents: write # the tag and the release
id-token: write # the OIDC exchange, and provenance for npm
env:
VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
registry-url: https://registry.npmjs.org
- name: Collect the prebuilds
uses: actions/download-artifact@v4
with:
pattern: prebuilds-*
path: prebuilds/
merge-multiple: true
- run: ls -R prebuilds/
- run: npm ci
# tree-sitter build --wasm needs Emscripten; the CLI runs it through
# Docker, which the ubuntu runners have
- name: Build the Wasm parser
run: npm run parser-build
# a retry of a half-finished release finds its tag and release already made
- name: Tag
if: needs.version.outputs.tag_exists != 'true'
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "v$VERSION" -m "v$VERSION"
git push origin "v$VERSION"
- name: Release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if gh release view "v$VERSION" >/dev/null 2>&1; then
echo "release v$VERSION exists; replacing its Wasm build"
gh release upload "v$VERSION" tree-sitter-django.wasm --clobber
else
gh release create "v$VERSION" --title "v$VERSION" --generate-notes tree-sitter-django.wasm
fi
# "npm stage" arrived in npm 12, which is newer than the npm any runner
# image ships with
- name: Install npm 12
run: npm install -g npm@^12
# staged versions share npm's version index with published ones, so a
# second stage of the same version is an error rather than a no-op. A run
# that staged and then failed later should find its stage and go on.
- name: Look for an existing stage
id: staged
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -uo pipefail
name="$(node -p "require('./package.json').name")"
# a short-lived OIDC token cannot run "npm stage list" at all, so a
# failure here means "ask npm by staging", not "stop"
if ! items="$(npm stage list "$name" --json 2>/dev/null)"; then
echo "could not list staged versions; assuming there are none"
echo "staged=false" >> "$GITHUB_OUTPUT"
exit 0
fi
id="$(node -e '
const items = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
const match = items.find((item) => item.version === process.env.VERSION);
if (match) console.log(match.id);
' <<< "$items")"
if [ -n "$id" ]; then
echo "::warning::$name@$VERSION is already staged as $id; not staging it again"
echo "staged=true" >> "$GITHUB_OUTPUT"
echo "stage_id=$id" >> "$GITHUB_OUTPUT"
else
echo "staged=false" >> "$GITHUB_OUTPUT"
fi
# NPM_TOKEN is the fallback: where the package has a trusted publisher for
# this workflow, npm exchanges the OIDC token above for one of its own and
# the secret is not read at all.
- name: Stage the publish
id: stage
if: steps.staged.outputs.staged != 'true'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
npm stage publish --provenance --access public 2>&1 | tee stage.log
id="$(sed -n 's/.*staged with id \([0-9a-fA-F-]\{36\}\).*/\1/p' stage.log | tail -n1)"
echo "stage_id=$id" >> "$GITHUB_OUTPUT"
- name: Say how to approve it
env:
STAGE_ID: ${{ steps.stage.outputs.stage_id || steps.staged.outputs.stage_id }}
run: |
set -euo pipefail
name="$(node -p "require('./package.json').name")"
{
echo "### $name@$VERSION is staged, not published"
echo
echo "npm has the tarball and nobody can install it until a maintainer approves it, which asks for 2FA:"
echo
echo '```sh'
if [ -n "$STAGE_ID" ]; then
echo "npm stage view $STAGE_ID # what was staged"
echo "npm stage download $STAGE_ID # the tarball itself"
echo "npm stage approve $STAGE_ID # publish it"
echo "npm stage reject $STAGE_ID # throw it away"
else
echo "npm stage list $name"
echo "npm stage approve <stage-id>"
fi
echo '```'
echo
echo "Approving needs npm 12: \`npm install -g npm@^12\`."
} >> "$GITHUB_STEP_SUMMARY"
+1
View File
@@ -27,3 +27,4 @@ libtree-sitter-django.*
.build/
Package.resolved
*.pc
*.tgz
+68
View File
@@ -55,6 +55,74 @@ Every binding was built and run on macOS: `npm install` plus `npm run binding-te
`||` (`-Wlogical-op-parentheses`), so a comparison chain written with both needs its `&&` groups
parenthesized.
### CI and releasing
`.github/workflows/ci.yml` runs on pull requests, and the release workflow calls it: the corpus tests, a
check that `npm run parser-generate` leaves `src/` unchanged, the Node binding on Linux, macOS and Windows,
and the Rust, Python, Go and Swift bindings.
`.github/workflows/release.yml` runs on every push to `main` but only releases when `package.json`'s version
differs from the previous commit's and no `v<version>` tag exists yet. Then it runs CI, `prebuildify` on
five runners (Linux x64 and arm64, macOS x64 and arm64, Windows x64), and a last job that tags `v<version>`,
creates the GitHub release with `tree-sitter-django.wasm` attached, and runs
`npm stage publish --provenance`. **A release is a version-bump commit on `main`**; no other push starts one.
**The workflow stages the publish; it does not publish.** `npm stage publish` hands npm the tarball and
holds it there, unavailable to install, until a maintainer runs `npm stage approve <stage-id>` and answers a
2FA prompt — proof of presence deferred rather than dropped, which is npm's answer to deprecating the tokens
that let a workflow publish unattended. The job summary prints the `view`/`download`/`approve`/`reject`
commands for the stage it made. Approving needs npm 12 (`npm install -g npm@^12`), which is also why the job
installs it: no runner image ships an npm that has the command.
Running the workflow by hand (`workflow_dispatch`) is the retry: it stages whatever version `package.json`
holds as long as `npm view` does not find it on the registry already, and it skips the tag and the release
when a previous attempt left them behind, replacing only the Wasm asset. So a run that tagged and then
failed to stage is finished by running it again, and a run against an already published version stops at the
version job. A version that is _staged_ and not yet approved is invisible to `npm view` while still
occupying npm's version index, so a second stage of it is an error rather than a no-op; the stage job asks
`npm stage list` first and skips straight to the approval summary when it finds one. That listing is
best-effort — a short-lived OIDC token may not run `npm stage` subcommands at all — and a failure there just
means the stage is attempted.
Releasing needs three things to line up:
- credentials to stage with: either a trusted publisher configured for this workflow
(`npm trust github --allow-stage-publish`), which needs no secret at all because npm exchanges the
workflow's OIDC token for one of its own, or the `NPM_TOKEN` secret as the fallback. Staging works with any
token type, so that token no longer needs — and should no longer have — the 2FA bypass an automation token
carries. Both the exchange and provenance need `id-token: write`, which the publish job already declares.
- **the version in `package.json` and in `tree-sitter.json`'s `metadata`**. The release trigger reads the
first; the second is compiled into `src/parser.c` as the language's version, so bumping one alone leaves
the parser disagreeing with the manifest.
- `npm run parser-generate` after that bump, committed with it, or CI's up-to-date check fails on a two-line
diff in `src/parser.c`.
The arm64 Linux runner (`ubuntu-24.04-arm`) is free for public repositories only. `tree-sitter build --wasm`
runs Emscripten through Docker, which the ubuntu runners provide.
**Windows needs a newer npm than its runner ships.** `windows-latest` is Windows Server 2025 **with Visual
Studio 2026**, and the node-gyp bundled with that image's npm (11.5.0) reads VS 18 as
`unknown version "undefined"` and then reports "Could not find any Visual Studio installation to use", so
`npm ci` fails in the install script. node-gyp learned VS 2026 in 12.1.0 and npm 12 bundles 13, which is why
both jobs that compile the addon there — CI's Node binding and the release's `prebuild` — install `npm@^12`
first. Pinning `windows-2022` instead would work, at the price of testing against a toolchain the image no
longer has. Installs of the published package never meet this: they unpack a prebuild and never run node-gyp.
**The npm package must carry its prebuilds**, or `npm install` runs `node-gyp-build`, finds nothing and
compiles, which fails on an image without `python3`, `make` and a compiler — `node:20-slim`, for instance.
0.1.0 was published by hand before the workflow existed and shipped none, which is what that image hit. The
`prebuild` jobs are what produce them, and `files` carries `prebuilds/**`.
`npm run prebuildify` passes `--napi --strip --tag-libc`. `--napi` makes one binary per platform that every
Node ABI can load, so the file needs no ABI tag and is named after the package. `--tag-libc` adds `glibc` or
`musl`: `node-gyp-build` treats a file with no libc tag as matching anything, so an Alpine install would
otherwise load a glibc binary and fail at run time; with the tag it compiles instead. Only glibc is built,
so musl installs need a toolchain.
`package.json` exports the Wasm build as `tree-sitter-django/tree-sitter-django.wasm`. An `exports` map
hides everything it does not name, so each published path — the queries, `grammar.js`, `src/scanner.c` —
needs its own entry.
### Stale parser library
tree-sitter caches the compiled parser in `~/.cache/tree-sitter/lib/django.*`, and it has served a stale
+41 -22
View File
@@ -1,6 +1,6 @@
# tree-sitter-django
A [tree-sitter](https://tree-sitter.github.io/tree-sitter/) grammar for the Django Template Language (DTL) — the `{% %}` tags, `{{ }}` variables and `{# #}` comments used in Django templates.
A [tree-sitter](https://tree-sitter.github.io/tree-sitter/) grammar for the [Django Template Language (DTL)](https://docs.djangoproject.com/en/6.1/ref/templates/language/#the-django-template-language) — the `{% %}` tags, `{{ }}` variables and `{# #}` comments used in Django templates.
## Coverage
@@ -18,14 +18,15 @@ disagree is listed under [Divergences](#divergences-from-django).
## Using it
There is no published package yet, but the language bindings are in place. What is here today:
`tree-sitter-django` is [on npm](https://www.npmjs.com/package/tree-sitter-django), and the other language
bindings are in place. What is here today:
- **`src/parser.c` and `src/scanner.c` are generated and committed**, so any tree-sitter host can compile
them directly without running the CLI. The scanner is required: block-name matching
(`{% block a %}…{% endblock a %}`), the repeated-argument guards and the missing-tag markers live in it.
- **`npm run parser-build`** produces `tree-sitter-django.wasm` for a `web-tree-sitter` host or an editor
that loads Wasm grammars. It is a build artifact rather than a checked-in one, so it has to be built
(or shipped by a release) before anything can load it.
- **`tree-sitter-django.wasm`** is for a `web-tree-sitter` host or an editor that loads Wasm grammars. It
is not checked in: `npm run parser-build` makes it, and a release ships it both in the npm package (the
`tree-sitter-django/tree-sitter-django.wasm` export) and as an asset on the GitHub release.
- **`queries/`** holds the highlight, locals, tags, injection and error queries an editor loads, plus three files
meant for a linter. See [Queries](#queries).
- **`tree-sitter.json`** names the grammar `django`, scopes it `source.django`, and points at the query
@@ -36,8 +37,8 @@ There is no published package yet, but the language bindings are in place. What
## Bindings
| Language | Build from a checkout | Entry point |
| -------- | -------------------------------------------------------- | ------------------------------------------------------ |
| Node | `npm install` builds the addon through `node-gyp-build` | `bindings/node/index.js` (ESM), typed by `index.d.ts` |
| -------- | ------------------------------------------------------------- | ------------------------------------------------------ |
| Node | `npm install`, which uses a prebuilt binary when there is one | `bindings/node/index.js` (ESM), typed by `index.d.ts` |
| Rust | `cargo build` | `bindings/rust/lib.rs`, `Cargo.toml` |
| Python | `pip install .` | `bindings/python/tree_sitter_django`, `pyproject.toml` |
| Go | `go test ./bindings/go` | `bindings/go/binding.go`, `go.mod` |
@@ -52,6 +53,17 @@ const parser = new Parser();
parser.setLanguage(django);
```
To use the parser on a website, the wasm needs to be served then loaded.
```js
import { Language, Parser } from "web-tree-sitter";
await Parser.init();
const wasm = "my-resources-path/tree-sitter-django.wasm";
const parser = new Parser();
parser.setLanguage(await Language.load(wasm));
```
```python
import tree_sitter, tree_sitter_django
@@ -59,10 +71,6 @@ language = tree_sitter.Language(tree_sitter_django.language())
tree = tree_sitter.Parser(language).parse(b"{% if x %}hi {{ y|upper }}{% endif %}")
```
The binding files are generated, so they are not edited by hand: change `tree-sitter.json` and re-run
`tree-sitter init`. `package.json` still carries `"private": true`, so nothing is published from this
repository yet.
## Development
```sh
@@ -78,6 +86,15 @@ to `grammar.js` or `src/scanner.c` is only half made until `parser-generate` has
committed alongside it. `tree-sitter test` compiles the parser itself, so a stale `src/` shows up as tests
that pass against the wrong grammar.
Pull requests run the corpus tests and every binding through `.github/workflows/ci.yml`. A release is a
version-bump commit on `main`: raise the version in `package.json` and in `tree-sitter.json`'s `metadata`,
run `npm run parser-generate`, and push. The workflow tags `v<version>`, attaches
`tree-sitter-django.wasm` to the GitHub release, and stages the npm publish with provenance and prebuilt
binaries for Linux, macOS and Windows. Staging is not publishing: npm holds the tarball until a maintainer
runs `npm stage approve <stage-id>` with 2FA, and the workflow's job summary carries that command. Running
the workflow by hand stages the current version if npm does not have it yet, which is how a release that
failed part way through is retried.
## Queries
Five of the files in `queries/` are query files an editor loads:
@@ -148,6 +165,9 @@ Because these names are modelled, they win over the fallbacks below: a project t
under one of them with a different signature gets a parse error. They are common enough that shadowing
them is treated as the mistake.
If a project insists on shadowing one of these tags, it may use a sub-grammar that replaces the rule with
their own. See [Adding rules for your own tags and filters](###adding-rules-for-your-own-tags-and-filters).
## Custom tags and filters
A library's tags and filters are registered in Python, so the grammar cannot know their names, their
@@ -156,9 +176,8 @@ arity, or whether a tag opens a body. Two fallback rules accept them:
- **Tags.** Any tag the grammar does not recognise parses as `custom_tag`, with the shape `simple_tag` and
`inclusion_tag` give: positional filter expressions, then `name=value` keyword arguments, then an
optional `as name`. So `{% mytag a b|upper "s" k=1 as out %}` parses, with the name on the `tag_name:` field.
A keyword argument written twice is refused, as `parse_bits` refuses it.
- **Filters.** An unknown filter parses as `(filter filter_name: (identifier))` with at most one `:argument`,
which is all Django's expression regex allows. No arity check is possible.
A keyword argument written twice is refused.
- **Filters.** An unknown filter parses as `(filter filter_name: (identifier))` with at most one `:argument`. No arity check for unknown filters is possible.
Both are captured as `@function.call` rather than `@function`, so an editor can distinguish a name the
grammar knows from one it is only tolerating.
@@ -197,7 +216,7 @@ export default grammar(django, {
map_tag: ($) =>
seq(
$.simpleTagOpen,
field("tag", "map"),
field("tag_name", "map"),
$._sep,
$.filtered_value,
$._sep,
@@ -211,14 +230,14 @@ export default grammar(django, {
loop_tag: ($) =>
seq(
$.startTagOpen,
field("tag", "loop"),
field("tag_name", "loop"),
$._sep,
$.identifier,
$.startTagClose,
),
loop_block: ($) => seq($.loop_tag, optional($.template)),
endloop_tag: ($) =>
seq($.endTagOpen, field("tag", "endloop"), $.endTagClose),
seq($.endTagOpen, field("tag_name", "endloop"), $.endTagClose),
loop_group: ($) =>
seq(
$.loop_block,
@@ -229,7 +248,7 @@ export default grammar(django, {
filter: ($, previous) =>
choice(
previous,
seq(field("name", "shout"), ":", field("argument", $.value)),
seq(field("filter_name", "shout"), ":", field("argument", $.value)),
),
},
});
@@ -307,10 +326,10 @@ carry on as before.
## Divergences from Django
The grammar aims to accept what Django accepts, with one deliberate exception. Where Django takes an
input without complaint but the input is almost certainly a mistake — it parses, and then silently
renders nothing or quietly ignores half of what was written — the grammar reports an error instead.
Every case below is input Django itself accepts:
The grammar aims to accept what Django accepts, with one deliberate exception: where Django takes an
input without complaint but the input is almost certainly a mistake. It reads the mistake and silently
renders nothing or quietly ignores half of what was written. This grammar tries to report such mistakes
as errors instead. Every case below is input Django itself accepts:
- **Number-shaped names.** Django has no number token: a word is a literal if `int()` or `float()` parses
it and a variable lookup otherwise, so `0x1f`, `1a`, `1.`, `1.2.3` and `1__0` look up a variable of that
Generated
+3 -3
View File
@@ -1,7 +1,6 @@
{
"name": "tree-sitter-django",
"version": "0.1.0",
"private": true,
"version": "0.1.2",
"license": "MIT",
"type": "module",
"exports": {
@@ -10,6 +9,7 @@
"default": "./bindings/node/index.js"
},
"./grammar": "./grammar.js",
"./tree-sitter-django.wasm": "./tree-sitter-django.wasm",
"./src/scanner.c": "./src/scanner.c",
"./queries/*": "./queries/*",
"./package.json": "./package.json"
@@ -26,7 +26,7 @@
"tag": "tree-sitter tag",
"playground": "tree-sitter playground",
"install": "node-gyp-build",
"prebuildify": "prebuildify --napi --strip",
"prebuildify": "prebuildify --napi --strip --tag-libc",
"binding-test": "node --test bindings/node/*_test.js"
},
"dependencies": {
Generated
+2 -2
View File
@@ -64579,8 +64579,8 @@ TS_PUBLIC const TSLanguage *tree_sitter_django(void) {
.max_reserved_word_set_size = 22,
.metadata = {
.major_version = 0,
.minor_version = 0,
.patch_version = 1,
.minor_version = 1,
.patch_version = 2,
},
};
return &language;
+212
View File
@@ -104,6 +104,120 @@ For With Empty
(endfor_tag))
(content))
==================
For Does Not Take An Else
==================
{% for a in b %}x{% else %}y{% endfor %}
---
(template
(content)
(for_group
(for_block
(for_tag
variable: (identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(content)
(unexpected_tag)
(content)))
(endfor_tag))
(content))
==================
For With Empty Twice
==================
{% for a in b %}x{% empty %}y{% empty %}z{% endfor %}
---
(template
(content)
(for_group
(for_block
(for_tag
variable: (identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(content)))
(empty_block
(empty_tag)
(template
(content)
(unexpected_tag)
(content)))
(endfor_tag))
(content))
==================
For With An Empty Clause And No Body
==================
{% for a in b %}{% empty %}{% endfor %}
---
(template
(content)
(for_group
(for_block
(for_tag
variable: (identifier)
(filtered_value
(value
(variable_attribute
(identifier))))))
(empty_block
(empty_tag))
(endfor_tag))
(content))
==================
For With A Nested If In Its Empty Clause
==================
{% for a in b %}x{% empty %}{% if q %}y{% endif %}{% endfor %}
---
(template
(content)
(for_group
(for_block
(for_tag
variable: (identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(content)))
(empty_block
(empty_tag)
(template
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(endif_tag))))
(endfor_tag))
(content))
==================
For With A Tag In Its Body
==================
@@ -178,6 +292,104 @@ For With A Nested For
(endfor_tag))
(content))
==================
For With A Nested With
==================
{% for a in b %}{% with c=1 %}y{% endwith %}{% endfor %}
---
(template
(content)
(for_group
(for_block
(for_tag
variable: (identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(with_group
(with_block
(with_tag
variable: (identifier)
(filtered_value
(value
(literal
(number)))))
(template
(content)))
(endwith_tag))))
(endfor_tag))
(content))
==================
For With A Cycle In Its Body
==================
{% for a in b %}{% cycle 1 2 %}{% endfor %}
---
(template
(content)
(for_group
(for_block
(for_tag
variable: (identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(cycle_tag
(filtered_value
(value
(literal
(number))))
(filtered_value
(value
(literal
(number)))))))
(endfor_tag))
(content))
==================
For With Multiple Variables And A Nested If
==================
{% for a, b in c %}{% if x %}y{% endif %}{% endfor %}
---
(template
(content)
(for_group
(for_block
(for_tag
variable: (identifier)
variable: (identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(template
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(endif_tag))))
(endfor_tag))
(content))
==================
For With Tags In Both Branches
==================
+147
View File
@@ -101,6 +101,153 @@ other
(endif_tag))
(content))
==================
If With Elif
==================
{% if x %}a{% elif y %}b{% endif %}
---
(template
(content)
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(elif_block
(elif_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(endif_tag))
(content))
==================
If With Else
==================
{% if x %}a{% else %}b{% endif %}
---
(template
(content)
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(else_block
(else_tag)
(template
(content)))
(endif_tag))
(content))
==================
If With An Elif Missing Its Condition
==================
{% if x %}a{% elif %}b{% endif %}
---
(template
(content)
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)
(unexpected_tag)
(content)))
(endif_tag))
(content))
==================
If With An Elif After An Else
==================
{% if x %}a{% else %}b{% elif y %}c{% endif %}
---
(template
(content)
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)
(unexpected_tag)
(content)))
(elif_block
(elif_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(endif_tag))
(content))
==================
If With Two Elses
==================
{% if x %}a{% else %}b{% else %}c{% endif %}
---
(template
(content)
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(else_block
(else_tag)
(template
(content)
(unexpected_tag)
(content)))
(endif_tag))
(content))
==================
If With Complex Predicate
==================
+25
View File
@@ -66,6 +66,31 @@ With Multiple Variables
(endwith_tag))
(content))
==================
With Does Not Take An Else
==================
{% with x=1 %}a{% else %}b{% endwith %}
---
(template
(content)
(with_group
(with_block
(with_tag
variable: (identifier)
(filtered_value
(value
(literal
(number)))))
(template
(content)
(unexpected_tag)
(content)))
(endwith_tag))
(content))
==================
With A Binding Twice
==================
+1 -1
View File
@@ -19,7 +19,7 @@
}
],
"metadata": {
"version": "0.1.0",
"version": "0.1.2",
"license": "MIT",
"description": "Django grammar for tree-sitter",
"authors": [