@marko/tree-sitter 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marko/tree-sitter",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Tree-sitter grammar for the Marko templating language",
5
5
  "keywords": [
6
6
  "incremental",
@@ -44,7 +44,7 @@
44
44
  "@types/mocha": "^10.0.6",
45
45
  "@types/node": "^22.20.4",
46
46
  "degit": "^2.8.4",
47
- "htmljs-parser": "^5.20.0",
47
+ "htmljs-parser": "^5.22.1",
48
48
  "mocha": "^12.0.2",
49
49
  "node-gyp": "^13.0.2",
50
50
  "prebuildify": "^6.0.1",
@@ -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,7 +7965,7 @@ 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 = 4,
7968
+ .minor_version = 5,
7969
7969
  .patch_version = 0,
7970
7970
  },
7971
7971
  };
package/src/scanner.c CHANGED
@@ -173,12 +173,22 @@ typedef struct {
173
173
  #define MAX_TAGS 24
174
174
  #define MAX_CONTENTS 12
175
175
 
176
+ // The longest operand keyword ("instanceof").
177
+ #define KEYWORD_MAX 10
178
+ // Cursor.word_len of a word after a ".": a property name, never a keyword.
179
+ #define WORD_AFTER_PERIOD UINT8_MAX
180
+
176
181
  // Position bookkeeping replacing the parser's backwards reads of the source.
177
182
  // Committed in lock-step with mark_end (see `mark`).
178
183
  typedef struct {
179
184
  uint8_t prev_char;
180
185
  uint8_t prev_nonws_char;
181
186
  uint8_t line_only_ws; // only whitespace since the last newline
187
+ // The word prev_nonws_char ends (0: it is not a word char), so a "/" can
188
+ // tell a keyword from an operand across token boundaries: its length
189
+ // (saturating) and its first KEYWORD_MAX chars.
190
+ uint8_t word_len;
191
+ char word[KEYWORD_MAX];
182
192
  uint16_t cur_indent_len;
183
193
  uint32_t cur_indent_hash;
184
194
  } Cursor;
@@ -224,6 +234,7 @@ typedef struct {
224
234
  // scratch (not serialized)
225
235
  char *buf;
226
236
  uint32_t buf_len, buf_cap;
237
+ uint8_t saved_pstring_quote; // the top content frame's, as of `mark`
227
238
  } Scanner;
228
239
 
229
240
  // ATTR_STAGE mirror of states/ATTRIBUTE.ts
@@ -253,6 +264,16 @@ static inline ContentFrame *top_content(Scanner *s) {
253
264
  }
254
265
 
255
266
  static void cursor_advance(Cursor *cur, int32_t c) {
267
+ if (is_word_code(c)) {
268
+ if (!cur->word_len || !is_word_code(cur->prev_char)) {
269
+ cur->word_len = cur->prev_char == '.' ? WORD_AFTER_PERIOD : 0;
270
+ }
271
+ if (cur->word_len < KEYWORD_MAX) cur->word[cur->word_len] = (char)c;
272
+ if (cur->word_len < UINT8_MAX) cur->word_len++;
273
+ } else if (!is_ws(c)) {
274
+ cur->word_len = 0;
275
+ }
276
+
256
277
  cur->prev_char = (uint8_t)(c & 0xff);
257
278
  if (is_line(c)) {
258
279
  cur->line_only_ws = 1;
@@ -280,6 +301,8 @@ static inline void skipc(Scanner *s, TSLexer *lexer) {
280
301
  static inline void mark(Scanner *s, TSLexer *lexer) {
281
302
  lexer->mark_end(lexer);
282
303
  s->saved = s->cur;
304
+ ContentFrame *f = top_content(s);
305
+ s->saved_pstring_quote = f ? f->pstring_quote : 0;
283
306
  }
284
307
 
285
308
  static inline bool at_eof(TSLexer *lexer) { return lexer->eof(lexer); }
@@ -569,7 +592,12 @@ static const char *const TS_UNARY_KEYWORDS[] = {
569
592
  static const char *const BINARY_KEYWORDS[] = {
570
593
  "as", "extends", "instanceof", "in", "satisfies", NULL,
571
594
  };
572
- static const char *const RELATIONAL_KEYWORDS[] = {"in", "instanceof", NULL};
595
+ // Keywords an operand follows, so a "/" after one starts a regular expression
596
+ // (`return /x/`) and a "!" after one is the prefix operator (`return !x`).
597
+ static const char *const OPERAND_KEYWORDS[] = {
598
+ "await", "case", "delete", "do", "else", "in", "instanceof",
599
+ "new", "return", "throw", "typeof", "void", "yield", NULL,
600
+ };
573
601
 
574
602
  static bool es_peek_matches(EStream *es, uint32_t k, const char *str) {
575
603
  for (uint32_t j = 0; str[j]; j++) {
@@ -640,6 +668,20 @@ static int64_t look_behind_for_keyword(EStream *es, int64_t pos,
640
668
  return -1;
641
669
  }
642
670
 
671
+ // endsWithOperandKeyword: whether a whole operand keyword ends at buf index
672
+ // `pos`; one after a "." is a property name.
673
+ static bool ends_with_operand_keyword(EStream *es, int64_t pos) {
674
+ int32_t c = es_at(es, pos);
675
+ if (c < 'a' || c > 'z') return false;
676
+ for (int i = 0; OPERAND_KEYWORDS[i]; i++) {
677
+ int64_t start = look_behind_for(es, pos, OPERAND_KEYWORDS[i]);
678
+ if (start != -1 && !is_word_or_period_code(es_at(es, start - 1))) {
679
+ return true;
680
+ }
681
+ }
682
+ return false;
683
+ }
684
+
643
685
  // lookBehindForOperator: `pos` is a buf index just past the candidate.
644
686
  static int64_t look_behind_for_operator(EStream *es, ExprState *e,
645
687
  int64_t pos) {
@@ -655,10 +697,12 @@ static int64_t look_behind_for_operator(EStream *es, ExprState *e,
655
697
 
656
698
  case '!': {
657
699
  // 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 `!`.
700
+ // as is each `!` of a run after one (`x!!`), spaced or not (`x !`);
701
+ // after an operator or keyword (`typeof!a`, `a in !b`, `return !a`) it
702
+ // is the prefix `!`.
660
703
  int64_t operand_end = cur_pos - 1;
661
704
  while (es_at(es, operand_end) == '!') operand_end--;
705
+ operand_end = look_behind_while_ws(es, operand_end) - 1;
662
706
  int32_t operand = es_at(es, operand_end);
663
707
  switch (operand) {
664
708
  case ')': case ']': case '"': case '\'': case '`':
@@ -667,8 +711,7 @@ static int64_t look_behind_for_operator(EStream *es, ExprState *e,
667
711
  return is_word_code(operand) &&
668
712
  look_behind_for_operator(es, e, operand_end + 1) ==
669
713
  -1 &&
670
- look_behind_for_keyword(es, operand_end,
671
- RELATIONAL_KEYWORDS) == -1
714
+ !ends_with_operand_keyword(es, operand_end)
672
715
  ? -1
673
716
  : (cur_pos < 0 ? 0 : cur_pos);
674
717
  }
@@ -771,6 +814,8 @@ static int64_t look_ahead_for_operator(EStream *es, ExprState *e, uint32_t k) {
771
814
  }
772
815
  }
773
816
 
817
+ // Whether a "/" after `c` is a division. A word char counts as an operand
818
+ // here; is_regexp_start also tells an operand keyword from one.
774
819
  static bool can_follow_division(int32_t c) {
775
820
  if (is_word_code(c)) return true;
776
821
  switch (c) {
@@ -782,6 +827,24 @@ static bool can_follow_division(int32_t c) {
782
827
  }
783
828
  }
784
829
 
830
+ // isRegExpStart: whether a "/" (not a comment) at the logical position starts
831
+ // a regular expression rather than a division, judged by what precedes it.
832
+ // The cursor sees past the token start, as the parser reads the whole source.
833
+ static bool is_regexp_start(Scanner *s) {
834
+ Cursor *cur = &s->cur;
835
+ if (!is_word_code(cur->prev_nonws_char)) {
836
+ return !can_follow_division(cur->prev_nonws_char);
837
+ }
838
+ for (int i = 0; OPERAND_KEYWORDS[i]; i++) {
839
+ size_t len = strlen(OPERAND_KEYWORDS[i]);
840
+ if (cur->word_len == len &&
841
+ memcmp(cur->word, OPERAND_KEYWORDS[i], len) == 0) {
842
+ return true;
843
+ }
844
+ }
845
+ return false;
846
+ }
847
+
785
848
  static void es_consume_ws(EStream *es) {
786
849
  while (true) {
787
850
  int32_t c = es_peek(es, 0);
@@ -790,8 +853,11 @@ static void es_consume_ws(EStream *es) {
790
853
  }
791
854
  }
792
855
 
793
- // checkForOperators(parser, expression, eol)
794
- static bool check_for_operators(EStream *es, ExprState *e, bool eol) {
856
+ // checkForOperators(parser, expression, eol). Its look-ahead skips the
857
+ // whitespace from `first` chars ahead: 1 (past the current whitespace char),
858
+ // or 0 for a caller that has consumed the whitespace already.
859
+ static bool check_for_operators_from(EStream *es, ExprState *e, bool eol,
860
+ uint32_t first) {
795
861
  if (!e->cfg.operators) return false;
796
862
  Scanner *s = es->s;
797
863
 
@@ -802,7 +868,7 @@ static bool check_for_operators(EStream *es, ExprState *e, bool eol) {
802
868
 
803
869
  bool terminated_by_eol = e->cfg.terminated_by_eol || is_concise(s);
804
870
  if (!(terminated_by_eol && eol)) {
805
- uint32_t k = 1;
871
+ uint32_t k = first;
806
872
  while (true) {
807
873
  int32_t c = es_peek(es, k);
808
874
  if (c >= 0 && (terminated_by_eol ? is_indent_code(c) : is_ws(c))) k++;
@@ -833,6 +899,10 @@ static bool check_for_operators(EStream *es, ExprState *e, bool eol) {
833
899
  return false;
834
900
  }
835
901
 
902
+ static bool check_for_operators(EStream *es, ExprState *e, bool eol) {
903
+ return check_for_operators_from(es, e, eol, 1);
904
+ }
905
+
836
906
  static bool scan_expr_inner(EStream *es, ExprState *e);
837
907
 
838
908
  // STRING state
@@ -928,6 +998,91 @@ static bool sub_scan_regex(EStream *es) {
928
998
  }
929
999
  }
930
1000
 
1001
+ // isEndedByCloser: an expression a closer ends (`(…)`, `{…}`, `<…>`, `|…|`,
1002
+ // `${…}`) is unfinished at EOF, in concise mode too. The tag params pieces
1003
+ // are all parts of the parser's one `|…|` expression.
1004
+ static bool is_ended_by_closer(uint8_t term) {
1005
+ switch (term) {
1006
+ case TERM_CLOSE_PAREN: case TERM_CLOSE_CURLY: case TERM_CLOSE_ANGLE:
1007
+ case TERM_PIPE: case TERM_PARAM_PATTERN: case TERM_PARAM_TYPE:
1008
+ case TERM_PARAM_DEFAULT:
1009
+ return true;
1010
+ default:
1011
+ return false;
1012
+ }
1013
+ }
1014
+
1015
+ static bool is_attr_name_term(uint8_t term) {
1016
+ return term == TERM_HTML_ATTR_NAME || term == TERM_CONCISE_ATTR_NAME ||
1017
+ term == TERM_CONCISE_GROUPED_ATTR_NAME;
1018
+ }
1019
+
1020
+ // isOnlyComments: whether the expression so far (from its first char) holds
1021
+ // nothing but comments and whitespace, which cannot end a whitespace
1022
+ // terminated value: `x=/* note */ 1` holds `1`. The expression scan consumed
1023
+ // any comment here whole, so each one closes within the range.
1024
+ static bool is_only_comments(EStream *es, ExprState *e) {
1025
+ // A resumed token (a tag var type) is not where the expression starts.
1026
+ if (e->cfg.resume_lookbehind) return false;
1027
+ Scanner *s = es->s;
1028
+ uint32_t end = s->buf_len;
1029
+ uint32_t i = es->start;
1030
+ if (i >= end || s->buf[i] != '/') return false;
1031
+ while (i < end) {
1032
+ int32_t c = (int32_t)(uint8_t)s->buf[i];
1033
+ if (is_ws(c)) {
1034
+ i++;
1035
+ } else if (c != '/' || i + 1 >= end) {
1036
+ return false;
1037
+ } else if (s->buf[i + 1] == '*') {
1038
+ i += 2;
1039
+ while (i + 1 < end && !(s->buf[i] == '*' && s->buf[i + 1] == '/')) i++;
1040
+ if (i + 1 >= end) return false;
1041
+ i += 2;
1042
+ } else if (s->buf[i + 1] == '/') {
1043
+ i += 2;
1044
+ while (i < end && !is_line((int32_t)(uint8_t)s->buf[i])) i++;
1045
+ } else {
1046
+ return false;
1047
+ }
1048
+ }
1049
+ return true;
1050
+ }
1051
+
1052
+ // Whitespace after only comments, when a value follows (eg `x=/* note */ 1`),
1053
+ // does not end the value.
1054
+ // The parser skips it a run at a time, looking ahead for an operator at each
1055
+ // step (but the newline right after a line comment); no operator ends a
1056
+ // comment, so each look-ahead reaches the same char after the whitespace.
1057
+ // Consuming all of it first and looking ahead once from there is the same
1058
+ // scan, and keeps the token end in step (it cannot be marked behind the
1059
+ // look-ahead). In concise mode a newline still ends the value.
1060
+ static void skip_comment_only_ws(EStream *es, ExprState *e) {
1061
+ bool concise = is_concise(es->s);
1062
+ bool look = false;
1063
+ for (;;) {
1064
+ int32_t c = es_peek(es, 0);
1065
+ if (c < 0 || !is_ws(c) || (concise && is_line(c))) break;
1066
+ if (!is_line(c) || !e->was_comment) look = true;
1067
+ if (is_line(c)) e->was_comment = false;
1068
+ es_next(es);
1069
+ }
1070
+ if (look) check_for_operators_from(es, e, false, 0);
1071
+ }
1072
+
1073
+ // Whether a value follows the whitespace after only comments, which then
1074
+ // continues the expression (`x=/* note */ 1`). Where the value would end after
1075
+ // the whitespace anyway (`a=/* c */ >`, EOF, or a concise newline) it ends
1076
+ // before the whitespace, as any value does.
1077
+ static bool value_follows_comments(EStream *es, ExprState *e) {
1078
+ bool concise = is_concise(es->s);
1079
+ uint32_t k = 0;
1080
+ int32_t c;
1081
+ while ((c = es_peek(es, k)) >= 0 && is_ws(c) && !(concise && is_line(c))) k++;
1082
+ if (c < 0 || is_line(c)) return false;
1083
+ return !should_terminate(es, e, c, k);
1084
+ }
1085
+
931
1086
  // The core of EXPRESSION.parse. On success the logical position sits at the
932
1087
  // terminator (not consumed). Returns false on parser-error paths.
933
1088
  static bool scan_expr_inner(EStream *es, ExprState *e) {
@@ -938,9 +1093,13 @@ static bool scan_expr_inner(EStream *es, ExprState *e) {
938
1093
 
939
1094
  if (code < 0) {
940
1095
  // EOF
941
- if (!e->group_len && (is_concise(s) || e->cfg.terminated_by_eol)) {
1096
+ if (!e->group_len && (is_concise(s) || e->cfg.terminated_by_eol) &&
1097
+ !is_ended_by_closer(e->cfg.terminator)) {
942
1098
  return true;
943
1099
  }
1100
+ // An attribute name, eg one still being typed in `<div foo`, is
1101
+ // reported; the open tag then fails at the EOF.
1102
+ if (!e->group_len && is_attr_name_term(e->cfg.terminator)) return true;
944
1103
  return false;
945
1104
  }
946
1105
 
@@ -950,6 +1109,15 @@ static bool scan_expr_inner(EStream *es, ExprState *e) {
950
1109
  uint32_t before = s->buf_len;
951
1110
  int32_t after_newline = es_peek(es, len);
952
1111
 
1112
+ // In html mode a newline after only comments does not end the value
1113
+ // when one follows.
1114
+ if (!e->group_len && e->cfg.terminated_by_whitespace &&
1115
+ !is_concise(s) && is_only_comments(es, e) &&
1116
+ value_follows_comments(es, e)) {
1117
+ skip_comment_only_ws(es, e);
1118
+ continue;
1119
+ }
1120
+
953
1121
  if (!e->group_len &&
954
1122
  (e->cfg.terminated_by_eol || e->cfg.terminated_by_whitespace) &&
955
1123
  (e->was_comment || !check_for_operators(es, e, true)) &&
@@ -972,6 +1140,10 @@ static bool scan_expr_inner(EStream *es, ExprState *e) {
972
1140
 
973
1141
  if (!e->group_len) {
974
1142
  if (e->cfg.terminated_by_whitespace && is_ws(code)) {
1143
+ if (is_only_comments(es, e) && value_follows_comments(es, e)) {
1144
+ skip_comment_only_ws(es, e);
1145
+ continue;
1146
+ }
975
1147
  if (!check_for_operators(es, e, false)) return true;
976
1148
  continue;
977
1149
  }
@@ -1085,7 +1257,7 @@ static bool scan_expr_inner(EStream *es, ExprState *e) {
1085
1257
  break;
1086
1258
  }
1087
1259
  default:
1088
- if (can_follow_division(es_prev_nonws(es))) {
1260
+ if (!is_regexp_start(s)) {
1089
1261
  es_next(es);
1090
1262
  es_consume_ws(es);
1091
1263
  continue;
@@ -1220,14 +1392,34 @@ static bool es_consume_block_comment(EStream *es) {
1220
1392
  }
1221
1393
  }
1222
1394
 
1395
+ // Consume a "<!--" comment through its closing "-->"; false at EOF
1396
+ // (MALFORMED_COMMENT). It ends at the first "-->", so a "->" inside it (eg
1397
+ // "a->b") is content; the last hyphen of "<!--" counts, so "<!--->" closes.
1398
+ static bool es_consume_html_comment(EStream *es) {
1399
+ for (int i = 0; i < 4; i++) es_next_mark(es); // <!--
1400
+ uint32_t hyphens = 1;
1401
+ while (true) {
1402
+ int32_t c = es_peek(es, 0);
1403
+ if (c < 0) return false;
1404
+ es_next_mark(es);
1405
+ if (c == '>' && hyphens >= 2) return true;
1406
+ hyphens = c == '-' ? hyphens + 1 : 0;
1407
+ }
1408
+ }
1409
+
1223
1410
  // --------------------------------------------------------------------------
1224
1411
  // Tag bookkeeping
1225
1412
  // --------------------------------------------------------------------------
1226
1413
 
1227
1414
  // Tag types follow the tags API (and the vscode tmLanguage grammar this
1228
1415
  // replaces): html void elements plus openTagOnly core tags are void;
1229
- // text:true core tags are text; statement:true core tags (and class, kept
1230
- // for class-API compatibility) are statements.
1416
+ // text:true html and core tags are text; statement:true core tags (and
1417
+ // class, kept for class-API compatibility) are statements.
1418
+ static const char *const TEXT_TAGS[] = {
1419
+ "script", "style", "textarea", "html-script",
1420
+ "html-style", "html-comment", NULL,
1421
+ };
1422
+
1231
1423
  static uint8_t classify_tag_name(Scanner *s) {
1232
1424
  static const char *const VOID_TAGS[] = {
1233
1425
  "area", "base", "br", "col", "hr", "embed", "img",
@@ -1235,9 +1427,6 @@ static uint8_t classify_tag_name(Scanner *s) {
1235
1427
  "const", "debug", "id", "let", "lifecycle",
1236
1428
  "log", "return", NULL,
1237
1429
  };
1238
- static const char *const TEXT_TAGS[] = {
1239
- "script", "style", "html-script", "html-style", "html-comment", NULL,
1240
- };
1241
1430
  static const char *const STATEMENT_TAGS[] = {
1242
1431
  "import", "export", "static", "class", "server", "client", NULL,
1243
1432
  };
@@ -1923,26 +2112,138 @@ static enum PhResult check_placeholder(Scanner *s, EStream *es,
1923
2112
  return valid[ESCAPE] ? PH_BOUNDARY : PH_NO;
1924
2113
  }
1925
2114
 
2115
+ // Whether the text tag's closing name and ">" (or just ">", for "</>") are
2116
+ // `k` chars ahead (peeks only). Only a TEXT_TAGS name opens parsed text, so
2117
+ // it is compared char by char: a mismatch stops the lookahead, which then
2118
+ // queued just a name prefix that no token boundary falls within, so the
2119
+ // token end keeps being marked past it (a hash compare would queue a whole
2120
+ // name's worth of chars, maybe the start of the real closing tag).
2121
+ static bool close_tag_name_ahead(TagFrame *t, EStream *es, uint32_t k) {
2122
+ if (es_peek(es, k) == '>') return true; // </>
2123
+ for (int i = 0; TEXT_TAGS[i]; i++) {
2124
+ uint32_t len = (uint32_t)strlen(TEXT_TAGS[i]);
2125
+ if (len != t->name_len || hash_str(TEXT_TAGS[i]) != t->name_hash) {
2126
+ continue;
2127
+ }
2128
+ for (uint32_t j = 0; j < len; j++) {
2129
+ if (es_peek(es, k + j) != (int32_t)TEXT_TAGS[i][j]) return false;
2130
+ }
2131
+ return es_peek(es, k + len) == '>';
2132
+ }
2133
+ return false;
2134
+ }
2135
+
1926
2136
  // Try to match the parsed-text closing tag at the current position (peeks
1927
2137
  // only). checkForClosingTag in CLOSE_TAG.ts.
1928
2138
  static bool peek_parsed_close_tag(Scanner *s, EStream *es) {
1929
2139
  TagFrame *t = top_tag(s);
1930
2140
  if (!t) return false;
1931
2141
  if (es_peek(es, 0) != '<' || es_peek(es, 1) != '/') return false;
1932
- if (es_peek(es, 2) == '>') return true; // </>
1933
- uint32_t len = t->name_len;
1934
- uint32_t h = HASH_INIT;
1935
- for (uint32_t i = 0; i < len; i++) {
1936
- int32_t c = es_peek(es, 2 + i);
1937
- if (c < 0) return false;
1938
- h = hash_push(h, c);
2142
+ return close_tag_name_ahead(t, es, 2);
2143
+ }
2144
+
2145
+ // A backslash before one of these in a text body escapes it (eg CSS `.a\"`
2146
+ // is text, not a string start); before "<" or a newline it does not, so
2147
+ // "\</style>" still closes the tag and a delimited block still ends.
2148
+ static bool is_escapable_parsed_text_code(int32_t c) {
2149
+ return c == '"' || c == '\'' || c == '`' || c == '/' || c == '\\';
2150
+ }
2151
+
2152
+ // In a parsed string any char is escaped (eg the quote of `"a\"b"`).
2153
+ static bool is_escapable_parsed_string_code(int32_t c) { return c >= 0; }
2154
+
2155
+ // A backslash run no placeholder follows is text: its pairs are escaped
2156
+ // backslashes and an odd one out escapes the next char when `escapable`
2157
+ // accepts it. check_placeholder has consumed the run when it started the
2158
+ // token (PH_CONSUMED, so the buffer holds just the run), otherwise nothing.
2159
+ static void consume_escape(EStream *es, enum PhResult r,
2160
+ bool (*escapable)(int32_t)) {
2161
+ uint32_t run = 0;
2162
+ if (r == PH_CONSUMED) {
2163
+ run = es->s->buf_len;
2164
+ } else {
2165
+ while (es_peek(es, 0) == '\\') {
2166
+ es_next_mark(es);
2167
+ run++;
2168
+ }
2169
+ }
2170
+ if (run % 2 && escapable(es_peek(es, 0))) es_next_mark(es);
2171
+ }
2172
+
2173
+ // skipRegExp in PARSED_TEXT_CONTENT: the length of the rest of a regular
2174
+ // expression whose "/" was just consumed, through its closing "/" (peeks
2175
+ // only), or 0 when no "/" closes it on its line before a "</", which may be
2176
+ // the closing tag, or a placeholder, which a CSS body's `url(/${dir}/a.png)`
2177
+ // must keep.
2178
+ static uint32_t regexp_rest_len(EStream *es) {
2179
+ bool in_charset = false;
2180
+ for (uint32_t k = 0;; k++) {
2181
+ int32_t c = es_peek(es, k);
2182
+ switch (c) {
2183
+ case -1:
2184
+ case '\n':
2185
+ case '\r':
2186
+ return 0;
2187
+ case '\\':
2188
+ // the escaped char, which still cannot be a line terminator
2189
+ c = es_peek(es, ++k);
2190
+ if (c < 0 || is_line(c)) return 0;
2191
+ break;
2192
+ case '<':
2193
+ if (es_peek(es, k + 1) == '/') return 0;
2194
+ break;
2195
+ case '$':
2196
+ if (es_peek(es, k + 1) == '{') return 0; // ${
2197
+ if (es_peek(es, k + 1) == '!' && es_peek(es, k + 2) == '{') {
2198
+ return 0; // $!{
2199
+ }
2200
+ break;
2201
+ case '[':
2202
+ in_charset = true;
2203
+ break;
2204
+ case ']':
2205
+ in_charset = false;
2206
+ break;
2207
+ case '/':
2208
+ if (!in_charset) return k + 1;
2209
+ break;
2210
+ }
2211
+ }
2212
+ }
2213
+
2214
+ // The rest of a JS comment in a text body whose "/" was just consumed, if the
2215
+ // next char makes one: a "//" comment is text, but the closing tag still ends
2216
+ // it; a "/*" comment is consumed blindly (no placeholders, no close tags,
2217
+ // delimiters not considered). False at EOF inside "/*" (MALFORMED_COMMENT).
2218
+ static bool consume_parsed_comment_rest(Scanner *s, EStream *es) {
2219
+ int32_t c = es_peek(es, 0);
2220
+ if (c == '/') {
2221
+ es_next_mark(es);
2222
+ while (true) {
2223
+ int32_t c2 = es_peek(es, 0);
2224
+ if (c2 < 0 || is_line(c2)) break;
2225
+ if (c2 == '<' && !is_concise(s) && peek_parsed_close_tag(s, es)) break;
2226
+ es_next_mark(es);
2227
+ }
2228
+ } else if (c == '*') {
2229
+ es_next_mark(es);
2230
+ while (true) {
2231
+ int32_t c2 = es_peek(es, 0);
2232
+ if (c2 < 0) return false;
2233
+ if (c2 == '*' && es_peek(es, 1) == '/') {
2234
+ es_next_mark(es);
2235
+ es_next_mark(es);
2236
+ break;
2237
+ }
2238
+ es_next_mark(es);
2239
+ }
1939
2240
  }
1940
- return h == t->name_hash && es_peek(es, 2 + len) == '>';
2241
+ return true;
1941
2242
  }
1942
2243
 
1943
2244
  // Scan one token of html / parsed-text content (or hand off to structure).
1944
- static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
1945
- TSSymbol *result) {
2245
+ static bool scan_frame_content_token(Scanner *s, TSLexer *lexer,
2246
+ const bool *valid, TSSymbol *result) {
1946
2247
  ContentFrame *f = top_content(s);
1947
2248
  bool parsed = f->content == CONTENT_PARSED_TEXT;
1948
2249
  bool delim = f->delimiter_len > 0;
@@ -2075,6 +2376,10 @@ static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
2075
2376
  if (!handled) *result = TEXT;
2076
2377
  return *result != TEXT || s->buf_len > 0;
2077
2378
  }
2379
+ if (c == '\\' && !handled) {
2380
+ consume_escape(&es, r, is_escapable_parsed_string_code);
2381
+ continue;
2382
+ }
2078
2383
  if (r == PH_NO) es_next_mark(&es);
2079
2384
  continue;
2080
2385
  }
@@ -2307,32 +2612,18 @@ static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
2307
2612
  es_next_mark(&es); // <
2308
2613
  es_next_mark(&es); // /
2309
2614
  TagFrame *t = top_tag(s);
2310
- bool match = false;
2311
- if (t) {
2312
- if (es_peek(&es, 0) == '>') {
2313
- match = true; // </>
2314
- } else {
2315
- uint32_t len = t->name_len;
2316
- uint32_t h = HASH_INIT;
2317
- bool ok = true;
2318
- for (uint32_t i = 0; i < len; i++) {
2319
- int32_t ci = es_peek(&es, i);
2320
- if (ci < 0) {
2321
- ok = false;
2322
- break;
2323
- }
2324
- h = hash_push(h, ci);
2325
- }
2326
- match = ok && h == t->name_hash &&
2327
- es_peek(&es, len) == '>';
2328
- }
2329
- }
2330
- if (match) {
2615
+ if (t && close_tag_name_ahead(t, &es, 0)) {
2331
2616
  es_free(&es);
2332
2617
  *result = CLOSE_TAG_START;
2333
2618
  return valid[CLOSE_TAG_START];
2334
2619
  }
2335
- continue; // "</" is plain text
2620
+ // "</" is plain text. The parser reads its "/" afresh, so it may
2621
+ // start a comment (never a regular expression, after "<").
2622
+ if (!consume_parsed_comment_rest(s, &es)) {
2623
+ es_free(&es);
2624
+ return false; // MALFORMED_COMMENT
2625
+ }
2626
+ continue;
2336
2627
  }
2337
2628
  es_next_mark(&es);
2338
2629
  continue;
@@ -2342,26 +2633,11 @@ static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
2342
2633
 
2343
2634
  if (n1 == '!') {
2344
2635
  if (es_peek(&es, 2) == '-' && es_peek(&es, 3) == '-') {
2345
- // <!-- ... -> (the comment ends at the first "->")
2636
+ // <!-- ... -->
2346
2637
  EMIT_TEXT_OR({
2347
- es_next_mark(&es);
2348
- es_next_mark(&es);
2349
- es_next_mark(&es);
2350
- es_next_mark(&es);
2351
- while (true) {
2352
- int32_t c2 = es_peek(&es, 0);
2353
- if (c2 < 0) {
2354
- es_free(&es);
2355
- return false; // MALFORMED_COMMENT
2356
- }
2357
- if (c2 == '-' && es_peek(&es, 1) == '>') {
2358
- es_next_mark(&es);
2359
- es_next_mark(&es);
2360
- break;
2361
- }
2362
- es_next_mark(&es);
2363
- }
2638
+ bool comment_ok = es_consume_html_comment(&es);
2364
2639
  es_free(&es);
2640
+ if (!comment_ok) return false; // MALFORMED_COMMENT
2365
2641
  *result = HTML_COMMENT;
2366
2642
  return valid[HTML_COMMENT];
2367
2643
  });
@@ -2487,41 +2763,21 @@ static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
2487
2763
  if (c == '/') {
2488
2764
  int32_t n1 = es_peek(&es, 1);
2489
2765
  if (parsed) {
2490
- if (n1 == '/') {
2491
- // JS line comment within parsed text: part of the text, but the
2492
- // closing tag is still recognized inside it.
2493
- es_next_mark(&es);
2494
- es_next_mark(&es);
2495
- while (true) {
2496
- int32_t c2 = es_peek(&es, 0);
2497
- if (c2 < 0 || is_line(c2)) break;
2498
- if (c2 == '<' && !is_concise(s) && peek_parsed_close_tag(s, &es))
2499
- break;
2500
- es_next_mark(&es);
2501
- }
2502
- continue;
2766
+ // A comment is text; otherwise skip a regular expression whole, so a
2767
+ // quote or "//" in it is not lexed (a "/" that cannot close one on
2768
+ // its line stays text). The "/" goes first, so a token ended within
2769
+ // the regular expression lookahead still holds it.
2770
+ bool regexp = n1 != '/' && n1 != '*' && is_regexp_start(s);
2771
+ es_next_mark(&es);
2772
+ if (!consume_parsed_comment_rest(s, &es)) {
2773
+ es_free(&es);
2774
+ return false; // MALFORMED_COMMENT
2503
2775
  }
2504
- if (n1 == '*') {
2505
- // JS block comment: consumed blindly (no placeholders, no close
2506
- // tags, delimiters not considered).
2507
- es_next_mark(&es);
2508
- es_next_mark(&es);
2509
- while (true) {
2510
- int32_t c2 = es_peek(&es, 0);
2511
- if (c2 < 0) {
2512
- es_free(&es);
2513
- return false; // MALFORMED_COMMENT
2514
- }
2515
- if (c2 == '*' && es_peek(&es, 1) == '/') {
2516
- es_next_mark(&es);
2517
- es_next_mark(&es);
2518
- break;
2519
- }
2776
+ if (regexp) {
2777
+ for (uint32_t i = regexp_rest_len(&es); i > 0; i--) {
2520
2778
  es_next_mark(&es);
2521
2779
  }
2522
- continue;
2523
2780
  }
2524
- es_next_mark(&es);
2525
2781
  continue;
2526
2782
  }
2527
2783
 
@@ -2570,6 +2826,10 @@ static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
2570
2826
  if (!handled) *result = TEXT;
2571
2827
  return *result != TEXT || s->buf_len > 0;
2572
2828
  }
2829
+ if (parsed && c == '\\' && !handled) {
2830
+ consume_escape(&es, r, is_escapable_parsed_text_code);
2831
+ continue;
2832
+ }
2573
2833
  if (r == PH_NO) es_next_mark(&es);
2574
2834
  continue;
2575
2835
  }
@@ -2579,6 +2839,20 @@ static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
2579
2839
  #undef EMIT_TEXT_OR
2580
2840
  }
2581
2841
 
2842
+ static bool scan_frame_content(Scanner *s, TSLexer *lexer, const bool *valid,
2843
+ TSSymbol *result) {
2844
+ ContentFrame *f = top_content(s);
2845
+ uint8_t content_len = s->content_len;
2846
+ if (!scan_frame_content_token(s, lexer, valid, result)) return false;
2847
+ // A token can end before chars its lookahead went on to lex (the end
2848
+ // cannot be marked behind a peek; the next scan lexes them again), so a
2849
+ // string they opened or closed must not outlive it.
2850
+ if (s->content_len == content_len && top_content(s) == f) {
2851
+ f->pstring_quote = s->saved_pstring_quote;
2852
+ }
2853
+ return true;
2854
+ }
2855
+
2582
2856
  // --------------------------------------------------------------------------
2583
2857
  // Concise mode content (CONCISE_HTML_CONTENT)
2584
2858
  // --------------------------------------------------------------------------
@@ -2639,21 +2913,9 @@ static bool scan_concise(Scanner *s, TSLexer *lexer, const bool *valid,
2639
2913
  if (c == '<') {
2640
2914
  if (es_peek(&es, 1) == '!' && es_peek(&es, 2) == '-' &&
2641
2915
  es_peek(&es, 3) == '-') {
2642
- for (int i = 0; i < 4; i++) es_next_mark(&es);
2643
- while (true) {
2644
- int32_t c2 = es_peek(&es, 0);
2645
- if (c2 < 0) {
2646
- es_free(&es);
2647
- return false; // MALFORMED_COMMENT
2648
- }
2649
- if (c2 == '-' && es_peek(&es, 1) == '>') {
2650
- es_next_mark(&es);
2651
- es_next_mark(&es);
2652
- break;
2653
- }
2654
- es_next_mark(&es);
2655
- }
2916
+ bool comment_ok = es_consume_html_comment(&es);
2656
2917
  es_free(&es);
2918
+ if (!comment_ok) return false; // MALFORMED_COMMENT
2657
2919
  *result = HTML_COMMENT;
2658
2920
  return valid[HTML_COMMENT];
2659
2921
  }
Binary file
package/tree-sitter.json CHANGED
@@ -16,7 +16,7 @@
16
16
  }
17
17
  ],
18
18
  "metadata": {
19
- "version": "0.4.0",
19
+ "version": "0.5.0",
20
20
  "license": "MIT",
21
21
  "description": "Tree-sitter grammar for the Marko templating language",
22
22
  "links": {