@marko/tree-sitter 0.4.1 → 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 +2 -2
- package/prebuilds/darwin-arm64/@marko+tree-sitter.node +0 -0
- package/prebuilds/darwin-x64/@marko+tree-sitter.node +0 -0
- package/prebuilds/linux-arm64/@marko+tree-sitter.node +0 -0
- package/prebuilds/linux-x64/@marko+tree-sitter.node +0 -0
- package/prebuilds/win32-x64/@marko+tree-sitter.node +0 -0
- package/src/parser.c +2 -2
- package/src/scanner.c +372 -110
- package/tree-sitter-marko.wasm +0 -0
- package/tree-sitter.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marko/tree-sitter",
|
|
3
|
-
"version": "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.
|
|
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",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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 =
|
|
7969
|
-
.patch_version =
|
|
7968
|
+
.minor_version = 5,
|
|
7969
|
+
.patch_version = 0,
|
|
7970
7970
|
},
|
|
7971
7971
|
};
|
|
7972
7972
|
return &language;
|
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
|
-
|
|
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!!`)
|
|
659
|
-
// (`typeof!a`, `a in!b`) it
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
|
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
|
|
1945
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// <!-- ...
|
|
2636
|
+
// <!-- ... -->
|
|
2346
2637
|
EMIT_TEXT_OR({
|
|
2347
|
-
|
|
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
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
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 (
|
|
2505
|
-
|
|
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
|
-
|
|
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
|
}
|
package/tree-sitter-marko.wasm
CHANGED
|
Binary file
|