Compare commits

...
17 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
danhanson 53e1ee2036 add bindings and fix documentation 2026-09-16 14:26:03 -05:00
danhanson 0e8ef8e7a5 fix scanner warning 2026-09-16 14:25:46 -05:00
danhanson 4a79d32f6a add _missing_variable_close 2026-09-14 22:08:39 -05:00
danhanson 7979890227 contain template variable errors 2026-09-14 21:50:16 -05:00
danhanson ccb80972b6 handle content edge cases 2026-09-14 21:40:26 -05:00
danhanson fa0d880e68 fixed content regex 2026-09-14 21:29:27 -05:00
danhanson 831acd3516 add unexpected argument 2026-09-14 18:32:37 -05:00
danhanson f17fdd2d74 add unexpected argument error 2026-09-14 17:51:45 -05:00
58 changed files with 56152 additions and 32964 deletions

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
root = true
[*]
charset = utf-8
[*.{json,toml,yml,gyp,xml}]
indent_style = space
indent_size = 2
[*.{js,ts}]
indent_style = space
indent_size = 2
[*.scm]
indent_style = space
indent_size = 2
[*.{c,cc,h}]
indent_style = space
indent_size = 4
[*.rs]
indent_style = space
indent_size = 4
[*.{py,pyi}]
indent_style = space
indent_size = 4
[*.swift]
indent_style = space
indent_size = 4
[*.java]
indent_style = space
indent_size = 4
[*.go]
indent_style = tab
indent_size = 8
[Makefile]
indent_style = tab
indent_size = 8
[parser.c]
indent_size = 2
[{alloc,array,parser}.h]
indent_size = 2
+46
View File
@@ -0,0 +1,46 @@
* text=auto eol=lf
# Generated source files
src/*.json linguist-generated
src/parser.c linguist-generated
src/tree_sitter/* linguist-generated
# C bindings
bindings/c/** linguist-generated
CMakeLists.txt linguist-generated
Makefile linguist-generated
# Rust bindings
bindings/rust/* linguist-generated
Cargo.toml linguist-generated
Cargo.lock linguist-generated
# Node.js bindings
bindings/node/* linguist-generated
binding.gyp linguist-generated
package.json linguist-generated
package-lock.json linguist-generated
# Python bindings
bindings/python/** linguist-generated
setup.py linguist-generated
pyproject.toml linguist-generated
# Go bindings
bindings/go/* linguist-generated
go.mod linguist-generated
go.sum linguist-generated
# Swift bindings
bindings/swift/** linguist-generated
Package.swift linguist-generated
Package.resolved linguist-generated
# Zig bindings
bindings/zig/* linguist-generated
build.zig linguist-generated
build.zig.zon linguist-generated
# Java bindings
pom.xml linguist-generated
bindings/java/** linguist-generated
+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"
+28
View File
@@ -1,2 +1,30 @@
node_modules
tree-sitter-django.wasm
# Node binding
build/
prebuilds/
*.node
# Rust binding
target/
Cargo.lock
# Python binding
dist/
*.egg-info/
__pycache__/
*.so
*.pyd
# C binding
*.o
*.a
*.dylib
libtree-sitter-django.*
# Swift binding
.build/
Package.resolved
*.pc
*.tgz
+154 -8
View File
@@ -11,6 +11,10 @@ npm run parser-generate # regenerate src/parser.c, grammar.json, node-types.js
npm run parser-test # run the corpus tests
npm run parser-build # build tree-sitter-django.wasm
npm run playground # explore the grammar interactively
npm install # also builds the Node binding, through node-gyp-build
npm run binding-test # the Node binding's own test
cargo test # the Rust binding
make # the C library, and make clean to remove it
npx tree-sitter test -i "Cycle With As" # one test, by name regex
npx tree-sitter test --file-name cycle.txt # one corpus file
@@ -30,6 +34,95 @@ back, producing churn in both directions.
`npm run lint` does not work: `eslint.config.mjs` was copied from a Next.js project and enables
`@typescript-eslint` and `react-hooks` rules whose plugins aren't installed. Formatting is prettier's job.
### Bindings
`bindings/` and the manifests beside it — `binding.gyp`, `Cargo.toml`, `pyproject.toml`, `setup.py`,
`go.mod`, `Package.swift`, `Makefile`, `CMakeLists.txt` — are generated by `tree-sitter init` from the
`bindings` and `metadata` sections of `tree-sitter.json`, and `.gitattributes` marks them
`linguist-generated`. Do not edit them by hand: change `tree-sitter.json` and regenerate.
`tree-sitter init --update` only rewrites what it considers outdated, and a changed `version` or
`description` does not count, so the manifests keep the old values until they are deleted and `init` is run
again. It leaves `README.md`, `package.json` and `.gitignore` alone, which is why the Node binding's
`package.json` entries — `main`/`types`, the `.` export, `files`, `node-addon-api`, `node-gyp-build`, the
`install` script and the optional `tree-sitter` peer — are maintained here rather than generated.
Every binding was built and run on macOS: `npm install` plus `npm run binding-test`, `cargo test`,
`go test ./bindings/go/`, `pip install .` with a parse, `swift build`, and `make`. The Go binding needs a
`go.sum`, which `go mod tidy` writes and which is committed with the rest.
`src/scanner.c` compiles without warnings, and should stay that way: cc and clang warn about `&&` within
`||` (`-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
@@ -197,13 +290,48 @@ group can hold it — `{% endif %}` with nothing open, `{% else %}` in a `{% for
and error recovery placed it badly: every tag starts with `{%` and the read token, which the parser has
already accepted when the name turns out to be wrong, so recovery rewinds to just after them and folds the
stray tag into whichever tag comes next, which then holds the `ERROR`. `unexpected_tag` takes any of
`NAMES_INSIDE_A_TAG_GROUP` with loose arguments, so there is nothing to recover; `prec.dynamic(-1)` keeps it
`NAMES_INSIDE_A_TAG_GROUP` with loose arguments, so there is nothing to recover; `prec.dynamic(-2)`, or `-4`, keeps it
from displacing a tag the group does take, and `queries/errors.scm` reports it as
`@error.unexpected_tag`. It added 31 states and no `conflicts` entry.
**Words a tag does not take are an `unexpected_argument` inside it.** Every close utility accepts one
before its scanner token, so `{% else x %}` stays the `else` of its group, `{% endif junk %}` still closes
its `if` (it used to leave the whole file an `ERROR`), and `{% static "a.css" "b.css" %}` is a `static_tag`.
The node is a run of hidden `_unexpected_word`s, and three details make it work:
- **Lexical precedence -1.** A loose word is `token(prec(-1, /(?:[^\s%}]|\}[^\s%}])+/))`, which never holds `}}`:
lexer states are shared between parse states, so the token is tried inside `{{ }}` too, and after
`{{ x| ` it once swallowed the closing braces and left recovery nothing to close the variable with. Tree-sitter weighs lexical
precedence before match length, so a token an argument really takes wins wherever one is valid
(`b|upper` stays a filtered value) and a loose word only matches where nothing else can.
- **Identifiers are words too.** Wherever a keyword is valid — after `{% for a in b`, where `reversed` may
follow, or after `as`, `only`, `noop`, `silent` — tree-sitter lexes every word as an `identifier` to
check for the keyword, and the loose-word token never gets a turn. So a word may also be an
`identifier`, aliased to an anonymous `unexpected_word` so `queries/locals.scm` does not count it as a
reference, with whatever touches it (`a=2`, `x.y|upper`, `"z"|lower:"q"`) as an immediate tail.
- **-1 of dynamic precedence per word**, not per node, so a reading that flags more words than it must
always loses: `{% cycle a b as x silent zqx %}` flags `zqx`, not everything after `a`. `unexpected_tag`
costs -2 or -4 so that a misused tag its group does take beats reading it as a stray.
A tag with a known keyword signature (`simpleTag` given names, so far only `csp_nonce_attr`) needs one more
piece: the scanner's `_kwarg_name` accepts any name followed by `=` and commits the parser to a keyword
argument before the name is checked, so `{% csp_nonce_attr other="b" %}` would be an `ERROR`. `simpleTag`
offers `_unexpected_keyword` — an identifier and its tail, aliased to `unexpected_argument` — beside the
names it knows.
A separator after any argument can now also start stray words, so this needs `conflicts` entries for
`unexpected_argument`, `predicate`, `_translate_option`, and `_unexpected_word` with `variable_attribute`,
`resetcycle_tag` and `library`. It took the table from 3,363 states to 4,913 and the Wasm build from 251KB to
323KB. Two consequences: a repeat the scanner refuses — `{% with a=1 a=2 %}`, `{% blocktrans trimmed trimmed %}`
— is reported as an unexpected argument rather than an `ERROR`; and a stray **reserved** word is still an
`ERROR`, since `not`, `if`, `in`, `is`, `as`, `for` and `from` can never be an identifier:
`{% trans "hi" as x as y %}` errors on the second `as`. A tag missing a required argument, such as
`{% with a = 1 %}`, is an `ERROR` too; `unexpected_argument` only takes words after a tag that is otherwise
complete.
Where several tags share a name the group holds only once — `{% if %}{% else %}{% else %}{% else %}` — reading
any of them as the stray parses, so the ties are broken by dynamic precedence: `unexpected_tag` is -1 when
`_group_held` confirms the innermost group already holds the name and -2 otherwise, so each tag flagged
any of them as the stray parses, so the ties are broken by dynamic precedence: `unexpected_tag` is -2 when
`_group_held` confirms the innermost group already holds the name and -4 otherwise, so each tag flagged
before its time costs one more, and the first still opens the block however many follow.
**Builtin names must stay keyword-extractable, or the fallbacks swallow them.** `word: $.identifier` turns
@@ -249,7 +377,7 @@ README nothing checks at all. The library-contents differential below, run again
### External scanner (`src/scanner.c`)
Eighteen external tokens, for the constraints a context-free grammar cannot express:
Twenty external tokens, for the constraints a context-free grammar cannot express:
- `push_block`/`push_partial`/`push_verbatim` and the matching `pop_*` — the names in
`{% block a %}…{% endblock a %}`. A push token reads the name into `pending_name`, which the group takes
@@ -305,6 +433,17 @@ Eighteen external tokens, for the constraints a context-free grammar cannot expr
`{% if %}…{% else %}{% if %}…{% else %}`, because the outer group, having held an `else`, claimed the
inner one's.
- `_text_brace` — a single `{` that is text although the content regex cannot tell: before `{%` or `{#`,
and at the end of input. Django splits a template with `({%.*?%}|{{.*?}}|{#.*?#})`, so a `{` is text
unless one of those starts at it; `{{{` is not a text brace, since `{{` starts a variable at its first
brace. `content` is a right-associative run of `_text` and `_text_brace`, so it stays one node. Inside a
group's body the missing-tag check reads past `{` first, so it scans the brace itself
(`scan_text_brace_rest`) rather than give up the position.
- `_missing_variable_close` — the marker where a variable's `}}` is missing, aliased to
`missing_variable_close`. It is placed only where the variable cannot go on — at the end of input, or
before a `}` that is not `}}`, a `%` or a `{` — so `{{ hello }{% if x %}` closes the variable, reads the
stray `}` as text and parses the `if` normally, where error recovery used to take the rest of the file.
It is valid only after a variable's value, never where `content` is. It added 18 states.
- `matcher_error` — used by no rule; returned only if the scanner reaches an error state.
`check_space()` defines whitespace for the scanner and **must stay in sync with `_sep`**, because the
@@ -368,7 +507,9 @@ A branch that is also a `@local.scope` confines its bindings whichever way the b
`context.push()` as the loop body, so `empty_block` is a scope alongside `for_block`.
`queries/errors.scm` collects what an editor should report: `(ERROR)` as `@error.syntax`, `(MISSING)` as
`@error.missing`, and every missing-tag marker as `@error.missing_tag`. A tree holding only markers has
`@error.missing`, every missing-tag marker as `@error.missing_tag`, `unexpected_tag` and
`unexpected_argument` as `@error.unexpected_tag` and `@error.unexpected_argument`, and
`missing_variable_close` as `@error.missing_variable_close`. A tree holding only markers has
no `has_error`, so a tool that checks that flag alone misses them. Adding a tag group means adding its
marker there, since no supertype matches them all.
@@ -423,8 +564,8 @@ unavoidable limitation, or a case where this grammar is the more permissive one
binds an `identifier`, because the keyword is not valid where a binding name is — but `{{ True.x }}` is
an error here and an ordinary two-part lookup in Django, which renders `string_if_invalid`.
- A `with` binding written twice (`{% with a=1 a=2 %}`, and the same in `{% include %}` and
`{% blocktranslate %}`) is an error here and not in Django, where `token_kwargs` builds a dict and the
later value wins. It has no use, so it is treated as the mistake it almost certainly is.
`{% blocktranslate %}`) is reported here, as an `unexpected_argument`, and not in Django, where
`token_kwargs` builds a dict and the later value wins. It has no use, so it is treated as the mistake it almost certainly is.
- `{% csp_nonce_attr "a" media="b" %}` is accepted, matching Django, which only rejects it at render time.
- A misspelled tag or filter cannot be caught: `{{ x|lenght }}` is indistinguishable from a filter some
library registered. Cross-referencing names against `{% load %}` needs the project's Python, so it
@@ -437,7 +578,7 @@ unavoidable limitation, or a case where this grammar is the more permissive one
redefines `length` with a different arity gets a false error here.
- Django's `static` tags ignore what follows their argument: `StaticNode.handle_token` looks for `as` two
bits from the end, so `{% static "a" junk %}` and `{% static "a" as u v %}` are accepted there and
rejected here, and `PrefixNode.handle_token` raises `IndexError` rather than `TemplateSyntaxError` on
reported here as an `unexpected_argument`, and `PrefixNode.handle_token` raises `IndexError` rather than `TemplateSyntaxError` on
`{% get_static_prefix as %}`.
- Django's `{% cache %}` takes its fragment name as the raw word in that position and finds the cache to
use by testing whether the last word starts with `using=`, and only when something precedes it. So
@@ -450,3 +591,8 @@ unavoidable limitation, or a case where this grammar is the more permissive one
`{% for %}` closed while an `{% if %}` inside it is still open, holds a `missing_endif_tag` marker
where the end tag belongs. The marker is the error, reported through `queries/errors.scm`; a check
for `ERROR` nodes alone accepts the tree.
- A `{{` that does not close is text in Django, whose lexer only takes a variable written `{{ … }}` on one
line, so `{{ hello }` renders as written. Here it is a `template_variable` closed by a
`missing_variable_close` marker, reported through `queries/errors.scm`, since a half-written variable is
what it almost always is. The same lexer makes `{{ hello\n}}` text too, which this grammar accepts as a
variable, because its separators allow newlines.
+76
View File
@@ -0,0 +1,76 @@
cmake_minimum_required(VERSION 3.13)
project(tree-sitter-django
VERSION "0.1.0"
DESCRIPTION "Django grammar for tree-sitter"
HOMEPAGE_URL "https://github.com/danhanson/tree-sitter-django"
LANGUAGES C)
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
option(TREE_SITTER_REUSE_ALLOCATOR "Reuse the library allocator" OFF)
set(TREE_SITTER_ABI_VERSION 15 CACHE STRING "Tree-sitter ABI version")
if(NOT ${TREE_SITTER_ABI_VERSION} MATCHES "^[0-9]+$")
unset(TREE_SITTER_ABI_VERSION CACHE)
message(FATAL_ERROR "TREE_SITTER_ABI_VERSION must be an integer")
endif()
include(GNUInstallDirs)
find_program(TREE_SITTER_CLI tree-sitter DOC "Tree-sitter CLI")
add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
"${CMAKE_CURRENT_SOURCE_DIR}/src/node-types.json"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/grammar.js"
COMMAND "${TREE_SITTER_CLI}" generate grammar.js --no-parser
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Generating grammar.json")
add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/parser.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/alloc.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/array.h"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
--abi=${TREE_SITTER_ABI_VERSION}
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Generating parser.c")
add_library(tree-sitter-django src/parser.c)
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/scanner.c)
target_sources(tree-sitter-django PRIVATE src/scanner.c)
endif()
target_include_directories(tree-sitter-django
PRIVATE src
INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/bindings/c>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
target_compile_definitions(tree-sitter-django PRIVATE
$<$<BOOL:${TREE_SITTER_REUSE_ALLOCATOR}>:TREE_SITTER_REUSE_ALLOCATOR>
$<$<CONFIG:Debug>:TREE_SITTER_DEBUG>)
set_target_properties(tree-sitter-django
PROPERTIES
C_STANDARD 11
POSITION_INDEPENDENT_CODE ON
SOVERSION "${TREE_SITTER_ABI_VERSION}.${PROJECT_VERSION_MAJOR}"
DEFINE_SYMBOL "")
configure_file(bindings/c/tree-sitter-django.pc.in
"${CMAKE_CURRENT_BINARY_DIR}/tree-sitter-django.pc" @ONLY)
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bindings/c/tree_sitter"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
FILES_MATCHING PATTERN "*.h")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/tree-sitter-django.pc"
DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
install(TARGETS tree-sitter-django
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}")
file(GLOB QUERIES queries/*.scm)
install(FILES ${QUERIES}
DESTINATION "${CMAKE_INSTALL_DATADIR}/tree-sitter/queries/django")
add_custom_target(ts-test "${TREE_SITTER_CLI}" test
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "tree-sitter test")
Generated
+34
View File
@@ -0,0 +1,34 @@
[package]
name = "tree-sitter-django"
description = "Django grammar for tree-sitter"
version = "0.1.0"
authors = ["Daniel Hanson"]
license = "MIT"
readme = "README.md"
keywords = ["incremental", "parsing", "tree-sitter", "django"]
categories = ["parser-implementations", "parsing", "text-editors"]
repository = "https://github.com/danhanson/tree-sitter-django"
edition = "2021"
autoexamples = false
build = "bindings/rust/build.rs"
include = [
"bindings/rust/*",
"grammar.js",
"queries/*",
"src/*",
"tree-sitter.json",
"/LICENSE",
]
[lib]
path = "bindings/rust/lib.rs"
[dependencies]
tree-sitter-language = "0.1"
[build-dependencies]
cc = "1.2"
[dev-dependencies]
tree-sitter = "0.26.12"
Generated
+116
View File
@@ -0,0 +1,116 @@
LANGUAGE_NAME := tree-sitter-django
HOMEPAGE_URL := https://github.com/danhanson/tree-sitter-django
VERSION := 0.1.0
DESCRIPTION := Django grammar for tree-sitter
# repository
SRC_DIR := src
TS ?= tree-sitter
# install directory layout
PREFIX ?= /usr/local
DATADIR ?= $(PREFIX)/share
INCLUDEDIR ?= $(PREFIX)/include
LIBDIR ?= $(PREFIX)/lib
BINDIR ?= $(PREFIX)/bin
PCLIBDIR ?= $(LIBDIR)/pkgconfig
# source/object files
PARSER := $(SRC_DIR)/parser.c
EXTRAS := $(filter-out $(PARSER),$(wildcard $(SRC_DIR)/*.c))
OBJS := $(patsubst %.c,%.o,$(PARSER) $(EXTRAS))
# flags
ARFLAGS ?= rcs
override CFLAGS += -I$(SRC_DIR) -std=c11 -fPIC
# ABI versioning
SONAME_MAJOR = $(shell sed -n 's/\#define LANGUAGE_VERSION //p' $(PARSER))
SONAME_MINOR = $(word 1,$(subst ., ,$(VERSION)))
# OS-specific bits
MACHINE := $(shell $(CC) -dumpmachine)
ifneq ($(findstring darwin,$(MACHINE)),)
SOEXT = dylib
SOEXTVER_MAJOR = $(SONAME_MAJOR).$(SOEXT)
SOEXTVER = $(SONAME_MAJOR).$(SONAME_MINOR).$(SOEXT)
LINKSHARED = -dynamiclib -Wl,-install_name,$(LIBDIR)/lib$(LANGUAGE_NAME).$(SOEXTVER),-rpath,@executable_path/../Frameworks
else ifneq ($(findstring mingw32,$(MACHINE)),)
SOEXT = dll
LINKSHARED += -s -shared -Wl,--out-implib,lib$(LANGUAGE_NAME).dll.a
else
SOEXT = so
SOEXTVER_MAJOR = $(SOEXT).$(SONAME_MAJOR)
SOEXTVER = $(SOEXT).$(SONAME_MAJOR).$(SONAME_MINOR)
LINKSHARED = -shared -Wl,-soname,lib$(LANGUAGE_NAME).$(SOEXTVER)
ifneq ($(filter $(shell uname),FreeBSD NetBSD DragonFly),)
PCLIBDIR := $(PREFIX)/libdata/pkgconfig
endif
endif
all: lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT) $(LANGUAGE_NAME).pc
lib$(LANGUAGE_NAME).a: $(OBJS)
$(AR) $(ARFLAGS) $@ $^
lib$(LANGUAGE_NAME).$(SOEXT): $(OBJS)
$(CC) $(LDFLAGS) $(LINKSHARED) $^ $(LDLIBS) -o $@
ifneq ($(STRIP),)
$(STRIP) $@
endif
ifneq ($(findstring mingw32,$(MACHINE)),)
lib$(LANGUAGE_NAME).dll.a: lib$(LANGUAGE_NAME).$(SOEXT)
endif
$(LANGUAGE_NAME).pc: bindings/c/$(LANGUAGE_NAME).pc.in
sed -e 's|@PROJECT_VERSION@|$(VERSION)|' \
-e 's|@CMAKE_INSTALL_LIBDIR@|$(LIBDIR:$(PREFIX)/%=%)|' \
-e 's|@CMAKE_INSTALL_INCLUDEDIR@|$(INCLUDEDIR:$(PREFIX)/%=%)|' \
-e 's|@PROJECT_DESCRIPTION@|$(DESCRIPTION)|' \
-e 's|@PROJECT_HOMEPAGE_URL@|$(HOMEPAGE_URL)|' \
-e 's|@CMAKE_INSTALL_PREFIX@|$(PREFIX)|' $< > $@
$(SRC_DIR)/grammar.json: grammar.js
$(TS) generate --no-parser $^
$(PARSER): $(SRC_DIR)/grammar.json
$(TS) generate $^
install: all
install -d '$(DESTDIR)$(DATADIR)'/tree-sitter/queries/django '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter '$(DESTDIR)$(PCLIBDIR)' '$(DESTDIR)$(LIBDIR)'
install -m644 bindings/c/tree_sitter/$(LANGUAGE_NAME).h '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h
install -m644 $(LANGUAGE_NAME).pc '$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc
install -m644 lib$(LANGUAGE_NAME).a '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a
install -m755 lib$(LANGUAGE_NAME).$(SOEXT) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER)
ifneq ($(findstring mingw32,$(MACHINE)),)
install -d '$(DESTDIR)$(BINDIR)'
install -m755 lib$(LANGUAGE_NAME).dll '$(DESTDIR)$(BINDIR)'/lib$(LANGUAGE_NAME).dll
install -m755 lib$(LANGUAGE_NAME).dll.a '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).dll.a
else
install -m755 lib$(LANGUAGE_NAME).$(SOEXT) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER)
cd '$(DESTDIR)$(LIBDIR)' && ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER) lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR)
cd '$(DESTDIR)$(LIBDIR)' && ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) lib$(LANGUAGE_NAME).$(SOEXT)
endif
ifneq ($(wildcard queries/*.scm),)
install -m644 queries/*.scm '$(DESTDIR)$(DATADIR)'/tree-sitter/queries/django
endif
uninstall:
$(RM) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a \
'$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER) \
'$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) \
'$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT) \
'$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h \
'$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc
$(RM) -r '$(DESTDIR)$(DATADIR)'/tree-sitter/queries/django
clean:
$(RM) $(OBJS) $(LANGUAGE_NAME).pc lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT) lib$(LANGUAGE_NAME).dll.a
test:
$(TS) test
.PHONY: all install uninstall clean test
Generated
+41
View File
@@ -0,0 +1,41 @@
// swift-tools-version:5.3
import Foundation
import PackageDescription
var sources = ["src/parser.c"]
if FileManager.default.fileExists(atPath: "src/scanner.c") {
sources.append("src/scanner.c")
}
let package = Package(
name: "TreeSitterDjango",
products: [
.library(name: "TreeSitterDjango", targets: ["TreeSitterDjango"]),
],
dependencies: [
.package(url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.10.0"),
],
targets: [
.target(
name: "TreeSitterDjango",
dependencies: [],
path: ".",
sources: sources,
resources: [
.copy("queries")
],
publicHeadersPath: "bindings/swift",
cSettings: [.headerSearchPath("src")]
),
.testTarget(
name: "TreeSitterDjangoTests",
dependencies: [
.product(name: "SwiftTreeSitter", package: "swift-tree-sitter"),
"TreeSitterDjango",
],
path: "bindings/swift/TreeSitterDjangoTests"
)
],
cLanguageStandard: .c11
)
+73 -18
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,18 +18,58 @@ disagree is listed under [Divergences](#divergences-from-django).
## Using it
There is no published package yet, and no language bindings — those are coming. 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
files, which is what a tree-sitter host reads to wire all of the above together.
- **`bindings/`** holds a binding per language, generated by `tree-sitter init` from the `bindings` and
`metadata` sections of `tree-sitter.json`. See [Bindings](#bindings).
## Bindings
| Language | Build from a checkout | Entry point |
| -------- | ------------------------------------------------------------- | ------------------------------------------------------ |
| 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` |
| Swift | `swift build` | `bindings/swift/TreeSitterDjango`, `Package.swift` |
| C / C++ | `make` (static, shared and a pkg-config file) or `cmake` | `bindings/c/tree_sitter/tree-sitter-django.h` |
```js
import Parser from "tree-sitter";
import django from "tree-sitter-django";
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
language = tree_sitter.Language(tree_sitter_django.language())
tree = tree_sitter.Parser(language).parse(b"{% if x %}hi {{ y|upper }}{% endif %}")
```
## Development
@@ -46,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:
@@ -69,7 +118,9 @@ Five of the files in `queries/` are query files an editor loads:
it. A marker is placed only where the scanner can tell: at the end of input, before an end tag that
belongs to an enclosing group, or before an `{% endblock %}` naming a block further out.
A group's tag written where no open group can hold it, such as `{% else %}` inside a `{% for %}`, is an
`unexpected_tag`, captured as `@error.unexpected_tag`.
`unexpected_tag`, captured as `@error.unexpected_tag`. Words a tag does not take, such as the `x` in
`{% else x %}`, are an `unexpected_argument` inside that tag, captured as `@error.unexpected_argument`. A `{{` whose `}}` is missing closes with a zero-width
`missing_variable_close`, captured as `@error.missing_variable_close`, so the rest of the template still parses.
The other three are data for a tool rather than queries an editor runs. Each captures the two halves of a
relation a tree-sitter query cannot express, and the tool does the join:
@@ -114,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
@@ -122,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.
@@ -163,7 +216,7 @@ export default grammar(django, {
map_tag: ($) =>
seq(
$.simpleTagOpen,
field("tag", "map"),
field("tag_name", "map"),
$._sep,
$.filtered_value,
$._sep,
@@ -177,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,
@@ -195,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)),
),
},
});
@@ -224,7 +277,9 @@ Seven things to know:
`conflicts` entry, as every block in the base grammar does. The generator offers an associativity
instead; taking it silently discards the parse in which the body continues.
- **End tags are not reserved**, a stray `{% endmap %}` parses as a `custom_tag` rather than an error — `reserved` takes no
`previous`, so the `tag_name` list cannot be added to a name at a time. Instead you may drop the `custom_tag` rule to avoid parsing end tags as custom tags.
`previous`, so the `tag_name` list cannot be added to a name at a time. Instead you may drop the `custom_tag` rule to avoid parsing end tags as custom tags. While the fallback
is there, stray words in one of your end tags (`{% endloop extra %}`) read as a `custom_tag` too, which
leaves your group with a missing-tag marker at the end of input instead of an `unexpected_argument`.
- **Your groups get missing-tag markers.** The scanner knows no group by name — a group is closed by `end`
followed by its opening tag's name — so the `alias($._missing_tag, $.missing_endloop_tag)` choice
above is all it takes: an unclosed `{% loop x %}`, or one left open inside an `{% if %}` that
@@ -271,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
+35
View File
@@ -0,0 +1,35 @@
{
"targets": [
{
"target_name": "tree_sitter_django_binding",
"dependencies": [
"<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
],
"include_dirs": [
"src",
],
"sources": [
"bindings/node/binding.cc",
"src/parser.c",
],
"variables": {
"has_scanner": "<!(node -p \"fs.existsSync('src/scanner.c')\")"
},
"conditions": [
["has_scanner=='true'", {
"sources+": ["src/scanner.c"],
}],
["OS!='win'", {
"cflags_c": [
"-std=c11",
],
}, { # OS == "win"
"cflags_c": [
"/std:c11",
"/utf-8",
],
}],
],
}
]
}
+10
View File
@@ -0,0 +1,10 @@
prefix=@CMAKE_INSTALL_PREFIX@
libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@
includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@
Name: tree-sitter-django
Description: @PROJECT_DESCRIPTION@
URL: @PROJECT_HOMEPAGE_URL@
Version: @PROJECT_VERSION@
Libs: -L${libdir} -ltree-sitter-django
Cflags: -I${includedir}
+16
View File
@@ -0,0 +1,16 @@
#ifndef TREE_SITTER_DJANGO_H_
#define TREE_SITTER_DJANGO_H_
typedef struct TSLanguage TSLanguage;
#ifdef __cplusplus
extern "C" {
#endif
const TSLanguage *tree_sitter_django(void);
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_DJANGO_H_
+15
View File
@@ -0,0 +1,15 @@
package tree_sitter_django
// #cgo CFLAGS: -std=c11 -fPIC
// #include "../../src/parser.c"
// #if __has_include("../../src/scanner.c")
// #include "../../src/scanner.c"
// #endif
import "C"
import "unsafe"
// Get the tree-sitter Language for this grammar.
func Language() unsafe.Pointer {
return unsafe.Pointer(C.tree_sitter_django())
}
+15
View File
@@ -0,0 +1,15 @@
package tree_sitter_django_test
import (
"testing"
tree_sitter "github.com/tree-sitter/go-tree-sitter"
tree_sitter_django "github.com/danhanson/tree-sitter-django/bindings/go"
)
func TestCanLoadGrammar(t *testing.T) {
language := tree_sitter.NewLanguage(tree_sitter_django.Language())
if language == nil {
t.Errorf("Error loading Django Parser grammar")
}
}
+19
View File
@@ -0,0 +1,19 @@
#include <napi.h>
typedef struct TSLanguage TSLanguage;
extern "C" TSLanguage *tree_sitter_django();
// "tree-sitter", "language" hashed with BLAKE2
const napi_type_tag LANGUAGE_TYPE_TAG = {
0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
auto language = Napi::External<TSLanguage>::New(env, tree_sitter_django());
language.TypeTag(&LANGUAGE_TYPE_TAG);
exports["language"] = language;
return exports;
}
NODE_API_MODULE(tree_sitter_django_binding, Init)
+11
View File
@@ -0,0 +1,11 @@
import assert from "node:assert";
import { test } from "node:test";
import Parser from "tree-sitter";
test("can load grammar", () => {
const parser = new Parser();
assert.doesNotReject(async () => {
const { default: language } = await import("./index.js");
parser.setLanguage(language);
});
});
Generated Vendored
+60
View File
@@ -0,0 +1,60 @@
type BaseNode = {
type: string;
named: boolean;
};
type ChildNode = {
multiple: boolean;
required: boolean;
types: BaseNode[];
};
type NodeInfo =
| (BaseNode & {
subtypes: BaseNode[];
})
| (BaseNode & {
fields: { [name: string]: ChildNode };
children: ChildNode[];
});
/**
* The tree-sitter language object for this grammar.
*
* @see {@linkcode https://tree-sitter.github.io/node-tree-sitter/interfaces/Parser.Language.html Parser.Language}
*
* @example
* import Parser from "tree-sitter";
* import django from "tree-sitter-django";
*
* const parser = new Parser();
* parser.setLanguage(django);
*/
declare const binding: {
/**
* The inner language object.
* @private
*/
language: unknown;
/**
* The content of the `node-types.json` file for this grammar.
*
* @see {@linkplain https://tree-sitter.github.io/tree-sitter/using-parsers/6-static-node-types Static Node Types}
*/
nodeTypeInfo: NodeInfo[];
/** The syntax highlighting query for this grammar. */
HIGHLIGHTS_QUERY?: string;
/** The language injection query for this grammar. */
INJECTIONS_QUERY?: string;
/** The local variable query for this grammar. */
LOCALS_QUERY?: string;
/** The symbol tagging query for this grammar. */
TAGS_QUERY?: string;
};
export default binding;
+37
View File
@@ -0,0 +1,37 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("../..", import.meta.url));
const binding = typeof process.versions.bun === "string"
// Support `bun build --compile` by being statically analyzable enough to find the .node file at build-time
? await import(`${root}/prebuilds/${process.platform}-${process.arch}/tree-sitter-django.node`)
: (await import("node-gyp-build")).default(root);
try {
const nodeTypes = await import(`${root}/src/node-types.json`, { with: { type: "json" } });
binding.nodeTypeInfo = nodeTypes.default;
} catch { }
const queries = [
["HIGHLIGHTS_QUERY", `${root}/queries/highlights.scm`],
["INJECTIONS_QUERY", `${root}/queries/injections.scm`],
["LOCALS_QUERY", `${root}/queries/locals.scm`],
["TAGS_QUERY", `${root}/queries/tags.scm`],
];
for (const [prop, path] of queries) {
Object.defineProperty(binding, prop, {
configurable: true,
enumerable: true,
get() {
delete binding[prop];
try {
binding[prop] = readFileSync(path, "utf8");
} catch { }
return binding[prop];
}
});
}
export default binding;
+12
View File
@@ -0,0 +1,12 @@
from unittest import TestCase
from tree_sitter import Language, Parser
import tree_sitter_django
class TestLanguage(TestCase):
def test_can_load_grammar(self):
try:
Parser(Language(tree_sitter_django.language()))
except Exception:
self.fail("Error loading Django Parser grammar")
+43
View File
@@ -0,0 +1,43 @@
"""Django grammar for tree-sitter"""
from importlib.resources import files as _files
from ._binding import language
def _get_query(name, file):
try:
query = _files(f"{__package__}") / file
globals()[name] = query.read_text()
except FileNotFoundError:
globals()[name] = None
return globals()[name]
def __getattr__(name):
if name == "HIGHLIGHTS_QUERY":
return _get_query("HIGHLIGHTS_QUERY", "queries/highlights.scm")
if name == "INJECTIONS_QUERY":
return _get_query("INJECTIONS_QUERY", "queries/injections.scm")
if name == "LOCALS_QUERY":
return _get_query("LOCALS_QUERY", "queries/locals.scm")
if name == "TAGS_QUERY":
return _get_query("TAGS_QUERY", "queries/tags.scm")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
"language",
"HIGHLIGHTS_QUERY",
"INJECTIONS_QUERY",
"LOCALS_QUERY",
"TAGS_QUERY",
]
def __dir__():
return sorted(__all__ + [
"__all__", "__builtins__", "__cached__", "__doc__", "__file__",
"__loader__", "__name__", "__package__", "__path__", "__spec__",
])
+17
View File
@@ -0,0 +1,17 @@
from typing import Final
from typing_extensions import CapsuleType
HIGHLIGHTS_QUERY: Final[str] | None
"""The syntax highlighting query for this grammar."""
INJECTIONS_QUERY: Final[str] | None
"""The language injection query for this grammar."""
LOCALS_QUERY: Final[str] | None
"""The local variable query for this grammar."""
TAGS_QUERY: Final[str] | None
"""The symbol tagging query for this grammar."""
def language() -> CapsuleType:
"""The tree-sitter language function for this grammar."""
+35
View File
@@ -0,0 +1,35 @@
#include <Python.h>
typedef struct TSLanguage TSLanguage;
TSLanguage *tree_sitter_django(void);
static PyObject* _binding_language(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) {
return PyCapsule_New(tree_sitter_django(), "tree_sitter.Language", NULL);
}
static struct PyModuleDef_Slot slots[] = {
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
static PyMethodDef methods[] = {
{"language", _binding_language, METH_NOARGS,
"Get the tree-sitter language for this grammar."},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "_binding",
.m_doc = NULL,
.m_size = 0,
.m_methods = methods,
.m_slots = slots,
};
PyMODINIT_FUNC PyInit__binding(void) {
return PyModuleDef_Init(&module);
}
View File
Whitespace-only changes.
+56
View File
@@ -0,0 +1,56 @@
fn main() {
let src_dir = std::path::Path::new("src");
let mut c_config = cc::Build::new();
c_config.std("c11").include(src_dir);
#[cfg(target_env = "msvc")]
c_config.flag("-utf-8");
if std::env::var("TARGET").unwrap() == "wasm32-unknown-unknown" {
let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
};
let Ok(wasm_src) =
std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
};
c_config.include(&wasm_headers);
c_config.files([
wasm_src.join("stdio.c"),
wasm_src.join("stdlib.c"),
wasm_src.join("string.c"),
]);
}
let parser_path = src_dir.join("parser.c");
c_config.file(&parser_path);
println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap());
let scanner_path = src_dir.join("scanner.c");
if scanner_path.exists() {
c_config.file(&scanner_path);
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
}
c_config.compile("tree-sitter-django");
println!("cargo:rustc-check-cfg=cfg(with_highlights_query)");
if !"queries/highlights.scm".is_empty() && std::path::Path::new("queries/highlights.scm").exists() {
println!("cargo:rustc-cfg=with_highlights_query");
}
println!("cargo:rustc-check-cfg=cfg(with_injections_query)");
if !"queries/injections.scm".is_empty() && std::path::Path::new("queries/injections.scm").exists() {
println!("cargo:rustc-cfg=with_injections_query");
}
println!("cargo:rustc-check-cfg=cfg(with_locals_query)");
if !"queries/locals.scm".is_empty() && std::path::Path::new("queries/locals.scm").exists() {
println!("cargo:rustc-cfg=with_locals_query");
}
println!("cargo:rustc-check-cfg=cfg(with_tags_query)");
if !"queries/tags.scm".is_empty() && std::path::Path::new("queries/tags.scm").exists() {
println!("cargo:rustc-cfg=with_tags_query");
}
}
+60
View File
@@ -0,0 +1,60 @@
//! This crate provides Django Parser language support for the [tree-sitter] parsing library.
//!
//! Typically, you will use the [`LANGUAGE`] constant to add this language to a
//! tree-sitter [`Parser`], and then use the parser to parse some code:
//!
//! ```
//! let code = r#"
//! "#;
//! let mut parser = tree_sitter::Parser::new();
//! let language = tree_sitter_django::LANGUAGE;
//! parser
//! .set_language(&language.into())
//! .expect("Error loading Django Parser parser");
//! let tree = parser.parse(code, None).unwrap();
//! assert!(!tree.root_node().has_error());
//! ```
//!
//! [`Parser`]: https://docs.rs/tree-sitter/0.26.12/tree_sitter/struct.Parser.html
//! [tree-sitter]: https://tree-sitter.github.io/
use tree_sitter_language::LanguageFn;
extern "C" {
fn tree_sitter_django() -> *const ();
}
/// The tree-sitter [`LanguageFn`] for this grammar.
pub const LANGUAGE: LanguageFn = unsafe { LanguageFn::from_raw(tree_sitter_django) };
/// The content of the [`node-types.json`] file for this grammar.
///
/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers/6-static-node-types
pub const NODE_TYPES: &str = include_str!("../../src/node-types.json");
#[cfg(with_highlights_query)]
/// The syntax highlighting query for this grammar.
pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm");
#[cfg(with_injections_query)]
/// The language injection query for this grammar.
pub const INJECTIONS_QUERY: &str = include_str!("../../queries/injections.scm");
#[cfg(with_locals_query)]
/// The local variable query for this grammar.
pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm");
#[cfg(with_tags_query)]
/// The symbol tagging query for this grammar.
pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm");
#[cfg(test)]
mod tests {
#[test]
fn test_can_load_grammar() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&super::LANGUAGE.into())
.expect("Error loading Django Parser parser");
}
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef TREE_SITTER_DJANGO_H_
#define TREE_SITTER_DJANGO_H_
typedef struct TSLanguage TSLanguage;
#ifdef __cplusplus
extern "C" {
#endif
const TSLanguage *tree_sitter_django(void);
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_DJANGO_H_
@@ -0,0 +1,12 @@
import XCTest
import SwiftTreeSitter
import TreeSitterDjango
final class TreeSitterDjangoTests: XCTestCase {
func testCanLoadGrammar() throws {
let parser = Parser()
let language = Language(language: tree_sitter_django())
XCTAssertNoThrow(try parser.setLanguage(language),
"Error loading Django Parser grammar")
}
}
Generated
+7
View File
@@ -0,0 +1,7 @@
module github.com/danhanson/tree-sitter-django
go 1.22
require github.com/tree-sitter/go-tree-sitter v0.24.0
require github.com/mattn/go-pointer v0.0.1 // indirect
Generated
+36
View File
@@ -0,0 +1,36 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tree-sitter/go-tree-sitter v0.24.0 h1:kRZb6aBNfcI/u0Qh8XEt3zjNVnmxTisDBN+kXK0xRYQ=
github.com/tree-sitter/go-tree-sitter v0.24.0/go.mod h1:x681iFVoLMEwOSIHA1chaLkXlroXEN7WY+VHGFaoDbk=
github.com/tree-sitter/tree-sitter-c v0.21.5-0.20240818205408-927da1f210eb h1:A8425heRM8mylnv4H58FPUiH+aYivyitre0PzxrfmWs=
github.com/tree-sitter/tree-sitter-c v0.21.5-0.20240818205408-927da1f210eb/go.mod h1:dOF6gtQiF9UwNh995T5OphYmtIypkjsp3ap7r9AN/iA=
github.com/tree-sitter/tree-sitter-cpp v0.22.4-0.20240818224355-b1a4e2b25148 h1:AfFPZwtwGN01BW1jDdqBVqscTwetvMpydqYZz57RSlc=
github.com/tree-sitter/tree-sitter-cpp v0.22.4-0.20240818224355-b1a4e2b25148/go.mod h1:Bh6U3viD57rFXRYIQ+kmiYtr+1Bx0AceypDLJJSyi9s=
github.com/tree-sitter/tree-sitter-embedded-template v0.21.1-0.20240819044651-ffbf64942c33 h1:TwqSV3qLp3tKSqirGLRHnjFk9Tc2oy57LIl+FQ4GjI4=
github.com/tree-sitter/tree-sitter-embedded-template v0.21.1-0.20240819044651-ffbf64942c33/go.mod h1:CvCKCt3v04Ufos1zZnNCelBDeCGRpPucaN8QczoUsN4=
github.com/tree-sitter/tree-sitter-go v0.21.3-0.20240818010209-8c0f0e7a6012 h1:Xvxck3tE5FW7F7bTS97iNM2ADMyCMJztVqn5HYKdJGo=
github.com/tree-sitter/tree-sitter-go v0.21.3-0.20240818010209-8c0f0e7a6012/go.mod h1:T40D0O1cPvUU/+AmiXVXy1cncYQT6wem4Z0g4SfAYvY=
github.com/tree-sitter/tree-sitter-html v0.20.5-0.20240818004741-d11201a263d0 h1:c46K6uh5Dz00zJeU9BfjXdb8I+E4RkUdfnWJpQADXFo=
github.com/tree-sitter/tree-sitter-html v0.20.5-0.20240818004741-d11201a263d0/go.mod h1:hcNt/kOJHcIcuMvouE7LJcYdeFUFbVpBJ6d4wmOA+tU=
github.com/tree-sitter/tree-sitter-java v0.21.1-0.20240824015150-576d8097e495 h1:jrt4qbJVEFs4H93/ITxygHc6u0TGqAkkate7TQ4wFSA=
github.com/tree-sitter/tree-sitter-java v0.21.1-0.20240824015150-576d8097e495/go.mod h1:oyaR7fLnRV0hT9z6qwE9GkaeTom/hTDwK3H2idcOJFc=
github.com/tree-sitter/tree-sitter-javascript v0.21.5-0.20240818005344-15887341e5b5 h1:om4X9AVg3asL8gxNJDcz4e/Wp+VpQj1PY3uJXKr6EOg=
github.com/tree-sitter/tree-sitter-javascript v0.21.5-0.20240818005344-15887341e5b5/go.mod h1:nNqgPoV/h9uYWk6kYEFdEAhNVOacpfpRW5SFmdaP4tU=
github.com/tree-sitter/tree-sitter-json v0.21.1-0.20240818005659-bdd69eb8c8a5 h1:pfV3G3k7NCKqKk8THBmyuh2zA33lgYHS3GVrzRR8ry4=
github.com/tree-sitter/tree-sitter-json v0.21.1-0.20240818005659-bdd69eb8c8a5/go.mod h1:GbMKRjLfk0H+PI7nLi1Sx5lHf5wCpLz9al8tQYSxpEk=
github.com/tree-sitter/tree-sitter-php v0.22.9-0.20240819002312-a552625b56c1 h1:ZXZMDwE+IhUtGug4Brv6NjJWUU3rfkZBKpemf6RY8/g=
github.com/tree-sitter/tree-sitter-php v0.22.9-0.20240819002312-a552625b56c1/go.mod h1:UKCLuYnJ312Mei+3cyTmGOHzn0YAnaPRECgJmHtzrqs=
github.com/tree-sitter/tree-sitter-python v0.21.1-0.20240818005537-55a9b8a4fbfb h1:EXEM82lFM7JjJb6qiKZXkpIDaCcbV2obNn82ghwj9lw=
github.com/tree-sitter/tree-sitter-python v0.21.1-0.20240818005537-55a9b8a4fbfb/go.mod h1:lXCF1nGG5Dr4J3BTS0ObN4xJCCICiSu/b+Xe/VqMV7g=
github.com/tree-sitter/tree-sitter-ruby v0.21.1-0.20240818211811-7dbc1e2d0e2d h1:fcYCvoXdcP1uRQYXqJHRy6Hec+uKScQdKVtMwK9JeCI=
github.com/tree-sitter/tree-sitter-ruby v0.21.1-0.20240818211811-7dbc1e2d0e2d/go.mod h1:T1nShQ4v5AJtozZ8YyAS4uzUtDAJj/iv4YfwXSbUHzg=
github.com/tree-sitter/tree-sitter-rust v0.21.3-0.20240818005432-2b43eafe6447 h1:o9alBu1J/WjrcTKEthYtXmdkDc5OVXD+PqlvnEZ0Lzc=
github.com/tree-sitter/tree-sitter-rust v0.21.3-0.20240818005432-2b43eafe6447/go.mod h1:1Oh95COkkTn6Ezp0vcMbvfhRP5gLeqqljR0BYnBzWvc=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+125 -20
View File
@@ -281,7 +281,14 @@ function simpleTag(name, args = true, kwargs = true) {
const named = Object.entries(kwargs).map(([name, value]) =>
part(seq(sym("_kwarg_name"), name, "=", value)),
);
parts.push(repeat(choice(...named)));
// a name the signature does not take is flagged in place
const unexpected = part(
seq(
sym("_kwarg_name"),
alias(sym("_unexpected_keyword"), sym("unexpected_argument")),
),
);
parts.push(repeat(choice(...named, unexpected)));
}
return tag(name, ...parts, optional(asVariable));
}
@@ -514,6 +521,12 @@ const django = grammar({
[$.timezone_block],
[$.with_block],
[$.binaryOperator],
[$.predicate],
[$._translate_option],
[$.unexpected_argument],
[$.variable_attribute, $._unexpected_word],
[$.resetcycle_tag, $._unexpected_word],
[$.library, $._unexpected_word],
[$.library, $.load_tag],
[$._filtered_value_spaced],
[$._filter_expression_spaced],
@@ -552,6 +565,8 @@ const django = grammar({
$._group_repeat,
$._group_close,
$._group_held,
$._text_brace,
$._missing_variable_close,
],
reserved: {
global: ($) => ["not", "if", "in", "is", "as", "for", "from"],
@@ -561,16 +576,43 @@ const django = grammar({
template: ($) => repeat1(choice($.template_node, $.content)),
// the tags of a group; see startTag
simpleTagOpen: ($) => seq("{%", $._group_open_tag_read, optional(SEP)),
simpleTagClose: ($) => seq(optional(SEP), "%}"),
simpleTagClose: ($) =>
seq(optional($.unexpected_argument), optional(SEP), "%}"),
startTagOpen: ($) => seq("{%", $._group_open_tag_read, optional(SEP)),
startTagClose: ($) => seq($._group_open_tag_push, optional(SEP), "%}"),
startTagClose: ($) =>
seq(
optional($.unexpected_argument),
$._group_open_tag_push,
optional(SEP),
"%}",
),
followTagOpen: ($) => seq("{%", $._group_open_tag_read, optional(SEP)),
followTagClose: ($) => seq($._group_follow, optional(SEP), "%}"),
followTagClose: ($) =>
seq(
optional($.unexpected_argument),
$._group_follow,
optional(SEP),
"%}",
),
repeatTagOpen: ($) => seq("{%", $._group_open_tag_read, optional(SEP)),
repeatTagClose: ($) => seq($._group_repeat, optional(SEP), "%}"),
repeatTagClose: ($) =>
seq(
optional($.unexpected_argument),
$._group_repeat,
optional(SEP),
"%}",
),
endTagOpen: ($) => seq("{%", $._group_open_tag_read, optional(SEP)),
endTagClose: ($) => seq($._group_close, optional(SEP), "%}"),
content: ($) => /(?:[^\{]|\{[^\{#%}])+/,
endTagClose: ($) =>
seq(optional($.unexpected_argument), $._group_close, optional(SEP), "%}"),
content: ($) => prec.right(repeat1(choice($._text, $._text_brace))),
/**
* Template text up to a "{{", "{%" or "{#". A "{" that is text only because
* of what comes after the next character, as in "a{{% if x %}" or a "{" at
* the end of input, is _text_brace from the scanner; see
* scan_text_brace_rest.
*/
_text: ($) => /(?:[^\{]|\{[^\{#%])+/,
// the separator between the parts of a tag; see SEP
_sep: ($) => /[ \t\r\n]+/,
template_node: ($) =>
@@ -636,13 +678,22 @@ const django = grammar({
),
),
),
/**
* A variable whose "}}" is missing closes with a zero-width marker instead,
* so that error recovery does not take the rest of the file with it. The
* scanner places it only where the variable cannot go on: at the end of
* input, or before a "}" that is not "}}", a "%" or a "{".
*/
template_variable: ($) =>
seq(
"{{",
optional(SEP),
alias($._filtered_value_spaced, $.filtered_value),
optional(SEP),
"}}",
choice(
"}}",
alias($._missing_variable_close, $.missing_variable_close),
),
),
/**
* Everything between "{{" and "}}" reaches Django as a single expression,
@@ -1165,30 +1216,84 @@ const django = grammar({
* second and a third "else", the later ones are the strays. Reading the
* first as the stray instead parses just as well, so _group_held, which the
* scanner returns only for a name the innermost group already holds, puts
* the later reading at -1 against -2 for each tag flagged before its time.
* the later reading at -2 against -4 for each tag flagged before its time. Both
* cost more than an unexpected_argument, at -1, so a tag its group does take
* but with a word it does not is read as that tag rather than as a stray.
*/
unexpected_tag: ($) =>
choice(
prec.dynamic(
-1,
seq(
$.simpleTagOpen,
field("tag_name", choice(...NAMES_INSIDE_A_TAG_GROUP)),
repeat(seq(SEP, /[^\s%]+/)),
$._group_held,
$.simpleTagClose,
),
),
prec.dynamic(
-2,
seq(
$.simpleTagOpen,
field("tag_name", choice(...NAMES_INSIDE_A_TAG_GROUP)),
repeat(seq(SEP, /[^\s%]+/)),
optional($.unexpected_argument),
$._group_held,
optional(SEP),
"%}",
),
),
prec.dynamic(
-4,
seq(
$.simpleTagOpen,
field("tag_name", choice(...NAMES_INSIDE_A_TAG_GROUP)),
$.simpleTagClose,
),
),
),
/**
* Words a tag does not take, kept inside the tag they were written in. Every
* close utility accepts them before its "%}", so a misused tag keeps its own
* node, "{% else x %}" is still the else of its group, and error recovery
* has nothing to repair. queries/errors.scm reports it.
*/
unexpected_argument: ($) => repeat1($._unexpected_word),
/**
* One word of an unexpected_argument, costing -1 of dynamic precedence of
* its own, so that a reading flagging more words than it must always loses:
* "{% cycle a b as x silent zqx %}" flags zqx, not everything after a.
*
* The word is a token of lexical precedence -1, so that a token an argument
* really takes wins wherever one is valid, even over a longer match:
* "b|upper" is still a filtered value. Where a keyword is valid, as after
* "{% for a in b" where "reversed" may follow, tree-sitter lexes every word
* as an identifier to check for the keyword, so an identifier is a word
* here too, followed by whatever touches it ("a=2", "x.y|upper"). It is
* aliased so that queries/locals.scm does not take it for a reference.
*
* A word never holds "}}": tree-sitter shares lexer states between parse
* states, so the token is tried inside {{ }} as well, and after "{{ x|" it
* would swallow the closing braces and leave recovery nothing to close with.
*/
_unexpected_word: ($) =>
prec.dynamic(
-1,
seq(
SEP,
choice(
token(prec(-1, /(?:[^\s%}]|\}[^\s%}])+/)),
seq(
alias($.identifier, "unexpected_word"),
optional(token.immediate(prec(-1, /(?:[^\s%}]|\}[^\s%}])+/))),
),
),
),
),
/**
* A "name=value" argument whose name a tag with a known signature does not
* take. The scanner's _kwarg_name reads any name followed by "=", which
* commits the parser to a keyword argument before the name is known, so
* the name the signature lacks is taken here rather than after the tag.
*/
_unexpected_keyword: ($) =>
prec.dynamic(
-1,
seq(
alias($.identifier, "unexpected_word"),
token.immediate(prec(-1, /(?:[^\s%}]|\}[^\s%}])+/)),
),
),
url_tag: ($) =>
tag(
"url",
+340
View File
@@ -7,16 +7,29 @@
"": {
"name": "tree-sitter-django",
"version": "0.1.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.3.1",
"node-gyp-build": "^4.8.4",
"web-tree-sitter": "^0.26.12"
},
"devDependencies": {
"eslint": "^9",
"eslint-config-prettier": "^10.1.8",
"prebuildify": "^6.0.1",
"prettier": "^3.9.6",
"tree-sitter": "^0.25.1",
"tree-sitter-cli": "^0.26.12",
"typescript": "^5"
},
"peerDependencies": {
"tree-sitter": "^0.25.0"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -313,6 +326,39 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/brace-expansion": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
@@ -324,6 +370,31 @@
"concat-map": "0.0.1"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -351,6 +422,13 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true,
"license": "ISC"
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -418,6 +496,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -673,6 +761,13 @@
"dev": true,
"license": "ISC"
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"license": "MIT"
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -709,6 +804,27 @@
"node": ">=8"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -746,6 +862,13 @@
"node": ">=0.8.19"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -880,6 +1003,23 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true,
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -894,6 +1034,62 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-abi": {
"version": "3.96.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",
"integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "8.9.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz",
"integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/npm-run-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz",
"integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -977,6 +1173,24 @@
"node": ">=8"
}
},
"node_modules/prebuildify": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/prebuildify/-/prebuildify-6.0.1.tgz",
"integrity": "sha512-8Y2oOOateom/s8dNBsGIcnm6AxPmLH4/nanQzL5lQMU+sC0CMhzARZHizwr36pUPLdvBnOkCNQzxg4djuFSgIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"mkdirp-classic": "^0.5.3",
"node-abi": "^3.3.0",
"npm-run-path": "^3.1.0",
"pump": "^3.0.0",
"tar-fs": "^2.1.0"
},
"bin": {
"prebuildify": "bin.js"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -1003,6 +1217,17 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -1013,6 +1238,21 @@
"node": ">=6"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -1023,6 +1263,40 @@
"node": ">=4"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -1046,6 +1320,16 @@
"node": ">=8"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -1072,6 +1356,48 @@
"node": ">=8"
}
},
"node_modules/tar-fs": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tree-sitter": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.25.1.tgz",
"integrity": "sha512-mrcEdkYtHfrK1A6fs3O6FxkBo0Qig5XUXqHhxUOQu0bmPo00QF4XaSx4edpazdHwxnSCjlGKGgIqWdaN4dvTLA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.5.0",
"node-gyp-build": "^4.8.4"
}
},
"node_modules/tree-sitter-cli": {
"version": "0.26.12",
"resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.26.12.tgz",
@@ -1123,6 +1449,13 @@
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/web-tree-sitter": {
"version": "0.26.12",
"resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.12.tgz",
@@ -1155,6 +1488,13 @@
"node": ">=0.10.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
Generated
+46 -3
View File
@@ -1,11 +1,15 @@
{
"name": "tree-sitter-django",
"version": "0.1.0",
"private": true,
"version": "0.1.2",
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./bindings/node/index.d.ts",
"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"
@@ -20,16 +24,55 @@
"parser-build": "tree-sitter build --wasm",
"highlight": "tree-sitter highlight",
"tag": "tree-sitter tag",
"playground": "tree-sitter playground"
"playground": "tree-sitter playground",
"install": "node-gyp-build",
"prebuildify": "prebuildify --napi --strip --tag-libc",
"binding-test": "node --test bindings/node/*_test.js"
},
"dependencies": {
"node-addon-api": "^8.3.1",
"node-gyp-build": "^4.8.4",
"web-tree-sitter": "^0.26.12"
},
"devDependencies": {
"eslint": "^9",
"eslint-config-prettier": "^10.1.8",
"prebuildify": "^6.0.1",
"prettier": "^3.9.6",
"tree-sitter": "^0.25.1",
"tree-sitter-cli": "^0.26.12",
"typescript": "^5"
},
"description": "Django grammar for tree-sitter",
"repository": {
"type": "git",
"url": "git+https://github.com/danhanson/tree-sitter-django.git"
},
"author": "Daniel Hanson (https://github.com/danhanson)",
"keywords": [
"incremental",
"parsing",
"tree-sitter",
"django"
],
"main": "bindings/node",
"types": "bindings/node",
"files": [
"grammar.js",
"tree-sitter.json",
"binding.gyp",
"prebuilds/**",
"bindings/node/*",
"queries/*",
"src/**",
"*.wasm"
],
"peerDependencies": {
"tree-sitter": "^0.25.0"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
}
+29
View File
@@ -0,0 +1,29 @@
[build-system]
requires = ["setuptools>=62.4.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "tree-sitter-django"
description = "Django grammar for tree-sitter"
version = "0.1.0"
keywords = ["incremental", "parsing", "tree-sitter", "django"]
classifiers = [
"Intended Audience :: Developers",
"Topic :: Software Development :: Compilers",
"Topic :: Text Processing :: Linguistic",
"Typing :: Typed",
]
authors = [{ name = "Daniel Hanson" }]
requires-python = ">=3.10"
license.text = "MIT"
readme = "README.md"
[project.urls]
Homepage = "https://github.com/danhanson/tree-sitter-django"
[project.optional-dependencies]
core = ["tree-sitter~=0.24"]
[tool.cibuildwheel]
build = "cp310-*"
build-frontend = "build"
+4
View File
@@ -41,3 +41,7 @@
(missing_plural_tag)] @error.missing_tag
(unexpected_tag) @error.unexpected_tag
(unexpected_argument) @error.unexpected_argument
(missing_variable_close) @error.missing_variable_close
Generated
+78
View File
@@ -0,0 +1,78 @@
from os import path
from sysconfig import get_config_var
from setuptools import Extension, find_packages, setup
from setuptools.command.build import build
from setuptools.command.build_ext import build_ext
from setuptools.command.egg_info import egg_info
from wheel.bdist_wheel import bdist_wheel
class Build(build):
def run(self):
if path.isdir("queries"):
dest = path.join(self.build_lib, "tree_sitter_django", "queries")
self.copy_tree("queries", dest)
super().run()
class BuildExt(build_ext):
def build_extension(self, ext: Extension):
if self.compiler.compiler_type != "msvc":
ext.extra_compile_args = ["-std=c11", "-fvisibility=hidden"]
else:
ext.extra_compile_args = ["/std:c11", "/utf-8"]
if path.exists("src/scanner.c"):
ext.sources.append("src/scanner.c")
if ext.py_limited_api:
ext.define_macros.append(("Py_LIMITED_API", "0x030A0000"))
super().build_extension(ext)
class BdistWheel(bdist_wheel):
def get_tag(self):
python, abi, platform = super().get_tag()
if python.startswith("cp") and not get_config_var("Py_GIL_DISABLED"):
python, abi = "cp310", "abi3"
return python, abi, platform
class EggInfo(egg_info):
def find_sources(self):
super().find_sources()
self.filelist.recursive_include("queries", "*.scm")
self.filelist.include("src/tree_sitter/*.h")
self.filelist.include("src/*.c")
setup(
packages=find_packages("bindings/python"),
package_dir={"": "bindings/python"},
package_data={
"tree_sitter_django": ["*.pyi", "py.typed"],
"tree_sitter_django.queries": ["*.scm"],
},
ext_package="tree_sitter_django",
ext_modules=[
Extension(
name="_binding",
sources=[
"bindings/python/tree_sitter_django/binding.c",
"src/parser.c",
],
define_macros=[
("PY_SSIZE_T_CLEAN", None),
("TREE_SITTER_HIDE_SYMBOLS", None),
],
include_dirs=["src"],
py_limited_api=not get_config_var("Py_GIL_DISABLED"),
)
],
cmdclass={
"build": Build,
"build_ext": BuildExt,
"bdist_wheel": BdistWheel,
"egg_info": EggInfo,
},
zip_safe=False
)
+383 -143
View File
@@ -47,6 +47,18 @@
"simpleTagClose": {
"type": "SEQ",
"members": [
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "unexpected_argument"
},
{
"type": "BLANK"
}
]
},
{
"type": "CHOICE",
"members": [
@@ -93,6 +105,18 @@
"startTagClose": {
"type": "SEQ",
"members": [
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "unexpected_argument"
},
{
"type": "BLANK"
}
]
},
{
"type": "SYMBOL",
"name": "_group_open_tag_push"
@@ -143,6 +167,18 @@
"followTagClose": {
"type": "SEQ",
"members": [
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "unexpected_argument"
},
{
"type": "BLANK"
}
]
},
{
"type": "SYMBOL",
"name": "_group_follow"
@@ -193,6 +229,18 @@
"repeatTagClose": {
"type": "SEQ",
"members": [
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "unexpected_argument"
},
{
"type": "BLANK"
}
]
},
{
"type": "SYMBOL",
"name": "_group_repeat"
@@ -243,6 +291,18 @@
"endTagClose": {
"type": "SEQ",
"members": [
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "unexpected_argument"
},
{
"type": "BLANK"
}
]
},
{
"type": "SYMBOL",
"name": "_group_close"
@@ -266,8 +326,28 @@
]
},
"content": {
"type": "PREC_RIGHT",
"value": 0,
"content": {
"type": "REPEAT1",
"content": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "_text"
},
{
"type": "SYMBOL",
"name": "_text_brace"
}
]
}
}
},
"_text": {
"type": "PATTERN",
"value": "(?:[^\\{]|\\{[^\\{#%}])+"
"value": "(?:[^\\{]|\\{[^\\{#%])+"
},
"_sep": {
"type": "PATTERN",
@@ -906,8 +986,22 @@
]
},
{
"type": "STRING",
"value": "}}"
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "}}"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_missing_variable_close"
},
"named": true,
"value": "missing_variable_close"
}
]
}
]
},
@@ -2833,6 +2927,33 @@
]
}
]
},
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_sep"
},
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_kwarg_name"
},
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "_unexpected_keyword"
},
"named": true,
"value": "unexpected_argument"
}
]
}
]
}
]
}
@@ -7062,140 +7183,6 @@
"unexpected_tag": {
"type": "CHOICE",
"members": [
{
"type": "PREC_DYNAMIC",
"value": -1,
"content": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "simpleTagOpen"
},
{
"type": "FIELD",
"name": "tag_name",
"content": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "elif"
},
{
"type": "STRING",
"value": "else"
},
{
"type": "STRING",
"value": "empty"
},
{
"type": "STRING",
"value": "endautoescape"
},
{
"type": "STRING",
"value": "endblock"
},
{
"type": "STRING",
"value": "endblocktrans"
},
{
"type": "STRING",
"value": "endblocktranslate"
},
{
"type": "STRING",
"value": "endcache"
},
{
"type": "STRING",
"value": "endcomment"
},
{
"type": "STRING",
"value": "endfilter"
},
{
"type": "STRING",
"value": "endfor"
},
{
"type": "STRING",
"value": "endif"
},
{
"type": "STRING",
"value": "endifchanged"
},
{
"type": "STRING",
"value": "endlanguage"
},
{
"type": "STRING",
"value": "endlocalize"
},
{
"type": "STRING",
"value": "endlocaltime"
},
{
"type": "STRING",
"value": "endpartialdef"
},
{
"type": "STRING",
"value": "endspaceless"
},
{
"type": "STRING",
"value": "endtimezone"
},
{
"type": "STRING",
"value": "endverbatim"
},
{
"type": "STRING",
"value": "endwith"
},
{
"type": "STRING",
"value": "plural"
}
]
}
},
{
"type": "REPEAT",
"content": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_sep"
},
{
"type": "PATTERN",
"value": "[^\\s%]+"
}
]
}
},
{
"type": "SYMBOL",
"name": "_group_held"
},
{
"type": "SYMBOL",
"name": "simpleTagClose"
}
]
}
},
{
"type": "PREC_DYNAMIC",
"value": -2,
@@ -7304,17 +7291,143 @@
}
},
{
"type": "REPEAT",
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "unexpected_argument"
},
{
"type": "BLANK"
}
]
},
{
"type": "SYMBOL",
"name": "_group_held"
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "_sep"
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": "%}"
}
]
}
},
{
"type": "PREC_DYNAMIC",
"value": -4,
"content": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "simpleTagOpen"
},
{
"type": "FIELD",
"name": "tag_name",
"content": {
"type": "SEQ",
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "_sep"
"type": "STRING",
"value": "elif"
},
{
"type": "PATTERN",
"value": "[^\\s%]+"
"type": "STRING",
"value": "else"
},
{
"type": "STRING",
"value": "empty"
},
{
"type": "STRING",
"value": "endautoescape"
},
{
"type": "STRING",
"value": "endblock"
},
{
"type": "STRING",
"value": "endblocktrans"
},
{
"type": "STRING",
"value": "endblocktranslate"
},
{
"type": "STRING",
"value": "endcache"
},
{
"type": "STRING",
"value": "endcomment"
},
{
"type": "STRING",
"value": "endfilter"
},
{
"type": "STRING",
"value": "endfor"
},
{
"type": "STRING",
"value": "endif"
},
{
"type": "STRING",
"value": "endifchanged"
},
{
"type": "STRING",
"value": "endlanguage"
},
{
"type": "STRING",
"value": "endlocalize"
},
{
"type": "STRING",
"value": "endlocaltime"
},
{
"type": "STRING",
"value": "endpartialdef"
},
{
"type": "STRING",
"value": "endspaceless"
},
{
"type": "STRING",
"value": "endtimezone"
},
{
"type": "STRING",
"value": "endverbatim"
},
{
"type": "STRING",
"value": "endwith"
},
{
"type": "STRING",
"value": "plural"
}
]
}
@@ -7328,6 +7441,104 @@
}
]
},
"unexpected_argument": {
"type": "REPEAT1",
"content": {
"type": "SYMBOL",
"name": "_unexpected_word"
}
},
"_unexpected_word": {
"type": "PREC_DYNAMIC",
"value": -1,
"content": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_sep"
},
{
"type": "CHOICE",
"members": [
{
"type": "TOKEN",
"content": {
"type": "PREC",
"value": -1,
"content": {
"type": "PATTERN",
"value": "(?:[^\\s%}]|\\}[^\\s%}])+"
}
}
},
{
"type": "SEQ",
"members": [
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "identifier"
},
"named": false,
"value": "unexpected_word"
},
{
"type": "CHOICE",
"members": [
{
"type": "IMMEDIATE_TOKEN",
"content": {
"type": "PREC",
"value": -1,
"content": {
"type": "PATTERN",
"value": "(?:[^\\s%}]|\\}[^\\s%}])+"
}
}
},
{
"type": "BLANK"
}
]
}
]
}
]
}
]
}
},
"_unexpected_keyword": {
"type": "PREC_DYNAMIC",
"value": -1,
"content": {
"type": "SEQ",
"members": [
{
"type": "ALIAS",
"content": {
"type": "SYMBOL",
"name": "identifier"
},
"named": false,
"value": "unexpected_word"
},
{
"type": "IMMEDIATE_TOKEN",
"content": {
"type": "PREC",
"value": -1,
"content": {
"type": "PATTERN",
"value": "(?:[^\\s%}]|\\}[^\\s%}])+"
}
}
}
]
}
},
"url_tag": {
"type": "SEQ",
"members": [
@@ -7821,6 +8032,27 @@
[
"binaryOperator"
],
[
"predicate"
],
[
"_translate_option"
],
[
"unexpected_argument"
],
[
"variable_attribute",
"_unexpected_word"
],
[
"resetcycle_tag",
"_unexpected_word"
],
[
"library",
"_unexpected_word"
],
[
"library",
"load_tag"
@@ -7909,6 +8141,14 @@
{
"type": "SYMBOL",
"name": "_group_held"
},
{
"type": "SYMBOL",
"name": "_text_brace"
},
{
"type": "SYMBOL",
"name": "_missing_variable_close"
}
],
"inline": [
+517 -25
View File
File diff suppressed because it is too large. Load diff
Generated
+52040 -32688
View File
File diff suppressed because it is too large. Load diff
+66 -3
View File
@@ -57,6 +57,11 @@ enum TokenType {
* the innermost group already holds a tag by that name, which makes this the
* likelier stray of the two */
GroupHeld,
/* a "{" that is text although another "{" follows it, because the second
* one opens a tag or a comment, or although nothing follows it at all */
TextBrace,
/* zero width, where a variable's "}}" is missing */
MissingVariableClose,
};
/* The options blocktranslate takes, in the order of the bits recording them.
@@ -336,11 +341,11 @@ static bool check_space(const int32_t character) {
}
static bool check_name_start_char(const int32_t letter) {
return 'a' <= letter && letter <= 'z' || 'A' <= letter && letter <= 'Z';
return ('a' <= letter && letter <= 'z') || ('A' <= letter && letter <= 'Z');
}
static bool check_name_char(const int32_t letter) {
return check_name_start_char(letter) || '0' <= letter && letter <= '9' || letter == '_';
return check_name_start_char(letter) || ('0' <= letter && letter <= '9') || letter == '_';
}
/* Reads one code point of the serialized state, or reports that none is left. */
@@ -773,8 +778,33 @@ typedef enum {
/* no marker, but input was read to decide that, so nothing else may be
* scanned from here */
NotMissingAfterReading,
/* no marker, but the "{" read to decide that was text, and was scanned */
TextBraceScanned,
} MissingResult;
/* Scans TextBrace once its "{" has been read. Django's lexer takes a template
* apart with ({%.*?%}|{{.*?}}|{#.*?#}), so a "{" is text unless one of those
* starts at it: before "{%" or "{#" the tag or comment starts one character
* later, and at the end of input nothing starts at all. The content regex
* cannot look that far ahead, so these are scanned here; "{{{" is not one of
* them, since "{{" does start a variable at its first brace. */
static bool scan_text_brace_rest(TSLexer *const lexer) {
lexer->mark_end(lexer);
if (lexer->eof(lexer)) {
lexer->result_symbol = TextBrace;
return true;
}
if (lexer->lookahead != '{') {
return false;
}
lexer->advance(lexer, false);
if (lexer->lookahead == '%' || lexer->lookahead == '#') {
lexer->result_symbol = TextBrace;
return true;
}
return false;
}
/* Scans the zero-width marker for the innermost group's missing tag. The
* marker is valid only where that group could close, which is after a body,
* where nothing else of this scanner is valid but the raw-text tokens, and
@@ -809,6 +839,10 @@ static MissingResult scan_missing_tag(
}
lexer->advance(lexer, false);
if (lexer->lookahead != '%') {
// not a tag, but the "{" may be text the grammar still has to take
if (valid_symbols[TextBrace] && scan_text_brace_rest(lexer)) {
return TextBraceScanned;
}
return NotMissingAfterReading;
}
lexer->advance(lexer, false);
@@ -924,6 +958,27 @@ static bool scan_raw_content(
}
}
/* Scans the marker for a variable whose "}}" is missing: at the end of input,
* or before a "}" that is not "}}", a "%" or a "{", none of which can continue
* the variable. Django would read such a "{{" as text, since its lexer only
* takes a variable that closes on the same line; the marker reports it as the
* unfinished variable it almost certainly is. */
static bool scan_missing_variable_close(TSLexer *const lexer) {
lexer->mark_end(lexer);
if (!lexer->eof(lexer)) {
if (lexer->lookahead == '}') {
lexer->advance(lexer, false);
if (lexer->lookahead == '}') {
return false;
}
} else if (lexer->lookahead != '%' && lexer->lookahead != '{') {
return false;
}
}
lexer->result_symbol = MissingVariableClose;
return true;
}
bool tree_sitter_django_external_scanner_scan(
void *payload,
TSLexer *lexer,
@@ -942,6 +997,9 @@ bool tree_sitter_django_external_scanner_scan(
if (valid_symbols[MatcherError]) {
return false;
}
if (valid_symbols[MissingVariableClose]) {
return scan_missing_variable_close(lexer);
}
/* The zero-width tokens below belong here: above the raw-text scanners,
* which find nothing at the end of an empty body, above the loop further
* down, which skips the separator the grammar still has to match, and below
@@ -950,6 +1008,7 @@ bool tree_sitter_django_external_scanner_scan(
if (valid_symbols[MissingTag]) {
switch (scan_missing_tag(scanner, lexer, valid_symbols)) {
case Missing:
case TextBraceScanned:
return true;
case NotMissingAfterReading:
return false;
@@ -957,6 +1016,10 @@ bool tree_sitter_django_external_scanner_scan(
break;
}
}
if (valid_symbols[TextBrace] && lexer->lookahead == '{') {
lexer->advance(lexer, false);
return scan_text_brace_rest(lexer);
}
if (valid_symbols[GroupOpenTagRead]) {
scan_group_open_tag_read(scanner, lexer);
return true;
@@ -1061,7 +1124,7 @@ bool tree_sitter_django_external_scanner_scan(
if (read_name(lexer, &name)) {
// validate tokens after name
lexer->mark_end(lexer);
if (check_close_tag(lexer) || token == PushPartial && check_inline(lexer)) {
if (check_close_tag(lexer) || (token == PushPartial && check_inline(lexer))) {
array_delete(&scanner->pending_name);
scanner->pending_name = name;
lexer->result_symbol = token;
+129 -10
View File
@@ -380,13 +380,29 @@ Blocktrans With Plural And No Count
==================
Blocktrans Counting Two Bindings
:error
==================
{% blocktrans count n=x c=d %}one{% plural %}many{% endblocktrans %}
---
(template
(content)
(blocktranslate_group
(blocktrans_block
(blocktrans_tag
(identifier)
(filtered_value
(value
(variable_attribute
(identifier))))
(unexpected_argument))
(content)
(plural_block
(plural_tag)
(content)))
(endblocktrans_tag))
(content))
==================
Blocktrans With Nothing To Bind
@@ -400,13 +416,21 @@ Blocktrans With Nothing To Bind
==================
Blocktrans With An Unknown Option
:error
==================
{% blocktrans bogus %}hi{% endblocktrans %}
---
(template
(content)
(blocktranslate_group
(blocktrans_block
(blocktrans_tag
(unexpected_argument))
(content))
(endblocktrans_tag))
(content))
==================
Blocktrans With Asvar And No Name
@@ -478,73 +502,155 @@ Blocktrans With Two Plurals
==================
Blocktrans With An Argument On Plural
:error
==================
{% blocktrans count n=x %}one{% plural extra %}many{% endblocktrans %}
---
(template
(content)
(blocktranslate_group
(blocktrans_block
(blocktrans_tag
(identifier)
(filtered_value
(value
(variable_attribute
(identifier)))))
(content)
(plural_block
(plural_tag
(unexpected_argument))
(content)))
(endblocktrans_tag))
(content))
==================
Blocktrans With Trimmed Twice
:error
==================
{% blocktrans trimmed trimmed %}hi{% endblocktrans %}
---
(template
(content)
(blocktranslate_group
(blocktrans_block
(blocktrans_tag
(unexpected_argument))
(content))
(endblocktrans_tag))
(content))
==================
Blocktrans With With Twice
:error
==================
{% blocktrans with a=b with c=d %}hi{% endblocktrans %}
---
(template
(content)
(blocktranslate_group
(blocktrans_block
(blocktrans_tag
(identifier)
(filtered_value
(value
(variable_attribute
(identifier))))
(unexpected_argument))
(content))
(endblocktrans_tag))
(content))
==================
Blocktrans With Context Twice
:error
==================
{% blocktrans context "a" context "b" %}hi{% endblocktrans %}
---
(template
(content)
(blocktranslate_group
(blocktrans_block
(blocktrans_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(content))
(endblocktrans_tag))
(content))
==================
Blocktrans With Asvar Twice
:error
==================
{% blocktrans asvar x asvar y %}hi{% endblocktrans %}
---
(template
(content)
(blocktranslate_group
(blocktrans_block
(blocktrans_tag
(identifier)
(unexpected_argument))
(content))
(endblocktrans_tag))
(content))
==================
Blocktrans With Count Twice
:error
==================
{% blocktrans count n=x count m=y %}a{% plural %}b{% endblocktrans %}
---
(template
(content)
(blocktranslate_group
(blocktrans_block
(blocktrans_tag
(identifier)
(filtered_value
(value
(variable_attribute
(identifier))))
(unexpected_argument))
(content)
(plural_block
(plural_tag)
(content)))
(endblocktrans_tag))
(content))
==================
Blocktranslate With Trimmed Twice
:error
==================
{% blocktranslate trimmed trimmed %}hi{% endblocktranslate %}
---
(template
(content)
(blocktranslate_group
(blocktranslate_block
(blocktranslate_tag
(unexpected_argument))
(content))
(endblocktranslate_tag))
(content))
==================
Blocktrans Repeating An Option Of The Tag Before It
@@ -570,10 +676,23 @@ Blocktrans Repeating An Option Of The Tag Before It
==================
Blocktrans With A Binding Twice
:error
==================
{% blocktrans with a=1 a=2 %}x{% endblocktrans %}
---
(template
(content)
(blocktranslate_group
(blocktrans_block
(blocktrans_tag
(identifier)
(filtered_value
(value
(literal
(number))))
(unexpected_argument))
(content))
(endblocktrans_tag))
(content))
+20 -1
View File
@@ -221,13 +221,31 @@ Cache Without A Fragment Name
==================
Cache With Anything After Using
:error
==================
{% cache 500 sidebar using="c" extra %}x{% endcache %}
---
(template
(content)
(cache_group
(cache_block
(cache_tag
(filtered_value
(value
(literal
(number))))
(identifier)
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(template
(content)))
(endcache_tag))
(content))
==================
Cache With An Empty Using
@@ -257,3 +275,4 @@ Cache With Using As Its Fragment Name
{% cache 500 using="c" %}x{% endcache %}
---
+33 -8
View File
@@ -48,43 +48,68 @@ Csp Nonce Attr With Media Keyword
==================
Csp Nonce Attr Takes One Argument
:error
==================
{% csp_nonce_attr "a" "b" %}
---
(ERROR)
(template
(content)
(csp_nonce_attr_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(content))
==================
Csp Nonce Attr Rejects Unknown Keyword
:error
==================
{% csp_nonce_attr other="b" %}
---
(ERROR)
(template
(content)
(csp_nonce_attr_tag
(unexpected_argument))
(content))
==================
Csp Nonce Attr Rejects Repeated Keyword
:error
==================
{% csp_nonce_attr media=1 media=2 %}
---
(ERROR)
(template
(content)
(csp_nonce_attr_tag
(filtered_value
(value
(literal
(number))))
(unexpected_argument))
(content))
==================
Csp Nonce Attr With Media Twice
:error
==================
{% csp_nonce_attr media="a" media="b" %}
---
(template
(content)
(csp_nonce_attr_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(content))
+11 -1
View File
@@ -294,13 +294,23 @@ A Name Reserved For A Tag Is Still An Argument
==================
Custom Tag With A Keyword Argument Twice
:error
==================
{% mytag a=1 a=2 %}
---
(template
(content)
(custom_tag
(identifier)
(identifier)
(filtered_value
(value
(literal
(number))))
(unexpected_argument))
(content))
==================
Custom Tag With Keyword Arguments And As
+243 -2
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
==================
@@ -486,13 +698,27 @@ For Reversed With A Tag In Its Body
==================
For With Reversed Twice
:error
==================
{% for x in y reversed reversed %}a{% endfor %}
---
(template
(content)
(for_group
(for_block
(for_tag
(identifier)
(filtered_value
(value
(variable_attribute
(identifier))))
(unexpected_argument))
(template
(content)))
(endfor_tag))
(content))
==================
For With Reversed Before In
@@ -503,6 +729,7 @@ For With Reversed Before In
---
==================
For Over A Variable Named Reversed
==================
@@ -587,10 +814,24 @@ For With Reversed As Its Whole Sequence
==================
For With Reversed Three Times
:error
==================
{% for x in reversed reversed reversed %}a{% endfor %}
---
(template
(content)
(for_group
(for_block
(for_tag
(identifier)
(filtered_value
(value
(variable_attribute
(identifier))))
(unexpected_argument))
(template
(content)))
(endfor_tag))
(content))
+33 -3
View File
@@ -180,13 +180,21 @@ Trans Without A Message
==================
Trans With An Option Twice
:error
==================
{% trans "hi" noop noop %}
---
(template
(content)
(translate_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(content))
==================
Trans With As Twice
@@ -200,13 +208,21 @@ Trans With As Twice
==================
Trans With An Unknown Option
:error
==================
{% trans "hi" bogus %}
---
(template
(content)
(translate_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(content))
==================
Trans With Context And No Value
@@ -400,13 +416,26 @@ Language Without An Argument
==================
Language With Two Arguments
:error
==================
{% language "a" "b" %}x{% endlanguage %}
---
(template
(content)
(language_group
(language_block
(language_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(template
(content)))
(endlanguage_tag))
(content))
==================
Language Name Filter
@@ -493,6 +522,7 @@ Language Name Filter With An Argument
---
==================
Translate With A Context Named After Noop
:error
+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
==================
+5 -2
View File
@@ -219,8 +219,11 @@ b
(ifchanged_block
(ifchanged_tag)
(template
(content)
(unexpected_tag)
(content)))
(else_block
(else_tag
(unexpected_argument))
(template
(content)))
(endifchanged_tag))
(content))
+56 -5
View File
@@ -163,13 +163,26 @@ Include With Only Before With
==================
Include With A Binding Twice
:error
==================
{% include "t.html" with a=1 a=2 %}
---
(template
(content)
(include_tag
(filtered_value
(value
(literal
(string))))
(identifier)
(filtered_value
(value
(literal
(number))))
(unexpected_argument))
(content))
==================
Include Without A Name
@@ -193,33 +206,62 @@ Include With Nothing To Bind
==================
Include With With Twice
:error
==================
{% include "t.html" with a=1 with b=2 %}
---
(template
(content)
(include_tag
(filtered_value
(value
(literal
(string))))
(identifier)
(filtered_value
(value
(literal
(number))))
(unexpected_argument))
(content))
==================
Include With Only Twice
:error
==================
{% include "t.html" only only %}
---
(template
(content)
(include_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(content))
==================
Include With An Unknown Option
:error
==================
{% include "t.html" bogus %}
---
(template
(content)
(include_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(content))
==================
Include With A Legacy Binding
@@ -233,9 +275,18 @@ Include With A Legacy Binding
==================
Include With A Second Name
:error
==================
{% include "t.html" "u.html" %}
---
(template
(content)
(include_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(content))
+21 -3
View File
@@ -95,23 +95,41 @@ Localize Nested
==================
Localize With Another Argument
:error
==================
{% localize maybe %}x{% endlocalize %}
---
(template
(content)
(localize_group
(localize_block
(localize_tag
(unexpected_argument))
(template
(content)))
(endlocalize_tag))
(content))
==================
Localize With Two Arguments
:error
==================
{% localize on off %}x{% endlocalize %}
---
(template
(content)
(localize_group
(localize_block
(localize_tag
(unexpected_argument))
(template
(content)))
(endlocalize_tag))
(content))
==================
Localize Unclosed
@@ -130,7 +148,6 @@ Localize Unclosed
(content)))
(missing_endlocalize_tag)))
==================
Localize Filter
==================
@@ -187,3 +204,4 @@ Unlocalize Filter With An Argument
{{ x|unlocalize:1 }}
---
+2 -1
View File
@@ -950,7 +950,8 @@ Endblock Naming No Open Block
(block_tag
(push_block))
(template
(unexpected_tag)
(unexpected_tag
(unexpected_argument))
(content)))
(missing_endblock_tag))))
(missing_endblock_tag)))
+26 -2
View File
@@ -100,19 +100,43 @@ Querystring With Two Keys
==================
Querystring With A Key Twice
:error
==================
{% querystring page=2 page=3 %}
---
(template
(content)
(querystring_tag
(identifier)
(filtered_value
(value
(literal
(number))))
(unexpected_argument))
(content))
==================
Querystring With A Key Repeated Later
:error
==================
{% querystring a=1 b=2 a=3 %}
---
(template
(content)
(querystring_tag
(identifier)
(filtered_value
(value
(literal
(number))))
(identifier)
(filtered_value
(value
(literal
(number))))
(unexpected_argument))
(content))
+24 -3
View File
@@ -81,23 +81,40 @@ Static Without A Path
==================
Static With A Second Path
:error
==================
{% static "a.css" "b.css" %}
---
(template
(content)
(static_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(content))
==================
Static With Anything After As
:error
==================
{% static "a.css" as url junk %}
---
(template
(content)
(static_tag
(filtered_value
(value
(literal
(string))))
(identifier)
(unexpected_argument))
(content))
==================
Get Static Prefix
@@ -142,13 +159,17 @@ Get Media Prefix With As
==================
Get Static Prefix Without As
:error
==================
{% get_static_prefix prefix %}
---
(template
(content)
(get_static_prefix_tag
(unexpected_argument))
(content))
==================
Get Static Prefix Without A Variable
+187
View File
@@ -498,3 +498,190 @@ Template Variable Signed Trailing Dot
---
==================
Braces That Open Nothing Are Text
==================
{% if x %}Hello {} world{% endif %}
---
(template
(content)
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(endif_tag))
(content))
==================
A Brace Before A Tag Is Text
==================
Hello {{% if x %}a{% endif %}
---
(template
(content)
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(endif_tag))
(content))
==================
A Brace Before A Comment Is Text
==================
a{{# c #}
---
(template
(content)
(template_comment)
(content))
==================
A Brace At The End Is Text
==================
a{
---
(template
(content))
==================
A Brace Before The End Tag Of A Group Is Text
==================
{% if x %}Hello {{% endif %}
---
(template
(content)
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(endif_tag))
(content))
==================
Three Braces Open A Variable
:error
==================
a{{{ x }}
---
==================
A Filter Pipe With No Filter Stays In Its Variable
==================
{{ oops| }}
{% if x == 1 %}
hello
{% endif %}
---
(template
(content)
(template_variable
(filtered_value
(value
(variable_attribute
(identifier))))
(ERROR))
(content)
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))
(binaryOperator)
(filtered_value
(value
(literal
(number))))))
(template
(content)))
(endif_tag))
(content))
==================
A Variable Missing Its Close Ends Where It Cannot Continue
==================
{{ hello }{% if display %}body {% endif %}
---
(template
(content)
(template_variable
(filtered_value
(value
(variable_attribute
(identifier))))
(missing_variable_close))
(content)
(if_group
(if_block
(if_tag
(predicate
(filtered_value
(value
(variable_attribute
(identifier))))))
(template
(content)))
(endif_tag))
(content))
==================
A Variable Missing Its Close At The End
==================
{{ hello
---
(template
(content)
(template_variable
(filtered_value
(value
(variable_attribute
(identifier))))
(missing_variable_close)))
+41 -4
View File
@@ -54,23 +54,41 @@ Localtime Off
==================
Localtime With Another Argument
:error
==================
{% localtime maybe %}x{% endlocaltime %}
---
(template
(content)
(localtime_group
(localtime_block
(localtime_tag
(unexpected_argument))
(template
(content)))
(endlocaltime_tag))
(content))
==================
Localtime With Two Arguments
:error
==================
{% localtime on off %}x{% endlocaltime %}
---
(template
(content)
(localtime_group
(localtime_block
(localtime_tag
(unexpected_argument))
(template
(content)))
(endlocaltime_tag))
(content))
==================
Timezone
@@ -152,13 +170,26 @@ Timezone Without An Argument
==================
Timezone With Two Arguments
:error
==================
{% timezone "a" "b" %}x{% endtimezone %}
---
(template
(content)
(timezone_group
(timezone_block
(timezone_tag
(filtered_value
(value
(literal
(string))))
(unexpected_argument))
(template
(content)))
(endtimezone_tag))
(content))
==================
Get Current Timezone
@@ -186,13 +217,18 @@ Get Current Timezone Without As
==================
Get Current Timezone With Anything After As
:error
==================
{% get_current_timezone as tz junk %}
---
(template
(content)
(get_current_timezone_tag
(identifier)
(unexpected_argument))
(content))
==================
Localtime Filter
@@ -272,3 +308,4 @@ Localtime Filter With An Argument
{{ x|localtime:1 }}
---
+60 -2
View File
@@ -66,25 +66,83 @@ 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
:error
==================
{% with a=1 a=2 %}x{% endwith %}
---
(template
(content)
(with_group
(with_block
(with_tag
(identifier)
(filtered_value
(value
(literal
(number))))
(unexpected_argument))
(template
(content)))
(endwith_tag))
(content))
==================
With A Binding Repeated Later
:error
==================
{% with a=1 b=2 a=3 %}x{% endwith %}
---
(template
(content)
(with_group
(with_block
(with_tag
(identifier)
(filtered_value
(value
(literal
(number))))
(identifier)
(filtered_value
(value
(literal
(number))))
(unexpected_argument))
(template
(content)))
(endwith_tag))
(content))
==================
With Repeating A Binding Of The Tag Before It
+20 -9
View File
@@ -6,21 +6,22 @@
"camelcase": "django",
"title": "Django Parser",
"scope": "source.django",
"file-types": ["html", "txt", "md"],
"injection-regex": "^django$",
"class-name": "TreeSitterDjango",
"file-types": [
"html",
"txt",
"md"
],
"highlights": "queries/highlights.scm",
"injections": "queries/injections.scm",
"locals": "queries/locals.scm",
"injections": "queries/injections.scm"
"injection-regex": "^django$",
"class-name": "TreeSitterDjango"
}
],
"bindings": {
"node": true
},
"metadata": {
"version": "0.0.1",
"description": "DJango grammar for tree-sitter",
"version": "0.1.2",
"license": "MIT",
"description": "Django grammar for tree-sitter",
"authors": [
{
"name": "Daniel Hanson",
@@ -30,5 +31,15 @@
"links": {
"repository": "https://github.com/danhanson/tree-sitter-django"
}
},
"bindings": {
"c": true,
"go": true,
"java": false,
"node": true,
"python": true,
"rust": true,
"swift": true,
"zig": false
}
}