@marko/tree-sitter 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JS Foundation and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # @marko/tree-sitter
2
+
3
+ A [tree-sitter](https://tree-sitter.github.io/) grammar for the
4
+ [Marko](https://markojs.com/) templating language.
5
+
6
+ It covers the full language: both authoring modes (HTML and
7
+ concise/indentation-based), `${placeholders}`, tag variables, arguments and
8
+ parameters, shorthand `#id`/`.class` and method attributes, attribute
9
+ groups, statement tags, raw-text tags, `--` content blocks, and `$`
10
+ scriptlets, all with the exact source ranges the Marko compiler sees, so the
11
+ tree is reliable for highlighting, folding, structural editing, and tooling.
12
+
13
+ ## Usage
14
+
15
+ With the node bindings:
16
+
17
+ ```js
18
+ const Parser = require("tree-sitter");
19
+ const Marko = require("@marko/tree-sitter");
20
+
21
+ const parser = new Parser();
22
+ parser.setLanguage(Marko);
23
+
24
+ const tree = parser.parse("<button onClick() { count++ }>${count}</button>");
25
+ console.log(tree.rootNode.toString());
26
+ ```
27
+
28
+ In the browser (or anywhere native bindings are unwanted), use the wasm
29
+ build with [web-tree-sitter](https://github.com/tree-sitter/tree-sitter/tree/master/lib/binding_web):
30
+
31
+ ```js
32
+ import { Language, Parser } from "web-tree-sitter";
33
+
34
+ await Parser.init();
35
+ const Marko = await Language.load("tree-sitter-marko.wasm");
36
+ const parser = new Parser();
37
+ parser.setLanguage(Marko);
38
+ ```
39
+
40
+ Building from a checkout:
41
+
42
+ ```sh
43
+ npm install
44
+ npm run build # tree-sitter generate && node-gyp rebuild
45
+ npm run build:wasm # tree-sitter-marko.wasm (the CLI fetches wasi-sdk itself)
46
+ ```
47
+
48
+ For editors, the grammar ships ready-to-use queries: `queries/highlights.scm`
49
+ for syntax highlighting captures and `queries/injections.scm` for
50
+ embedded-language injections (described below).
51
+
52
+ ## Editors and tools
53
+
54
+ Every integration consumes the same artifacts: the generated parser
55
+ (`src/parser.c` + `src/scanner.c`), the queries in `queries/`, and, for
56
+ embedded highlighting, the `typescript`, `css`, and `scss` grammars installed
57
+ in the host tool like any other language.
58
+
59
+ ### tree-sitter CLI
60
+
61
+ From this directory:
62
+
63
+ ```sh
64
+ npx tree-sitter playground # interactive tree in the browser
65
+ npx tree-sitter parse file.marko # print the syntax tree
66
+ npx tree-sitter highlight file.marko # ANSI highlighting (--html for a page)
67
+ ```
68
+
69
+ `highlight` resolves the injected languages through the CLI config: run
70
+ `tree-sitter init-config`, then make sure one of the `parser-directories`
71
+ in `~/.config/tree-sitter/config.json` holds clones of
72
+ `tree-sitter-typescript` (run `npm install --ignore-scripts` inside it, as
73
+ its queries reference its `tree-sitter-javascript` dependency),
74
+ `tree-sitter-css`, and `tree-sitter-scss`. Note the loader only discovers
75
+ grammars that have a `tree-sitter.json`.
76
+
77
+ ### Neovim
78
+
79
+ Neovim (0.10+) needs no plugins. Compile the parser onto the runtime path
80
+ and start treesitter for the filetype:
81
+
82
+ ```sh
83
+ # from this directory
84
+ mkdir -p ~/.local/share/nvim/site/parser ~/.local/share/nvim/site/queries
85
+ npx tree-sitter build -o ~/.local/share/nvim/site/parser/marko.so
86
+ cp -R queries ~/.local/share/nvim/site/queries/marko
87
+ ```
88
+
89
+ ```lua
90
+ -- init.lua
91
+ vim.filetype.add({ extension = { marko = "marko" } })
92
+ vim.api.nvim_create_autocmd("FileType", {
93
+ pattern = "marko",
94
+ callback = function()
95
+ vim.treesitter.start()
96
+ end,
97
+ })
98
+ ```
99
+
100
+ Embedded highlighting uses whatever parsers are installed; with
101
+ nvim-treesitter that's `:TSInstall typescript css scss`.
102
+
103
+ ### Helix
104
+
105
+ Register the grammar and language in `~/.config/helix/languages.toml`:
106
+
107
+ ```toml
108
+ [[language]]
109
+ name = "marko"
110
+ scope = "source.marko"
111
+ file-types = ["marko"]
112
+ comment-token = "//"
113
+ block-comment-tokens = { start = "<!--", end = "-->" }
114
+ injection-regex = "^marko$"
115
+
116
+ [[grammar]]
117
+ name = "marko"
118
+ source = { git = "https://github.com/marko-js/tree-sitter", rev = "<commit>" }
119
+ ```
120
+
121
+ (or `source = { path = "/path/to/tree-sitter" }` for a local
122
+ checkout), then:
123
+
124
+ ```sh
125
+ hx --grammar fetch && hx --grammar build
126
+ ```
127
+
128
+ and copy the queries to `~/.config/helix/runtime/queries/marko/`.
129
+
130
+ ### Zed
131
+
132
+ Marko support in Zed is provided by the
133
+ [marko-js/zed](https://github.com/marko-js/zed) extension, which bundles this
134
+ grammar (via its `[grammars.marko]` entry pointing here), the language config,
135
+ the editor queries, and the
136
+ [Marko language server](https://github.com/marko-js/language-server). Install
137
+ it from Zed's extension registry, or run `zed: install dev extension` on a
138
+ local checkout of that repo. Its `highlights.scm`/`injections.scm` are copies
139
+ of the `queries/` here, used as-is: Zed evaluates the
140
+ `#eq?`/`#any-of?`/`#not-any-of?` predicates, `injection.combined`, and the
141
+ dynamic `@injection.language` dialects. The Zed-specific `brackets.scm` and
142
+ `outline.scm` are maintained in that extension repo.
143
+
144
+ ### VS Code
145
+
146
+ VS Code does not consume tree-sitter grammars. Marko support there remains
147
+ the tmLanguage-based
148
+ [Marko VS Code extension](https://github.com/marko-js/language-server),
149
+ which this grammar's queries are designed to match visually.
150
+
151
+ ## The tree
152
+
153
+ ```marko
154
+ div.panel
155
+ <button onClick() { count++ }>${count}</button>
156
+ ```
157
+
158
+ ```
159
+ (document
160
+ (element
161
+ (tag_name (tag_name_fragment))
162
+ (shorthand_class (tag_name_fragment))
163
+ (concise_open_tag_end)
164
+ (element
165
+ (open_tag_start)
166
+ (tag_name (tag_name_fragment))
167
+ (attr_name)
168
+ (args)
169
+ (method_body (method_body_expr))
170
+ (open_tag_end)
171
+ (placeholder (placeholder_start) (placeholder_expr))
172
+ (close_tag (close_tag_start) (close_tag_name) (close_tag_end))
173
+ (element_end))
174
+ (element_end)))
175
+ ```
176
+
177
+ Commonly queried nodes:
178
+
179
+ | node | meaning |
180
+ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------ |
181
+ | `element` | a tag in either mode, including its body and close |
182
+ | `tag_name`, `tag_name_fragment` | tag names; interpolated names contain `placeholder` children |
183
+ | `shorthand_id`, `shorthand_class` | `#id` / `.class` shorthands |
184
+ | `attr_name`, `attr_value`, `attr_bound_value`, `attr_spread` | attributes (`x=…`, `x:=…`, `...spread`) |
185
+ | `args`, `method_body` | `(…)` arguments and `{ … }` shorthand-method bodies |
186
+ | `tag_var` → `var_pattern`, `var_type` | tag variables: `/pattern` with an optional `: type` |
187
+ | `params` → `param` → `param_pattern`, `param_type`, `param_default` | `\|a: T = 1, b\|` tag parameters |
188
+ | `type_args`, `type_params` → `type_expr` | `<T>` type arguments/parameters |
189
+ | `placeholder`, `placeholder_expr` | `${…}` and `$!{…}` |
190
+ | `scriptlet`, `scriptlet_expr`, `scriptlet_block_expr` | `$ statement` / `$ { block }` |
191
+ | `statement_expr` | the body of statement tags (`import`, `static`, `server`, …) |
192
+ | `text`, `html_comment`, `line_comment`, `block_comment`, `cdata`, `doctype`, `declaration` | content |
193
+ | `html_block` | `--` delimited / single-line content blocks in concise mode |
194
+
195
+ A few structural notes:
196
+
197
+ - `element_end` is a zero-width node marking where an element closes
198
+ implicitly (concise dedent, void tags, self-closing, end of input).
199
+ `concise_open_tag_end` is similarly zero-width (or covers a trailing `;`).
200
+ - Delimiters are visible named tokens (`args_open`/`args_close`,
201
+ `params_open`/`params_close`, `type_open`/`type_close`,
202
+ `attr_group_open`/`attr_group_close`, `method_body_open`/
203
+ `method_body_close`, `scriptlet_block_open`/`scriptlet_block_close`,
204
+ `placeholder_start`/`placeholder_end`), so bracket-matching and
205
+ punctuation queries can target them.
206
+ - Whitespace is significant in Marko, so the grammar has no `extras`; text
207
+ nodes contain their exact whitespace.
208
+ - Invalid input produces an `ERROR` node from the first error onward,
209
+ matching how Marko itself reports a single error per template rather than
210
+ guessing at recovery.
211
+
212
+ ## Tag types
213
+
214
+ Tag parsing in Marko depends on the tag. This grammar follows the
215
+ [tags API](https://markojs.com/docs/reference/core-tag)'s `parseOptions` (and
216
+ the vscode tmLanguage grammar it replaces), which sorts tags into three
217
+ groups.
218
+
219
+ Void tags take no children and no closing tag: the html void elements plus
220
+ `const`, `debug`, `id`, `let`, `lifecycle`, `log`, and `return`.
221
+
222
+ Raw-text tags (`script`, `style`, `html-script`, `html-style`,
223
+ `html-comment`) have bodies of text and placeholders only.
224
+
225
+ Statement tags (`import`, `export`, `static`, `server`, `client`, plus
226
+ `class` for class-API compatibility) treat the rest of the line, and any
227
+ indented continuation, as a single embedded statement.
228
+
229
+ ## Embedded languages
230
+
231
+ Marko templates embed TypeScript and CSS throughout, and
232
+ `queries/injections.scm` maps it all. TypeScript is injected into every
233
+ expression position: placeholders, attribute values/spreads, arguments,
234
+ shorthand-method bodies, scriptlets, statement-tag bodies, and the
235
+ pattern/default parts of tag variables and parameters.
236
+
237
+ Statement tags split into two groups, mirroring how the compiler consumes
238
+ them. `import`, `export`, and `class` are pass-through TypeScript, where the
239
+ keyword is part of the statement, so the whole element (keyword included) is
240
+ injected and the keyword takes its color from the TS grammar. For `static`,
241
+ `server`, and `client` the keyword is Marko syntax that the compiler strips,
242
+ so only the body is injected and `highlights.scm` captures the keyword as
243
+ `@keyword` (the tmLanguage grammar makes the same split). `<script>`/
244
+ `<html-script>` bodies inject TypeScript and `<style>`/`<html-style>` bodies
245
+ inject CSS, including their concise `script --` / `style --` block forms,
246
+ with `injection.combined` merging chunks split by placeholders. `style`
247
+ dialect shorthands are honored the way the compiler resolves them: the last
248
+ shorthand segment names the injected language, so `<style.scss>` and
249
+ `style.module.scss --` inject `scss` while plain `<style>` falls back to
250
+ `css`. `html-style` is exempt, since its shorthand is a real class attribute,
251
+ not a dialect.
252
+
253
+ Type annotations (tag var/param types, `<T>` type args/params) are captured
254
+ as `@type` instead of injected: a bare type is not a valid TypeScript
255
+ program, so flat coloring is the accurate option (the tmLanguage grammar
256
+ makes the same approximation). One residual case: the parameters of a
257
+ shorthand method (`onClick(event) { … }`) inject as a whole TS program,
258
+ which renders typed parameters slightly off, since that position cannot be
259
+ split without lookahead the scanner doesn't have.
260
+
261
+ ## Fidelity and development
262
+
263
+ Marko's parser is [htmljs-parser](https://github.com/marko-js/htmljs-parser);
264
+ this grammar's external scanner reimplements its state machine, and the test
265
+ suite (`npm test`, Node 22) asserts the tree reproduces the parser's event
266
+ stream with byte-precise ranges across the parser's full fixture suite, plus
267
+ thousands of templates from the Marko ecosystem during development.
268
+
269
+ The reference parser and its fixtures are always the same htmljs-parser
270
+ revision: they are fetched together into `.cache/` with degit, so the suite
271
+ compares the grammar against the exact parser the fixtures came from (set
272
+ `HTMLJS_FIXTURES` to point at a local checkout's fixtures instead, and the
273
+ adjacent sources are used as the parser; if this directory is dropped inside
274
+ an htmljs-parser checkout, the local parser sources are used automatically).
275
+ The published `htmljs-parser` package is only a last-resort fallback. The
276
+ grammar tracks the parser, so the suite is only expected to be green against
277
+ the htmljs-parser revision the grammar was synced with.
278
+
279
+ See `__tests__/` and `tools/` to work on the grammar itself; the scanner
280
+ internals are documented at the top of `src/scanner.c`.
package/binding.gyp ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "tree_sitter_marko_binding",
5
+ "dependencies": [
6
+ "<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except"
7
+ ],
8
+ "include_dirs": ["src"],
9
+ "sources": ["bindings/node/binding.cc", "src/parser.c", "src/scanner.c"],
10
+ "conditions": [
11
+ ["OS!='win'", {
12
+ "cflags_c": ["-std=c11"]
13
+ }, {
14
+ "cflags_c": ["/std:c11", "/utf-8"]
15
+ }]
16
+ ]
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,22 @@
1
+ #include <napi.h>
2
+
3
+ typedef struct TSLanguage TSLanguage;
4
+
5
+ extern "C" TSLanguage *tree_sitter_marko();
6
+
7
+ namespace {
8
+
9
+ // "tree-sitter", "language" hashed with BLAKE2
10
+ const napi_type_tag LANGUAGE_TYPE_TAG = {0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16};
11
+
12
+ Napi::Object Init(Napi::Env env, Napi::Object exports) {
13
+ exports["name"] = Napi::String::New(env, "marko");
14
+ auto language = Napi::External<TSLanguage>::New(env, tree_sitter_marko());
15
+ language.TypeTag(&LANGUAGE_TYPE_TAG);
16
+ exports["language"] = language;
17
+ return exports;
18
+ }
19
+
20
+ NODE_API_MODULE(tree_sitter_marko_binding, Init)
21
+
22
+ } // namespace
@@ -0,0 +1,28 @@
1
+ type BaseNode = {
2
+ type: string;
3
+ named: boolean;
4
+ };
5
+
6
+ type ChildNode = {
7
+ multiple: boolean;
8
+ required: boolean;
9
+ types: BaseNode[];
10
+ };
11
+
12
+ type NodeInfo =
13
+ | (BaseNode & {
14
+ subtypes: BaseNode[];
15
+ })
16
+ | (BaseNode & {
17
+ fields: { [name: string]: ChildNode };
18
+ children: ChildNode[];
19
+ });
20
+
21
+ type Language = {
22
+ name: string;
23
+ language: unknown;
24
+ nodeTypeInfo: NodeInfo[];
25
+ };
26
+
27
+ declare const language: Language;
28
+ export = language;
@@ -0,0 +1,13 @@
1
+ const root = require("path").join(__dirname, "..", "..");
2
+
3
+ module.exports =
4
+ typeof process.versions.bun === "string"
5
+ ? // Support `bun build --compile` by being statically analyzable enough to find the .node binding during build-time
6
+ require(
7
+ `../../prebuilds/${process.platform}-${process.arch}/@marko+tree-sitter.node`,
8
+ )
9
+ : require("node-gyp-build")(root);
10
+
11
+ try {
12
+ module.exports.nodeTypeInfo = require("../../src/node-types.json");
13
+ } catch (_) {}
package/grammar.js ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Tree-sitter grammar for Marko templates.
3
+ *
4
+ * This grammar is a faithful port of htmljs-parser (../src). Because nearly
5
+ * all of Marko's syntax is context sensitive (concise mode indentation,
6
+ * expression terminators that depend on the surrounding construct, operator
7
+ * continuation across whitespace, parsed-text tags like <script>, etc.) every
8
+ * terminal is produced by the external scanner in src/scanner.c, which ports
9
+ * the parser's state machine. The rules below only describe how those tokens
10
+ * compose, mirroring the event stream emitted by createParser().
11
+ */
12
+
13
+ /// <reference types="tree-sitter-cli/dsl" />
14
+ // @ts-check
15
+
16
+ module.exports = grammar({
17
+ name: "marko",
18
+
19
+ // Whitespace is significant almost everywhere (text content, expression
20
+ // termination, concise indentation), so there are no extras; the external
21
+ // scanner skips insignificant trivia (whitespace/commas/comments inside
22
+ // open tags) itself, exactly where htmljs-parser does.
23
+ extras: () => [],
24
+
25
+ externals: ($) => [
26
+ $.text,
27
+ $.placeholder_start, // "${" (escape=true placeholders)
28
+ $.placeholder_start_raw, // "$!{"
29
+ $._interp_start, // "${" within tag names / shorthands
30
+ $.placeholder_expr,
31
+ $._placeholder_end, // "}"
32
+ $.html_comment, // "<!--" ... "->"
33
+ $.line_comment, // "//" ...
34
+ $.block_comment, // "/*" ... "*/"
35
+ $.cdata, // "<![CDATA[" ... "]]>"
36
+ $.doctype, // "<!" ... ">"
37
+ $.declaration, // "<?" ... ">" | "?>"
38
+ $.scriptlet_start, // "$" at the start of a line in html mode
39
+ $.scriptlet_start_concise, // "$" at the start of a concise line
40
+ $._scriptlet_block_open, // "{"
41
+ $.scriptlet_block_expr,
42
+ $._scriptlet_block_close, // "}" plus an optional trailing ";"
43
+ $.scriptlet_expr, // statement after "$ "
44
+ $._block_open, // "--", "---", ... starting a delimited html block
45
+ $._block_close, // matching delimiter line, or zero-width
46
+ $.open_tag_start, // "<"
47
+ $.tag_name_fragment, // run of tag name characters between placeholders
48
+ $._tag_name_empty, // zero-width: shorthand-only tags like <.foo>
49
+ $._shorthand_id_start, // "#"
50
+ $._shorthand_class_start, // "."
51
+ $.statement_expr, // body of statement tags (import/export/static/class)
52
+ $._tag_var_start, // "/"
53
+ $.var_pattern, // tag variable binding pattern (before any ": type")
54
+ $._var_colon, // ":" between a tag var pattern and its type
55
+ $.var_type, // tag variable type annotation
56
+ $._args_open, // "("
57
+ $.args_expr,
58
+ $._args_close, // ")"
59
+ $._params_open, // "|"
60
+ $.param_pattern, // parameter binding pattern
61
+ $._param_colon, // ":" before a parameter type
62
+ $.param_type, // parameter type annotation
63
+ $._param_eq, // "=" before a parameter default
64
+ $.param_default, // parameter default value expression
65
+ $._param_comma, // "," between parameters
66
+ $._params_close, // "|"
67
+ $._type_args_open, // "<" directly after the tag name
68
+ $._type_params_open, // "<" before tag params or a method shorthand
69
+ $.type_expr,
70
+ $._types_close, // ">"
71
+ $._attr_group_open, // "[" (concise mode)
72
+ $._attr_group_close, // "]"
73
+ $.attr_name,
74
+ $.attr_eq, // "="
75
+ $.attr_bound_eq, // ":="
76
+ $._attr_spread_start, // "..."
77
+ $.attr_value_expr,
78
+ $._method_body_open, // "{"
79
+ $.method_body_expr,
80
+ $._method_body_close, // "}"
81
+ $.open_tag_end, // ">"
82
+ $.open_tag_end_self, // "/>"
83
+ $.concise_open_tag_end, // zero-width (EOL/EOF/"--") or ";"
84
+ $.close_tag_start, // "</"
85
+ $.close_tag_name, // may be zero-width for "</>"
86
+ $.close_tag_end, // ">"
87
+ $.element_end, // zero-width: dedent, void, self-closed, EOF, after </x>
88
+ $._tag_comment, // "//..." or "/*...*/" inside an open tag (no events)
89
+ $.escape, // backslash run before an escaped/double-escaped "${"
90
+ $._error_sentinel, // never emitted; detects error recovery
91
+ ],
92
+
93
+ rules: {
94
+ document: ($) => repeat($._child),
95
+
96
+ _child: ($) =>
97
+ choice(
98
+ $.element,
99
+ $.text,
100
+ $.placeholder,
101
+ $.html_comment,
102
+ $.line_comment,
103
+ $.block_comment,
104
+ $.cdata,
105
+ $.doctype,
106
+ $.declaration,
107
+ $.scriptlet,
108
+ $.html_block,
109
+ $.escape,
110
+ ),
111
+
112
+ // Covers html mode (<a>...</a>, <br>, <input/>), concise mode (indent
113
+ // based, ended by a zero-width element_end at the dedent), statement tags
114
+ // (import/export/static/class) and parsed-text tags (script/style/
115
+ // textarea/html-comment whose bodies only contain text & placeholders).
116
+ element: ($) =>
117
+ seq(
118
+ optional($.open_tag_start),
119
+ $.tag_name,
120
+ repeat(choice($.shorthand_id, $.shorthand_class)),
121
+ optional($.statement_expr),
122
+ repeat($._tag_part),
123
+ choice($.open_tag_end, $.open_tag_end_self, $.concise_open_tag_end),
124
+ repeat($._child),
125
+ optional($.close_tag),
126
+ $.element_end,
127
+ ),
128
+
129
+ tag_name: ($) => choice(repeat1($._tag_name_part), $._tag_name_empty),
130
+
131
+ shorthand_id: ($) => seq($._shorthand_id_start, repeat($._tag_name_part)),
132
+
133
+ shorthand_class: ($) =>
134
+ seq($._shorthand_class_start, repeat($._tag_name_part)),
135
+
136
+ _tag_name_part: ($) =>
137
+ choice($.tag_name_fragment, alias($.tag_name_placeholder, $.placeholder)),
138
+
139
+ tag_name_placeholder: ($) =>
140
+ seq(
141
+ alias($._interp_start, $.placeholder_start),
142
+ $.placeholder_expr,
143
+ alias($._placeholder_end, $.placeholder_end),
144
+ ),
145
+
146
+ _tag_part: ($) =>
147
+ choice(
148
+ $.tag_var,
149
+ $.args,
150
+ $.params,
151
+ $.type_args,
152
+ $.type_params,
153
+ $.attr_group,
154
+ $.attr_name,
155
+ $.attr_value,
156
+ $.attr_bound_value,
157
+ $.attr_spread,
158
+ $.method_body,
159
+ $._tag_comment,
160
+ ),
161
+
162
+ tag_var: ($) => seq($._tag_var_start, $.tag_var_expr),
163
+
164
+ // The pattern/type split happens exactly where the parser's expression
165
+ // scanner enters type mode (a top-level ":" outside a ternary), so the
166
+ // node's extent always equals the parser's tagVar value range.
167
+ tag_var_expr: ($) =>
168
+ seq($.var_pattern, optional(seq($._var_colon, $.var_type))),
169
+
170
+ // Delimiter tokens are visible named nodes (aliased from the hidden
171
+ // externals) so editors can pair them in bracket-matching queries and
172
+ // capture them as punctuation; they map to no parser events.
173
+ args: ($) =>
174
+ seq(
175
+ alias($._args_open, $.args_open),
176
+ optional($.args_expr),
177
+ alias($._args_close, $.args_close),
178
+ ),
179
+ params: ($) =>
180
+ seq(
181
+ alias($._params_open, $.params_open),
182
+ optional($.params_expr),
183
+ alias($._params_close, $.params_close),
184
+ ),
185
+
186
+ // Parameters split at depth-0 commas/colons/equals; the scanner keeps
187
+ // the parser's exact "|" termination so the extent is unchanged.
188
+ params_expr: ($) => seq($.param, repeat(seq($._param_comma, $.param))),
189
+
190
+ param: ($) =>
191
+ seq(
192
+ $.param_pattern,
193
+ optional(seq($._param_colon, $.param_type)),
194
+ optional(seq($._param_eq, $.param_default)),
195
+ ),
196
+ type_args: ($) =>
197
+ seq(
198
+ alias($._type_args_open, $.type_open),
199
+ optional($.type_expr),
200
+ alias($._types_close, $.type_close),
201
+ ),
202
+ type_params: ($) =>
203
+ seq(
204
+ alias($._type_params_open, $.type_open),
205
+ optional($.type_expr),
206
+ alias($._types_close, $.type_close),
207
+ ),
208
+ attr_group: ($) =>
209
+ seq(
210
+ alias($._attr_group_open, $.attr_group_open),
211
+ repeat($._tag_part),
212
+ alias($._attr_group_close, $.attr_group_close),
213
+ ),
214
+ attr_value: ($) => seq($.attr_eq, $.attr_value_expr),
215
+ attr_bound_value: ($) => seq($.attr_bound_eq, $.attr_value_expr),
216
+ attr_spread: ($) => seq($._attr_spread_start, $.attr_value_expr),
217
+ method_body: ($) =>
218
+ seq(
219
+ alias($._method_body_open, $.method_body_open),
220
+ optional($.method_body_expr),
221
+ alias($._method_body_close, $.method_body_close),
222
+ ),
223
+
224
+ placeholder: ($) =>
225
+ seq(
226
+ choice($.placeholder_start, $.placeholder_start_raw),
227
+ $.placeholder_expr,
228
+ alias($._placeholder_end, $.placeholder_end),
229
+ ),
230
+
231
+ scriptlet: ($) =>
232
+ seq(
233
+ choice($.scriptlet_start, $.scriptlet_start_concise),
234
+ choice($.scriptlet_block, $.scriptlet_expr),
235
+ ),
236
+
237
+ scriptlet_block: ($) =>
238
+ seq(
239
+ alias($._scriptlet_block_open, $.scriptlet_block_open),
240
+ optional($.scriptlet_block_expr),
241
+ alias($._scriptlet_block_close, $.scriptlet_block_close),
242
+ ),
243
+
244
+ html_block: ($) => seq($._block_open, repeat($._child), $._block_close),
245
+
246
+ close_tag: ($) => seq($.close_tag_start, $.close_tag_name, $.close_tag_end),
247
+ },
248
+ });