@marko/tree-sitter 0.3.0 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marko/tree-sitter",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Tree-sitter grammar for the Marko templating language",
5
5
  "keywords": [
6
6
  "incremental",
@@ -34,7 +34,7 @@
34
34
  "tree-sitter.json"
35
35
  ],
36
36
  "dependencies": {
37
- "node-addon-api": "^8.3.0",
37
+ "node-addon-api": "^8.9.2",
38
38
  "node-gyp-build": "^4.8.4"
39
39
  },
40
40
  "devDependencies": {
@@ -42,15 +42,15 @@
42
42
  "@changesets/cli": "^2.31.0",
43
43
  "@types/degit": "^2.8.6",
44
44
  "@types/mocha": "^10.0.6",
45
- "@types/node": "^20.11.16",
45
+ "@types/node": "^22.20.4",
46
46
  "degit": "^2.8.4",
47
- "htmljs-parser": "^5.14.0",
48
- "mocha": "^10.2.0",
49
- "node-gyp": "^11.0.0",
47
+ "htmljs-parser": "^5.20.0",
48
+ "mocha": "^12.0.2",
49
+ "node-gyp": "^13.0.2",
50
50
  "prebuildify": "^6.0.1",
51
- "tree-sitter": "^0.25.0",
51
+ "tree-sitter": "^0.25.1",
52
52
  "tree-sitter-cli": "^0.26.9",
53
- "tsx": "^4.19.2",
53
+ "tsx": "^4.23.15",
54
54
  "web-tree-sitter": "^0.26.9"
55
55
  },
56
56
  "peerDependencies": {
@@ -81,6 +81,6 @@
81
81
  "prebuild": "prebuildify --napi --strip",
82
82
  "build:wasm": "tree-sitter build --wasm -o tree-sitter-marko.wasm",
83
83
  "test:wasm": "tsx tools/check-wasm.mts",
84
- "version": "changeset version && node tools/sync-tree-sitter-version.mjs && pnpm install --lockfile-only"
84
+ "version": "changeset version && node tools/sync-tree-sitter-version.mjs && tree-sitter generate && pnpm install --lockfile-only"
85
85
  }
86
86
  }
@@ -13,6 +13,7 @@
13
13
  (shorthand_class) @property
14
14
 
15
15
  (attr_name) @attribute
16
+ (attr_method_async) @keyword
16
17
 
17
18
  ; Binding positions: patterns get the TS injection; types are captured
18
19
  ; flatly because a bare type is not a valid TS program (matching the
package/src/parser.c CHANGED
@@ -7965,8 +7965,8 @@ TS_PUBLIC const TSLanguage *tree_sitter_marko(void) {
7965
7965
  .max_reserved_word_set_size = 0,
7966
7966
  .metadata = {
7967
7967
  .major_version = 0,
7968
- .minor_version = 3,
7969
- .patch_version = 0,
7968
+ .minor_version = 4,
7969
+ .patch_version = 1,
7970
7970
  },
7971
7971
  };
7972
7972
  return &language;
package/src/scanner.c CHANGED
@@ -555,16 +555,57 @@ static bool should_terminate(EStream *es, ExprState *e, int32_t code,
555
555
  return false;
556
556
  }
557
557
 
558
+ // Only JavaScript has `delete` and `void` operators; in a type `void` is the
559
+ // `void` type. `return`, `throw` and `yield` are left out: JavaScript allows
560
+ // no line break after them, so they never continue an expression.
558
561
  static const char *const UNARY_KEYWORDS[] = {
559
- "async", "await", "class", "function", "new", "typeof", "void", NULL,
562
+ "async", "await", "class", "function", "new", "typeof", "delete", "void",
563
+ NULL,
560
564
  };
561
565
  static const char *const TS_UNARY_KEYWORDS[] = {
562
- "async", "await", "class", "function", "new", "typeof", "void",
563
- "asserts", "infer", "is", "keyof", "readonly", "unique", NULL,
566
+ "async", "await", "class", "function", "new", "typeof",
567
+ "asserts", "infer", "is", "keyof", "readonly", "unique", NULL,
564
568
  };
565
569
  static const char *const BINARY_KEYWORDS[] = {
566
570
  "as", "extends", "instanceof", "in", "satisfies", NULL,
567
571
  };
572
+ static const char *const RELATIONAL_KEYWORDS[] = {"in", "instanceof", NULL};
573
+
574
+ static bool es_peek_matches(EStream *es, uint32_t k, const char *str) {
575
+ for (uint32_t j = 0; str[j]; j++) {
576
+ if (es_peek(es, k + j) != (int32_t)str[j]) return false;
577
+ }
578
+ return true;
579
+ }
580
+
581
+ // prepareScriptlet's type look-ahead from offset `k`: `declare`, `interface`
582
+ // or `type` (after spaces) before a name, or `type` before `{`/`*`, starts a
583
+ // type; `type = 1` and `type in x` stay JavaScript.
584
+ static bool looks_like_type_statement(EStream *es, uint32_t k) {
585
+ static const char *const TYPE_KEYWORDS[] = {"declare", "interface", "type",
586
+ NULL};
587
+ while (is_indent_code(es_peek(es, k))) k++;
588
+ for (int i = 0; TYPE_KEYWORDS[i]; i++) {
589
+ if (!es_peek_matches(es, k, TYPE_KEYWORDS[i])) continue;
590
+
591
+ uint32_t n = k + (uint32_t)strlen(TYPE_KEYWORDS[i]);
592
+ if (!is_indent_code(es_peek(es, n))) return false;
593
+ while (is_indent_code(es_peek(es, n))) n++;
594
+
595
+ int32_t c = es_peek(es, n);
596
+ if (c == '{' || c == '*') return strcmp(TYPE_KEYWORDS[i], "type") == 0;
597
+ if (!is_word_code(c) || (c >= '0' && c <= '9')) return false;
598
+ for (int b = 0; BINARY_KEYWORDS[b]; b++) {
599
+ if (es_peek_matches(es, n, BINARY_KEYWORDS[b]) &&
600
+ !is_word_code(
601
+ es_peek(es, n + (uint32_t)strlen(BINARY_KEYWORDS[b])))) {
602
+ return false;
603
+ }
604
+ }
605
+ return true;
606
+ }
607
+ return false;
608
+ }
568
609
 
569
610
  static int64_t look_behind_for(EStream *es, int64_t pos, const char *str) {
570
611
  int64_t len = (int64_t)strlen(str);
@@ -586,6 +627,19 @@ static int64_t look_behind_while_ws(EStream *es, int64_t pos) {
586
627
  return 0;
587
628
  }
588
629
 
630
+ // Returns where a whole keyword ending at `pos` starts, or -1.
631
+ static int64_t look_behind_for_keyword(EStream *es, int64_t pos,
632
+ const char *const *keywords) {
633
+ for (int i = 0; keywords[i]; i++) {
634
+ int64_t keyword_pos = look_behind_for(es, pos, keywords[i]);
635
+ if (keyword_pos != -1) {
636
+ int32_t before = keyword_pos > 0 ? es_at(es, keyword_pos - 1) : -1;
637
+ return is_word_or_period_code(before) ? -1 : keyword_pos;
638
+ }
639
+ }
640
+ return -1;
641
+ }
642
+
589
643
  // lookBehindForOperator: `pos` is a buf index just past the candidate.
590
644
  static int64_t look_behind_for_operator(EStream *es, ExprState *e,
591
645
  int64_t pos) {
@@ -593,12 +647,33 @@ static int64_t look_behind_for_operator(EStream *es, ExprState *e,
593
647
  int32_t code = es_at(es, cur_pos);
594
648
 
595
649
  switch (code) {
596
- case '&': case '*': case '^': case ':': case '=': case '!':
650
+ case '&': case '*': case '^': case ':': case '=':
597
651
  case '<': case '%': case '|': case '?': case '~':
598
652
  // The operator may sit just before the token start (cur_pos == -1, eg
599
653
  // resuming a tag var type after its ":"); callers only test != -1.
600
654
  return cur_pos < 0 ? 0 : cur_pos;
601
655
 
656
+ case '!': {
657
+ // After an operand, `!` is a TypeScript non-null assertion (postfix),
658
+ // as is each `!` of a run after one (`x!!`); after a keyword operator
659
+ // (`typeof!a`, `a in!b`) it is the prefix `!`.
660
+ int64_t operand_end = cur_pos - 1;
661
+ while (es_at(es, operand_end) == '!') operand_end--;
662
+ int32_t operand = es_at(es, operand_end);
663
+ switch (operand) {
664
+ case ')': case ']': case '"': case '\'': case '`':
665
+ return -1;
666
+ default:
667
+ return is_word_code(operand) &&
668
+ look_behind_for_operator(es, e, operand_end + 1) ==
669
+ -1 &&
670
+ look_behind_for_keyword(es, operand_end,
671
+ RELATIONAL_KEYWORDS) == -1
672
+ ? -1
673
+ : (cur_pos < 0 ? 0 : cur_pos);
674
+ }
675
+ }
676
+
602
677
  case '>':
603
678
  if (es_at(es, cur_pos - 1) == '=') return cur_pos - 1;
604
679
  return e->in_type ? -1 : (cur_pos < 0 ? 0 : cur_pos);
@@ -629,16 +704,8 @@ static int64_t look_behind_for_operator(EStream *es, ExprState *e,
629
704
 
630
705
  default: {
631
706
  if (code < 'a' || code > 'z') return -1;
632
- const char *const *keywords =
633
- e->in_type ? TS_UNARY_KEYWORDS : UNARY_KEYWORDS;
634
- for (int i = 0; keywords[i]; i++) {
635
- int64_t keyword_pos = look_behind_for(es, cur_pos, keywords[i]);
636
- if (keyword_pos != -1) {
637
- int32_t before = keyword_pos > 0 ? es_at(es, keyword_pos - 1) : -1;
638
- return is_word_or_period_code(before) ? -1 : keyword_pos;
639
- }
640
- }
641
- return -1;
707
+ return look_behind_for_keyword(
708
+ es, cur_pos, e->in_type ? TS_UNARY_KEYWORDS : UNARY_KEYWORDS);
642
709
  }
643
710
  }
644
711
  }
@@ -743,9 +810,15 @@ static bool check_for_operators(EStream *es, ExprState *e, bool eol) {
743
810
  }
744
811
 
745
812
  int32_t next_c = es_peek(es, k);
746
- // In HTML mode a "</" is a close tag rather than a less-than operator.
747
- if (!is_concise(s) && next_c == '<' && es_peek(es, k + 1) == '/') {
748
- return false;
813
+ // A "</" close tag (html mode) or a "<!--" html comment is markup rather
814
+ // than a less-than operator, which look_ahead_for_operator would otherwise
815
+ // continue across.
816
+ if (next_c == '<') {
817
+ if (!is_concise(s) && es_peek(es, k + 1) == '/') return false;
818
+ if (es_peek(es, k + 1) == '!' && es_peek(es, k + 2) == '-' &&
819
+ es_peek(es, k + 3) == '-') {
820
+ return false;
821
+ }
749
822
  }
750
823
 
751
824
  if (next_c >= 0 && !should_terminate(es, e, next_c, k)) {
@@ -1643,7 +1716,16 @@ static bool scan_scriptlet_body(Scanner *s, TSLexer *lexer, const bool *valid,
1643
1716
  memset(&cfg, 0, sizeof(cfg));
1644
1717
  cfg.operators = true;
1645
1718
  cfg.terminated_by_eol = true;
1646
- if (!scan_expr_token(s, lexer, cfg, NULL)) return false;
1719
+ EStream es;
1720
+ es_init(&es, s, lexer, true);
1721
+ mark(s, lexer); // empty expression: zero-width token
1722
+ if (looks_like_type_statement(&es, 0)) {
1723
+ cfg.in_type = true;
1724
+ cfg.force_type = true;
1725
+ }
1726
+ bool ok = scan_expr_es(&es, cfg, NULL);
1727
+ es_free(&es);
1728
+ if (!ok) return false;
1647
1729
  *result = SCRIPTLET_EXPR;
1648
1730
  return true;
1649
1731
  }
@@ -2501,6 +2583,19 @@ static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
2501
2583
  // Concise mode content (CONCISE_HTML_CONTENT)
2502
2584
  // --------------------------------------------------------------------------
2503
2585
 
2586
+ // In concise mode only whitespace may follow a comment block on its line; the
2587
+ // comment event still fires before the error.
2588
+ static void check_block_comment_tail(Scanner *s, EStream *es) {
2589
+ for (uint32_t k = 0;; k++) {
2590
+ int32_t c = es_peek(es, k);
2591
+ if (c < 0 || is_line(c)) return;
2592
+ if (!is_ws(c)) {
2593
+ s->fail_next = 1; // INVALID_CHARACTER after this token
2594
+ return;
2595
+ }
2596
+ }
2597
+ }
2598
+
2504
2599
  static bool scan_concise(Scanner *s, TSLexer *lexer, const bool *valid,
2505
2600
  TSSymbol *result) {
2506
2601
  // Trivia: newlines and indentation.
@@ -2574,6 +2669,7 @@ static bool scan_concise(Scanner *s, TSLexer *lexer, const bool *valid,
2574
2669
  } else if (n1 == '*') {
2575
2670
  *result = BLOCK_COMMENT;
2576
2671
  ok = es_consume_block_comment(&es) && valid[BLOCK_COMMENT];
2672
+ if (ok) check_block_comment_tail(s, &es);
2577
2673
  }
2578
2674
  es_free(&es);
2579
2675
  return ok;
@@ -2609,6 +2705,14 @@ static bool scan_concise(Scanner *s, TSLexer *lexer, const bool *valid,
2609
2705
  }
2610
2706
  }
2611
2707
 
2708
+ if (c == ';') {
2709
+ // An empty line, as after a tag ended by ";": the ";" is trivia and only
2710
+ // comments may follow it.
2711
+ skipc(s, lexer);
2712
+ s->semicolon_line = 1;
2713
+ return scan_concise(s, lexer, valid, result);
2714
+ }
2715
+
2612
2716
  if (c == '<') {
2613
2717
  // Begin a mixed-mode html block; HTML_CONTENT handles the "<".
2614
2718
  s->begin_mixed_mode = 1;
@@ -2633,12 +2737,17 @@ static bool scan_concise(Scanner *s, TSLexer *lexer, const bool *valid,
2633
2737
  }
2634
2738
 
2635
2739
  if (c == '-') {
2636
- if (es_peek(&es, 1) == '-') {
2740
+ int32_t n1 = es_peek(&es, 1);
2741
+ if (n1 == '-') {
2637
2742
  ok = valid[BLOCK_OPEN] && scan_block_open(s, &es, result);
2743
+ es_free(&es);
2744
+ return ok;
2638
2745
  }
2639
- // else: INVALID_LINE_START: single hyphen
2640
- es_free(&es);
2641
- return ok;
2746
+ if ((t && t->type == TAG_TEXT) || !is_word_code(n1)) {
2747
+ es_free(&es);
2748
+ return false; // INVALID_LINE_START: single hyphen
2749
+ }
2750
+ // A tag name starting with "-" (eg "-if"): the open tag below reads it.
2642
2751
  }
2643
2752
 
2644
2753
  if (c == '/') {
@@ -2649,20 +2758,7 @@ static bool scan_concise(Scanner *s, TSLexer *lexer, const bool *valid,
2649
2758
  ok = valid[LINE_COMMENT];
2650
2759
  } else if (n1 == '*') {
2651
2760
  ok = es_consume_block_comment(&es);
2652
- if (ok) {
2653
- // In concise mode, only whitespace may follow a comment block; the
2654
- // comment event still fires before the error.
2655
- uint32_t k = 0;
2656
- while (true) {
2657
- int32_t c2 = es_peek(&es, k);
2658
- if (c2 < 0 || is_line(c2)) break;
2659
- if (!is_ws(c2)) {
2660
- s->fail_next = 1; // INVALID_CHARACTER after this token
2661
- break;
2662
- }
2663
- k++;
2664
- }
2665
- }
2761
+ if (ok) check_block_comment_tail(s, &es);
2666
2762
  *result = BLOCK_COMMENT;
2667
2763
  ok = ok && valid[BLOCK_COMMENT];
2668
2764
  }
@@ -2703,6 +2799,14 @@ static bool is_tag_name_terminator(Scanner *s, int32_t c, int32_t next) {
2703
2799
  }
2704
2800
  }
2705
2801
 
2802
+ // Nothing of the tag name is read yet: OPEN_TAG still skips "," and
2803
+ // whitespace, and TAG_NAME starts at the next char.
2804
+ static bool tag_name_pending(Scanner *s) {
2805
+ TagFrame *t = top_tag(s);
2806
+ return t && !s->tag_name_done && !s->in_shorthand && t->name_len == 0 &&
2807
+ !(t->flags & TF_NAME_INTERP);
2808
+ }
2809
+
2706
2810
  // Finish the current tag name segment (called when a terminator is seen).
2707
2811
  static void finalize_tag_name(Scanner *s, int32_t stop_char) {
2708
2812
  if (!s->tag_name_done) {
@@ -2835,23 +2939,10 @@ static bool scan_statement_tail(Scanner *s, TSLexer *lexer, const bool *valid,
2835
2939
  cfg.terminated_by_eol = true;
2836
2940
  cfg.consume_indented = true;
2837
2941
 
2838
- // lookAheadFor("declare " | "interface " | "type ") from pos + 1.
2839
- static const char *const PREFIXES[] = {"declare ", "interface ", "type ",
2840
- NULL};
2841
- for (int i = 0; PREFIXES[i]; i++) {
2842
- const char *p = PREFIXES[i];
2843
- bool match_p = true;
2844
- for (uint32_t j = 0; p[j]; j++) {
2845
- if (es_peek(es, 1 + j) != (int32_t)p[j]) {
2846
- match_p = false;
2847
- break;
2848
- }
2849
- }
2850
- if (match_p) {
2851
- cfg.in_type = true;
2852
- cfg.force_type = true;
2853
- break;
2854
- }
2942
+ // prepareStatement: the code starts one past the tag name.
2943
+ if (looks_like_type_statement(es, 1)) {
2944
+ cfg.in_type = true;
2945
+ cfg.force_type = true;
2855
2946
  }
2856
2947
 
2857
2948
  if (!scan_expr_es(es, cfg, NULL)) return false;
@@ -2872,6 +2963,14 @@ static bool scan_open_tag_es(Scanner *s, TSLexer *lexer, const bool *valid,
2872
2963
  return scan_statement_tail(s, lexer, valid, es);
2873
2964
  }
2874
2965
 
2966
+ // ---- a ";" read before the (empty) name, eg ",;", ended the tag there
2967
+ if (s->semicolon_line) {
2968
+ if (!valid[CONCISE_OPEN_TAG_END]) return false;
2969
+ open_tag_ended(s, false);
2970
+ lexer->result_symbol = CONCISE_OPEN_TAG_END; // zero-width
2971
+ return true;
2972
+ }
2973
+
2875
2974
  // ---- grammar-position expressions (no trivia before them)
2876
2975
  if (valid[ARGS_EXPR] && !valid[ATTR_NAME]) {
2877
2976
  bool empty = false;
@@ -3105,6 +3204,22 @@ static bool scan_open_tag_es(Scanner *s, TSLexer *lexer, const bool *valid,
3105
3204
 
3106
3205
  // ---- trivia loop (only when no lookahead is pending)
3107
3206
  while (es_drained(es)) {
3207
+ if (concise && tag_name_pending(s) &&
3208
+ (at_eof(lexer) || is_line(lexer->lookahead) ||
3209
+ lexer->lookahead == ';')) {
3210
+ // The open tag ends before a name (eg a line of just "," or ",;"): it
3211
+ // is named empty where it ends, which is after a ";".
3212
+ if (!valid[TAG_NAME_EMPTY]) return false;
3213
+ if (!at_eof(lexer) && lexer->lookahead == ';') {
3214
+ skipc(s, lexer);
3215
+ s->semicolon_line = 1; // the open tag ends on the next scan
3216
+ }
3217
+ finalize_tag_name(s, -1);
3218
+ mark(s, lexer);
3219
+ lexer->result_symbol = TAG_NAME_EMPTY;
3220
+ return true;
3221
+ }
3222
+
3108
3223
  if (at_eof(lexer)) {
3109
3224
  if (!s->tag_name_done || s->in_shorthand) finalize_tag_name(s, -1);
3110
3225
  s->statement_pending = 0; // an empty statement expression: no events
@@ -3119,7 +3234,7 @@ static bool scan_open_tag_es(Scanner *s, TSLexer *lexer, const bool *valid,
3119
3234
 
3120
3235
  int32_t cc = lexer->lookahead;
3121
3236
 
3122
- if ((!s->tag_name_done || s->in_shorthand) &&
3237
+ if ((!s->tag_name_done || s->in_shorthand) && !tag_name_pending(s) &&
3123
3238
  (is_ws(cc) || cc == ',')) {
3124
3239
  // TAG_NAME terminates at whitespace/comma before any trivia skipping.
3125
3240
  finalize_tag_name(s, cc);
@@ -3163,6 +3278,29 @@ static bool scan_open_tag_es(Scanner *s, TSLexer *lexer, const bool *valid,
3163
3278
  continue;
3164
3279
  }
3165
3280
 
3281
+ if (cc == '/' && tag_name_pending(s)) {
3282
+ // OPEN_TAG reads comments before TAG_NAME (eg ", /* c */ div"), so the
3283
+ // name starts after them. An open tag comment is a hidden token anyway,
3284
+ // so skip it as trivia; any other "/" ends an empty name here.
3285
+ mark(s, lexer);
3286
+ int32_t n1 = es_peek(es, 1);
3287
+ if (n1 != '/' && n1 != '*') break;
3288
+ es_next(es); // "/": skipping the rest drops it from the token
3289
+ skipc(s, lexer); // "/" or "*"
3290
+ if (n1 == '/') {
3291
+ while (!at_eof(lexer) && !is_line(lexer->lookahead)) skipc(s, lexer);
3292
+ } else {
3293
+ int32_t prev = 0;
3294
+ while (!at_eof(lexer) && !(prev == '*' && lexer->lookahead == '/')) {
3295
+ prev = lexer->lookahead;
3296
+ skipc(s, lexer);
3297
+ }
3298
+ if (at_eof(lexer)) return false; // MALFORMED_COMMENT
3299
+ skipc(s, lexer);
3300
+ }
3301
+ continue;
3302
+ }
3303
+
3166
3304
  break;
3167
3305
  }
3168
3306
 
@@ -3179,6 +3317,18 @@ static bool scan_open_tag_es(Scanner *s, TSLexer *lexer, const bool *valid,
3179
3317
 
3180
3318
  // ---- tag name section (TAG_NAME handles its chars before OPEN_TAG does)
3181
3319
  if (!s->tag_name_done || s->in_shorthand) {
3320
+ if (concise && c == '-' && tag_name_pending(s)) {
3321
+ // OPEN_TAG reads "-" before TAG_NAME: a name only starts with "-" and a
3322
+ // word char (eg "-if"), and "--" ends the tag before a name.
3323
+ int32_t n1 = es_peek(es, 1);
3324
+ if (n1 == '-') {
3325
+ if (!valid[TAG_NAME_EMPTY]) return false;
3326
+ finalize_tag_name(s, c);
3327
+ lexer->result_symbol = TAG_NAME_EMPTY; // zero-width at the "--"
3328
+ return true;
3329
+ }
3330
+ if (!is_word_code(n1)) return false; // MALFORMED_OPEN_TAG
3331
+ }
3182
3332
  TSSymbol name_sym = 0;
3183
3333
  int r = scan_tag_name_token(s, valid, es, &name_sym);
3184
3334
  if (r == 0) return false;
Binary file
package/tree-sitter.json CHANGED
@@ -16,7 +16,7 @@
16
16
  }
17
17
  ],
18
18
  "metadata": {
19
- "version": "0.3.0",
19
+ "version": "0.4.1",
20
20
  "license": "MIT",
21
21
  "description": "Tree-sitter grammar for the Marko templating language",
22
22
  "links": {