tree-sitter-aspx 0.1.3 → 1.0.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/README.md CHANGED
@@ -19,7 +19,7 @@ cargo add tree-sitter-aspx # Rust
19
19
 
20
20
  ## Usage
21
21
 
22
- The grammar parses directives, server comments, HTML comments, code nuggets, `<script>` and `<style>` elements, and tags with attributes. It does not validate HTML. Void tags (`br`, `img`, `meta`, and the other HTML void elements) do not swallow the following markup. An end tag closes the nearest open element; the names are not required to match. `<script>` and `<style>` bodies are raw text, so `<` inside JavaScript or CSS is not a tag.
22
+ The grammar parses directives, server comments, HTML comments, code nuggets, `<script>` and `<style>` elements, and tags with attributes. It does not validate HTML. Void tags (`br`, `img`, `meta`, and the other HTML void elements) do not swallow the following markup. End tags that HTML lets a page leave out are implied, as a browser would: an open `li`, `p`, `td`, `tr`, `option`, `dt`, `dd`, or other element with an optional end tag ends at the start tag that closes it (a sibling `li`, a block element after a `p`, and so on), at an ancestor's end tag, or at the end of the file. An end tag that names an element further out closes the elements inside it, and an end tag whose name matches no open element closes nothing and stays in the tree as an `end_tag`, as a browser ignores it. The scanner keeps the names of the innermost 64 open elements; deeper than that, end tags pair with start tags by position. A `<` followed by whitespace starts no tag, so `if a < b` is text, as browsers and ASP.NET read it. `<script>` and `<style>` bodies are raw text, so `<` inside JavaScript or CSS is not a tag, but a nugget or server comment inside a body is still parsed as one. Nuggets and server comments may also stand in for attributes in a tag, as in `<option <%= selected %>>`. `<![CDATA[ ]]>` sections and `<?xml ?>` declarations parse as `cdata_section` and `processing_instruction` nodes whose nuggets are still parsed, and self-closing `<script ... />` and `<style />` tags are `self_closing_element` nodes.
23
23
 
24
24
  The C# and VB.NET inside a page are exposed as `c_sharp_code` and `vb_code` nodes, with injection queries for editors. See [docs/EMBEDDED_CODE.md](docs/EMBEDDED_CODE.md).
25
25
 
package/grammar.js CHANGED
@@ -56,8 +56,9 @@ export default grammar({
56
56
 
57
57
  // Reserved names (`col`, `script`, `Language`, `WebHandler`) must not match
58
58
  // a prefix of a longer name (`Columns`, `asp:ScriptManager`). Keywords are
59
- // recognized only against this word token, so directive names and tag names
60
- // both alias it.
59
+ // recognized only against this word token, which directive names use. Other
60
+ // tag names come from the scanner, which reads the whole name and leaves
61
+ // the reserved ones to the grammar.
61
62
  word: ($) => $._name,
62
63
 
63
64
  externals: ($) => [
@@ -71,7 +72,14 @@ export default grammar({
71
72
  $._script_vb,
72
73
  $.style_text,
73
74
  $.expression_text,
74
- $.handler_text,
75
+ $._handler_csharp,
76
+ $._handler_vb,
77
+ $._start_tag_name,
78
+ $._end_tag_name,
79
+ $._implicit_end_tag,
80
+ $._self_closing_delimiter,
81
+ $._stray_end_tag_name,
82
+ $._script_tag_probe,
75
83
  ],
76
84
 
77
85
  rules: {
@@ -86,9 +94,14 @@ export default grammar({
86
94
  handler_file: ($) =>
87
95
  seq(
88
96
  $.handler_directive,
89
- repeat(choice($.server_comment, $.script_element, alias($.handler_text, $.handler_code))),
97
+ repeat(choice($.directive, $.server_comment, $.script_element, $.handler_code)),
90
98
  ),
91
99
 
100
+ // The code of a handler file, labeled with the directive's language like
101
+ // nugget code so consumers count and inject it the same way.
102
+ handler_code: ($) =>
103
+ choice(alias($._handler_csharp, $.c_sharp_code), alias($._handler_vb, $.vb_code)),
104
+
92
105
  _top: ($) => choice($._content, $.end_tag),
93
106
 
94
107
  _content: ($) =>
@@ -103,33 +116,40 @@ export default grammar({
103
116
  $.self_closing_element,
104
117
  $.element,
105
118
  $.doctype,
119
+ $.cdata_section,
120
+ $.processing_instruction,
121
+ alias($.stray_end_tag, $.end_tag),
106
122
  $.text,
107
123
  ),
108
124
 
109
- directive: ($) => seq('<%@', field('name', $.directive_name), repeat($._attribute), '%>'),
125
+ directive: ($) =>
126
+ seq('<%@', field('name', $.directive_name), repeat($._directive_attribute), '%>'),
110
127
 
111
128
  handler_directive: ($) =>
112
129
  seq(
113
130
  '<%@',
114
131
  field('name', alias(choice($.web_handler_name, $.web_service_name), $.directive_name)),
115
- repeat($._attribute),
132
+ repeat($._directive_attribute),
116
133
  '%>',
117
134
  ),
118
135
 
119
136
  _name: (_) => /[A-Za-z_][A-Za-z0-9_.:-]*/,
120
137
  directive_name: ($) => $._name,
121
- web_handler_name: (_) => choice('WebHandler', 'webhandler', 'WEBHANDLER'),
122
- web_service_name: (_) => choice('WebService', 'webservice', 'WEBSERVICE'),
138
+ web_handler_name: (_) => reserved('WebHandler'),
139
+ web_service_name: (_) => reserved('WebService'),
123
140
 
124
141
  // `Language` is its own attribute so the value is a language_value node
125
- // and the scanner can record C# vs VB for later code tokens.
142
+ // and the scanner can record C# vs VB for later code tokens. On an
143
+ // element it is an ordinary attribute; the scanner reads a server
144
+ // script's `language` itself, before the attributes are parsed.
126
145
  language_attribute: ($) =>
127
146
  seq(
128
147
  field('name', alias($.language_name, $.attribute_name)),
129
148
  '=',
130
149
  choice(
131
- seq('"', field('value', $._language_value), '"'),
132
- seq("'", field('value', $._language_value), "'"),
150
+ seq('"', optional(field('value', $._language_value)), '"'),
151
+ seq("'", optional(field('value', $._language_value)), "'"),
152
+ field('value', $._language_value),
133
153
  ),
134
154
  ),
135
155
 
@@ -138,7 +158,7 @@ export default grammar({
138
158
  _language_value: ($) =>
139
159
  alias(choice($._lang_csharp, $._lang_vb, $._lang_other), $.language_value),
140
160
 
141
- _attribute: ($) => choice($.language_attribute, $.attribute),
161
+ _directive_attribute: ($) => choice($.language_attribute, $.attribute),
142
162
 
143
163
  attribute: ($) =>
144
164
  seq(
@@ -185,7 +205,10 @@ export default grammar({
185
205
 
186
206
  unquoted_attribute_value: (_) => /[^"'=<>\s`]+/,
187
207
 
188
- server_comment: ($) => seq('<%--', optional($.server_comment_text), '--%>'),
208
+ // Any run of two or more dashes before `%>` closes the comment, as in
209
+ // `<%-- note ---%>`; the scanner stops the text before that run.
210
+ server_comment: ($) =>
211
+ seq('<%--', optional($.server_comment_text), alias(token(/-*--%>/), '--%>')),
189
212
 
190
213
  // Nuggets inside an HTML comment still execute, so they stay real nodes.
191
214
  html_comment: ($) =>
@@ -226,15 +249,36 @@ export default grammar({
226
249
  expression_builder: ($) =>
227
250
  seq('<%$', optional(alias($.expression_text, $.expression_value)), '%>'),
228
251
 
252
+ // Script and style bodies are raw text up to their end tag, except that
253
+ // nuggets, server comments, and directives inside them are still parsed. A body
254
+ // split by a nugget is several code or style_text chunks; the injection
255
+ // query combines them.
229
256
  script_element: ($) =>
230
257
  seq(
231
258
  alias($.script_start_tag, $.start_tag),
232
- optional(choice(alias($._script_csharp, $.c_sharp_code), alias($._script_vb, $.vb_code))),
259
+ repeat(
260
+ choice(
261
+ alias($._script_csharp, $.c_sharp_code),
262
+ alias($._script_vb, $.vb_code),
263
+ $._nugget,
264
+ $.server_comment,
265
+ $.directive,
266
+ ),
267
+ ),
233
268
  alias($.script_end_tag, $.end_tag),
234
269
  ),
235
270
 
271
+ // The probe is a zero-width scanner token that reads the tag's `runat`
272
+ // and `language` ahead of the grammar, so a server script's body is
273
+ // labeled with its own language.
236
274
  script_start_tag: ($) =>
237
- seq('<', field('name', alias($.script_tag_name, $.tag_name)), repeat($._attribute), '>'),
275
+ seq(
276
+ '<',
277
+ field('name', alias($.script_tag_name, $.tag_name)),
278
+ $._script_tag_probe,
279
+ repeat($._tag_item),
280
+ '>',
281
+ ),
238
282
 
239
283
  script_end_tag: ($) => seq('</', field('name', alias($.script_tag_name, $.tag_name)), '>'),
240
284
 
@@ -243,12 +287,12 @@ export default grammar({
243
287
  style_element: ($) =>
244
288
  seq(
245
289
  alias($.style_start_tag, $.start_tag),
246
- optional($.style_text),
290
+ repeat(choice($.style_text, $._nugget, $.server_comment, $.directive)),
247
291
  alias($.style_end_tag, $.end_tag),
248
292
  ),
249
293
 
250
294
  style_start_tag: ($) =>
251
- seq('<', field('name', alias($.style_tag_name, $.tag_name)), repeat($._attribute), '>'),
295
+ seq('<', field('name', alias($.style_tag_name, $.tag_name)), repeat($._tag_item), '>'),
252
296
 
253
297
  style_end_tag: ($) => seq('</', field('name', alias($.style_tag_name, $.tag_name)), '>'),
254
298
 
@@ -258,17 +302,48 @@ export default grammar({
258
302
  seq(
259
303
  '<',
260
304
  field('name', alias($.void_tag_name, $.tag_name)),
261
- repeat($._attribute),
305
+ repeat($._tag_item),
262
306
  choice('/>', '>'),
263
307
  ),
264
308
 
265
309
  void_tag_name: (_) => choice(...VOID_TAGS.map((name) => reserved(name))),
266
310
 
267
- self_closing_element: ($) => seq('<', field('name', $.tag_name), repeat($._attribute), '/>'),
311
+ // `<script src="..." />` and `<style />` close themselves in XHTML-era
312
+ // pages instead of opening a raw-text body. Other names go through the
313
+ // scanner's open-element stack, so their `/>` is the scanner's token.
314
+ self_closing_element: ($) =>
315
+ choice(
316
+ seq(
317
+ '<',
318
+ field('name', $._tag_name),
319
+ repeat($._tag_item),
320
+ alias($._self_closing_delimiter, '/>'),
321
+ ),
322
+ seq(
323
+ '<',
324
+ field('name', alias($.script_tag_name, $.tag_name)),
325
+ $._script_tag_probe,
326
+ repeat($._tag_item),
327
+ '/>',
328
+ ),
329
+ seq('<', field('name', alias($.style_tag_name, $.tag_name)), repeat($._tag_item), '/>'),
330
+ ),
331
+
332
+ // An element ends at its end tag or, for HTML's optional end tags (`li`,
333
+ // `p`, `td`, `option`, and the rest), where the scanner finds it implied:
334
+ // at a sibling or other start tag that closes it, at an ancestor's end
335
+ // tag, or at the end of the file.
336
+ element: ($) => seq($.start_tag, repeat($._content), choice($.end_tag, $._implicit_end_tag)),
337
+
338
+ start_tag: ($) => seq('<', field('name', $._tag_name), repeat($._tag_item), '>'),
268
339
 
269
- element: ($) => seq($.start_tag, repeat($._content), $.end_tag),
340
+ // A nugget or server comment can stand in for an attribute, as in
341
+ // `<option value="1" <%= selected ? "selected" : "" %>>`.
342
+ _tag_item: ($) => choice($.attribute, $._nugget, $.server_comment),
270
343
 
271
- start_tag: ($) => seq('<', field('name', $.tag_name), repeat($._attribute), '>'),
344
+ // Start and end tag names come from the scanner, which keeps the stack of
345
+ // open elements that implied end tags are decided from.
346
+ _tag_name: ($) => alias($._start_tag_name, $.tag_name),
272
347
 
273
348
  end_tag: ($) =>
274
349
  seq(
@@ -279,18 +354,56 @@ export default grammar({
279
354
  alias($.script_tag_name, $.tag_name),
280
355
  alias($.style_tag_name, $.tag_name),
281
356
  alias($.void_tag_name, $.tag_name),
282
- $.tag_name,
357
+ alias($._end_tag_name, $.tag_name),
283
358
  ),
284
359
  ),
285
360
  '>',
286
361
  ),
287
362
 
288
- tag_name: ($) => $._name,
363
+ // An end tag that names no open element closes nothing, as a browser
364
+ // treats it, so `<p><table></table></p>` keeps its parent open.
365
+ stray_end_tag: ($) => seq('</', field('name', alias($._stray_end_tag_name, $.tag_name)), '>'),
289
366
 
290
367
  doctype: ($) => seq($.doctype_token, optional(alias(/[^>]+/, $.doctype_declaration)), '>'),
291
368
 
292
369
  doctype_token: (_) => token(prec(RESERVED, /<![Dd][Oo][Cc][Tt][Yy][Pp][Ee]/)),
293
370
 
294
- text: (_) => /[^<\s][^<]*/,
371
+ // `<![CDATA[ ... ]]>` and `<?xml ... ?>` appear in XHTML-era pages and
372
+ // .asmx files. ASP.NET still runs nuggets inside them, as in an RSS feed's
373
+ // `<![CDATA[<%# Eval("Body") %>]]>`, so nuggets stay real nodes and the
374
+ // rest is text.
375
+ cdata_section: ($) =>
376
+ seq(
377
+ '<![CDATA[',
378
+ repeat(
379
+ choice(
380
+ $._nugget,
381
+ $.server_comment,
382
+ alias(/[^\]<]+/, $.text),
383
+ alias(']', $.text),
384
+ alias('<', $.text),
385
+ ),
386
+ ),
387
+ ']]>',
388
+ ),
389
+
390
+ processing_instruction: ($) =>
391
+ seq(
392
+ '<?',
393
+ repeat(
394
+ choice(
395
+ $._nugget,
396
+ $.server_comment,
397
+ alias(/[^?<]+/, $.text),
398
+ alias('?', $.text),
399
+ alias('<', $.text),
400
+ ),
401
+ ),
402
+ '?>',
403
+ ),
404
+
405
+ // A `<` followed by whitespace starts no tag, as browsers and ASP.NET's
406
+ // own tag pattern read it, so `a < b` is text rather than an element.
407
+ text: (_) => token(choice(/[^<\s][^<]*/, /<\s[^<]*/)),
295
408
  },
296
409
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tree-sitter-aspx",
3
- "version": "0.1.3",
3
+ "version": "1.0.0",
4
4
  "description": "Tree-sitter grammar for ASP.NET Web Forms markup",
5
5
  "main": "bindings/node",
6
6
  "types": "bindings/node",
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "node-gyp": "13.1.0",
24
- "oxfmt": "0.74.0",
25
- "oxlint": "1.89.0",
24
+ "oxfmt": "0.75.0",
25
+ "oxlint": "1.90.0",
26
26
  "tree-sitter": "0.25.1",
27
27
  "tree-sitter-cli": "0.27.1"
28
28
  },
@@ -7,6 +7,8 @@
7
7
  (language_value) @type
8
8
  (doctype_token) @keyword
9
9
  (doctype_declaration) @string
10
+ (processing_instruction) @keyword
11
+ (cdata_section) @string
10
12
 
11
13
  (server_comment) @comment
12
14
  (server_comment_text) @comment
@@ -9,9 +9,22 @@
9
9
  ;
10
10
  ; A <script> body uses the same code node. Without runat="server" the body
11
11
  ; is client script, so the query below replaces the page language with
12
- ; JavaScript. With runat="server" the page language stands.
12
+ ; JavaScript. With runat="server" the page language, or the script's own
13
+ ; language attribute, stands. ASP.NET compares the value without regard to
14
+ ; case and accepts it unquoted, so runat=SERVER and runat=server count too.
15
+ ; The pattern needs whitespace before runat and a non-name character after
16
+ ; server, so text such as src="x.ashx?runat=server" or runat="serverless"
17
+ ; does not match. It uses only syntax that Rust, ECMAScript, and Vim regex
18
+ ; read the same way.
13
19
  ; Expression builders (`<%$ ... %>`) are not C# or VB and are not injected.
14
20
  ; <style> bodies inject as CSS.
21
+ ;
22
+ ; A nugget inside a <script> or <style> body splits the body into several
23
+ ; code or style_text chunks. Those patterns set injection.combined so the
24
+ ; chunks are parsed together with the nuggets left out; the nuggets
25
+ ; themselves are injected by the patterns above. Combined injection joins
26
+ ; every match of a pattern in the page, so all client script bodies form one
27
+ ; JavaScript document, and likewise for server scripts and styles.
15
28
 
16
29
  ((statement_block
17
30
  (c_sharp_code) @injection.content)
@@ -47,24 +60,40 @@
47
60
  (vb_code) @injection.content)
48
61
  (#set! injection.language "vb"))
49
62
 
63
+ ; Handler files (.ashx, .asmx) are a directive plus code. The code pieces
64
+ ; between any <script> elements and server comments form one document.
65
+ ((handler_code
66
+ (c_sharp_code) @injection.content)
67
+ (#set! injection.language "c_sharp")
68
+ (#set! injection.combined))
69
+
70
+ ((handler_code
71
+ (vb_code) @injection.content)
72
+ (#set! injection.language "vb")
73
+ (#set! injection.combined))
74
+
50
75
  ((script_element
51
76
  (start_tag) @_tag
52
77
  (c_sharp_code) @injection.content)
53
- (#match? @_tag "[Rr][Uu][Nn][Aa][Tt]\\s*=\\s*[\"'][Ss]erver")
54
- (#set! injection.language "c_sharp"))
78
+ (#match? @_tag "\\s[Rr][Uu][Nn][Aa][Tt]\\s*=\\s*[\"']*[Ss][Ee][Rr][Vv][Ee][Rr][^A-Za-z0-9_:.-]")
79
+ (#set! injection.language "c_sharp")
80
+ (#set! injection.combined))
55
81
 
56
82
  ((script_element
57
83
  (start_tag) @_tag
58
84
  (vb_code) @injection.content)
59
- (#match? @_tag "[Rr][Uu][Nn][Aa][Tt]\\s*=\\s*[\"'][Ss]erver")
60
- (#set! injection.language "vb"))
85
+ (#match? @_tag "\\s[Rr][Uu][Nn][Aa][Tt]\\s*=\\s*[\"']*[Ss][Ee][Rr][Vv][Ee][Rr][^A-Za-z0-9_:.-]")
86
+ (#set! injection.language "vb")
87
+ (#set! injection.combined))
61
88
 
62
89
  ((script_element
63
90
  (start_tag) @_tag
64
91
  [(c_sharp_code) (vb_code)] @injection.content)
65
- (#not-match? @_tag "[Rr][Uu][Nn][Aa][Tt]\\s*=\\s*[\"'][Ss]erver")
66
- (#set! injection.language "javascript"))
92
+ (#not-match? @_tag "\\s[Rr][Uu][Nn][Aa][Tt]\\s*=\\s*[\"']*[Ss][Ee][Rr][Vv][Ee][Rr][^A-Za-z0-9_:.-]")
93
+ (#set! injection.language "javascript")
94
+ (#set! injection.combined))
67
95
 
68
96
  ((style_element
69
97
  (style_text) @injection.content)
70
- (#set! injection.language "css"))
98
+ (#set! injection.language "css")
99
+ (#set! injection.combined))