@tsrx/core 0.1.7 → 0.1.9
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 +6 -6
- package/src/diagnostics.js +1 -0
- package/src/index.js +7 -0
- package/src/plugin.js +241 -116
- package/src/transform/jsx/index.js +177 -9
- package/types/jsx-platform.d.ts +2 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Core compiler infrastructure for TSRX syntax",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.9",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@types/estree-jsx": "^1.0.5",
|
|
65
65
|
"@types/estree": "^1.0.8",
|
|
66
66
|
"acorn": "^8.15.0",
|
|
67
|
-
"esrap": "^2.2.
|
|
67
|
+
"esrap": "^2.2.8",
|
|
68
68
|
"is-reference": "^3.0.3",
|
|
69
69
|
"magic-string": "^0.30.18",
|
|
70
70
|
"zimmerframe": "^1.1.2"
|
|
@@ -82,10 +82,10 @@
|
|
|
82
82
|
"vscode-languageserver-types": "^3.17.5",
|
|
83
83
|
"vue": "3.6.0-beta.10",
|
|
84
84
|
"vue-jsx-vapor": "^3.2.12",
|
|
85
|
-
"@tsrx/preact": "0.1.
|
|
86
|
-
"@tsrx/react": "0.2.
|
|
87
|
-
"@tsrx/solid": "0.1.
|
|
88
|
-
"@tsrx/vue": "0.1.
|
|
85
|
+
"@tsrx/preact": "0.1.9",
|
|
86
|
+
"@tsrx/react": "0.2.9",
|
|
87
|
+
"@tsrx/solid": "0.1.9",
|
|
88
|
+
"@tsrx/vue": "0.1.9"
|
|
89
89
|
},
|
|
90
90
|
"files": [
|
|
91
91
|
"src",
|
package/src/diagnostics.js
CHANGED
|
@@ -5,4 +5,5 @@ export const DIAGNOSTIC_CODES = {
|
|
|
5
5
|
UNCLOSED_TAG: 'tsrx-unclosed-tag',
|
|
6
6
|
MISMATCHED_CLOSING_TAG: 'tsrx-mismatched-closing-tag',
|
|
7
7
|
TEMPLATE_EXPRESSION_TRAILING_SEMICOLON: 'tsrx-template-expression-trailing-semicolon',
|
|
8
|
+
HTML_DIRECTIVE_AS_ATTRIBUTE_VALUE: 'tsrx-html-directive-as-attribute-value',
|
|
8
9
|
};
|
package/src/index.js
CHANGED
|
@@ -146,14 +146,21 @@ export {
|
|
|
146
146
|
clone_switch_helper_invocation as cloneSwitchHelperInvocation,
|
|
147
147
|
collect_param_bindings as collectParamBindings,
|
|
148
148
|
collect_statement_bindings as collectStatementBindings,
|
|
149
|
+
create_host_html_attribute as createHostHtmlAttribute,
|
|
150
|
+
create_host_html_conflict_error as createHostHtmlConflictError,
|
|
149
151
|
createJsxTransform,
|
|
150
152
|
CREATE_REF_PROP_INTERNAL_NAME,
|
|
151
153
|
extract_jsx_setup_declarations as extractJsxSetupDeclarations,
|
|
154
|
+
get_host_html_conflicting_attribute as getHostHtmlConflictingAttribute,
|
|
155
|
+
get_invalid_html_child_error_message as getInvalidHtmlChildErrorMessage,
|
|
156
|
+
is_component_like_element,
|
|
152
157
|
is_ref_prop_expression as isRefPropExpression,
|
|
153
158
|
MERGE_REFS_INTERNAL_NAME,
|
|
154
159
|
merge_duplicate_refs as mergeDuplicateRefs,
|
|
155
160
|
NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
|
|
156
161
|
plan_switch_lift as planSwitchLift,
|
|
162
|
+
recover_invalid_html_child as recoverInvalidHtmlChild,
|
|
163
|
+
rewrite_host_html_children as rewriteHostHtmlChildren,
|
|
157
164
|
return_value_body_to_expression as returnValueBodyToExpression,
|
|
158
165
|
rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
|
|
159
166
|
to_jsx_attribute as toJsxAttribute,
|
package/src/plugin.js
CHANGED
|
@@ -19,6 +19,41 @@ import { DIAGNOSTIC_CODES } from './diagnostics.js';
|
|
|
19
19
|
|
|
20
20
|
const JSX_EXPRESSION_VALUE_ERROR =
|
|
21
21
|
'JSX elements cannot be used as expressions. Wrap JSX with `<>...</>` or `<tsx>...</tsx>`, wrap TSRX templates with `<tsrx>...</tsrx>`, or use elements as statements within a component.';
|
|
22
|
+
const HTML_ATTRIBUTE_VALUE_ERROR =
|
|
23
|
+
'`{html ...}` is not supported as an attribute value. Use a string literal or expression without `html`.';
|
|
24
|
+
|
|
25
|
+
const CharCode = Object.freeze({
|
|
26
|
+
tab: 9,
|
|
27
|
+
lineFeed: 10,
|
|
28
|
+
carriageReturn: 13,
|
|
29
|
+
space: 32,
|
|
30
|
+
doubleQuote: 34,
|
|
31
|
+
dollar: 36,
|
|
32
|
+
ampersand: 38,
|
|
33
|
+
singleQuote: 39,
|
|
34
|
+
openParen: 40,
|
|
35
|
+
closeParen: 41,
|
|
36
|
+
asterisk: 42,
|
|
37
|
+
slash: 47,
|
|
38
|
+
colon: 58,
|
|
39
|
+
semicolon: 59,
|
|
40
|
+
lessThan: 60,
|
|
41
|
+
equals: 61,
|
|
42
|
+
greaterThan: 62,
|
|
43
|
+
at: 64,
|
|
44
|
+
digit0: 48,
|
|
45
|
+
digit9: 57,
|
|
46
|
+
uppercaseA: 65,
|
|
47
|
+
uppercaseZ: 90,
|
|
48
|
+
openBracket: 91,
|
|
49
|
+
backslash: 92,
|
|
50
|
+
underscore: 95,
|
|
51
|
+
backtick: 96,
|
|
52
|
+
lowercaseA: 97,
|
|
53
|
+
lowercaseZ: 122,
|
|
54
|
+
openBrace: 123,
|
|
55
|
+
closeBrace: 125,
|
|
56
|
+
});
|
|
22
57
|
|
|
23
58
|
/** @type {WeakMap<Record<string, boolean>, Map<string, number>>} */
|
|
24
59
|
const argument_clash_first_positions = new WeakMap();
|
|
@@ -58,7 +93,13 @@ function get_argument_clash_reported_names(check_clashes) {
|
|
|
58
93
|
function skip_whitespace_from(input, i) {
|
|
59
94
|
while (i < input.length) {
|
|
60
95
|
const ch = input.charCodeAt(i);
|
|
61
|
-
if (
|
|
96
|
+
if (
|
|
97
|
+
ch !== CharCode.space &&
|
|
98
|
+
ch !== CharCode.tab &&
|
|
99
|
+
ch !== CharCode.lineFeed &&
|
|
100
|
+
ch !== CharCode.carriageReturn
|
|
101
|
+
)
|
|
102
|
+
break;
|
|
62
103
|
i++;
|
|
63
104
|
}
|
|
64
105
|
return i;
|
|
@@ -75,7 +116,7 @@ function skip_string_from(input, i, quote) {
|
|
|
75
116
|
while (i < input.length) {
|
|
76
117
|
const ch = input.charCodeAt(i);
|
|
77
118
|
i++;
|
|
78
|
-
if (ch ===
|
|
119
|
+
if (ch === CharCode.backslash)
|
|
79
120
|
i++; // backslash escape
|
|
80
121
|
else if (ch === quote) return i;
|
|
81
122
|
}
|
|
@@ -95,7 +136,7 @@ function scan_balanced_from(input, i, open, close) {
|
|
|
95
136
|
i++;
|
|
96
137
|
while (i < input.length) {
|
|
97
138
|
const ch = input.charCodeAt(i);
|
|
98
|
-
if (ch ===
|
|
139
|
+
if (ch === CharCode.doubleQuote || ch === CharCode.singleQuote || ch === CharCode.backtick) {
|
|
99
140
|
i = skip_string_from(input, i, ch);
|
|
100
141
|
continue;
|
|
101
142
|
}
|
|
@@ -114,47 +155,50 @@ function scan_balanced_from(input, i, open, close) {
|
|
|
114
155
|
* @param {number} pos
|
|
115
156
|
*/
|
|
116
157
|
function looks_like_generic_arrow(input, pos) {
|
|
117
|
-
if (input.charCodeAt(pos) !==
|
|
158
|
+
if (input.charCodeAt(pos) !== CharCode.lessThan) return false;
|
|
118
159
|
|
|
119
160
|
// Match the angle brackets, skipping over string literals.
|
|
120
161
|
let i = pos + 1;
|
|
121
162
|
let depth = 1;
|
|
122
163
|
while (i < input.length) {
|
|
123
164
|
const ch = input.charCodeAt(i);
|
|
124
|
-
if (ch ===
|
|
165
|
+
if (ch === CharCode.doubleQuote || ch === CharCode.singleQuote || ch === CharCode.backtick) {
|
|
125
166
|
i = skip_string_from(input, i, ch);
|
|
126
167
|
continue;
|
|
127
168
|
}
|
|
128
|
-
if (ch ===
|
|
129
|
-
else if (ch ===
|
|
169
|
+
if (ch === CharCode.lessThan) depth++;
|
|
170
|
+
else if (ch === CharCode.greaterThan && --depth === 0) break;
|
|
130
171
|
i++;
|
|
131
172
|
}
|
|
132
173
|
if (depth !== 0) return false;
|
|
133
174
|
|
|
134
175
|
// `>` must be followed by `(...)`.
|
|
135
176
|
i = skip_whitespace_from(input, i + 1);
|
|
136
|
-
if (input.charCodeAt(i) !==
|
|
137
|
-
i = scan_balanced_from(input, i,
|
|
177
|
+
if (input.charCodeAt(i) !== CharCode.openParen) return false;
|
|
178
|
+
i = scan_balanced_from(input, i, CharCode.openParen, CharCode.closeParen);
|
|
138
179
|
if (i === -1) return false;
|
|
139
180
|
|
|
140
181
|
// Optional `: ReturnType` before `=>`.
|
|
141
182
|
i = skip_whitespace_from(input, i);
|
|
142
|
-
if (input.charCodeAt(i) ===
|
|
183
|
+
if (input.charCodeAt(i) === CharCode.colon) {
|
|
143
184
|
i++;
|
|
144
185
|
while (i < input.length) {
|
|
145
186
|
const ch = input.charCodeAt(i);
|
|
146
|
-
if (ch ===
|
|
187
|
+
if (ch === CharCode.doubleQuote || ch === CharCode.singleQuote || ch === CharCode.backtick) {
|
|
147
188
|
i = skip_string_from(input, i, ch);
|
|
148
189
|
continue;
|
|
149
190
|
}
|
|
150
|
-
if (ch ===
|
|
151
|
-
if (ch ===
|
|
191
|
+
if (ch === CharCode.equals && input.charCodeAt(i + 1) === CharCode.greaterThan) return true;
|
|
192
|
+
if (ch === CharCode.semicolon || ch === CharCode.openBrace || ch === CharCode.closeBrace)
|
|
193
|
+
return false;
|
|
152
194
|
i++;
|
|
153
195
|
}
|
|
154
196
|
return false;
|
|
155
197
|
}
|
|
156
198
|
|
|
157
|
-
return
|
|
199
|
+
return (
|
|
200
|
+
input.charCodeAt(i) === CharCode.equals && input.charCodeAt(i + 1) === CharCode.greaterThan
|
|
201
|
+
);
|
|
158
202
|
}
|
|
159
203
|
|
|
160
204
|
/**
|
|
@@ -176,7 +220,13 @@ function previous_word_before(input, pos) {
|
|
|
176
220
|
let i = pos - 1;
|
|
177
221
|
while (i >= 0) {
|
|
178
222
|
const ch = input.charCodeAt(i);
|
|
179
|
-
if (
|
|
223
|
+
if (
|
|
224
|
+
ch !== CharCode.space &&
|
|
225
|
+
ch !== CharCode.tab &&
|
|
226
|
+
ch !== CharCode.lineFeed &&
|
|
227
|
+
ch !== CharCode.carriageReturn
|
|
228
|
+
)
|
|
229
|
+
break;
|
|
180
230
|
i--;
|
|
181
231
|
}
|
|
182
232
|
const end = i + 1;
|
|
@@ -266,7 +316,12 @@ export function TSRXPlugin(config) {
|
|
|
266
316
|
let index = this.pos - 1;
|
|
267
317
|
while (index >= 0) {
|
|
268
318
|
const ch = this.input.charCodeAt(index);
|
|
269
|
-
if (
|
|
319
|
+
if (
|
|
320
|
+
ch !== CharCode.space &&
|
|
321
|
+
ch !== CharCode.tab &&
|
|
322
|
+
ch !== CharCode.lineFeed &&
|
|
323
|
+
ch !== CharCode.carriageReturn
|
|
324
|
+
) {
|
|
270
325
|
return ch;
|
|
271
326
|
}
|
|
272
327
|
index--;
|
|
@@ -454,7 +509,7 @@ export function TSRXPlugin(config) {
|
|
|
454
509
|
}
|
|
455
510
|
|
|
456
511
|
const after = this.input.charCodeAt(this.pos + 4);
|
|
457
|
-
return after ===
|
|
512
|
+
return after === CharCode.greaterThan;
|
|
458
513
|
}
|
|
459
514
|
|
|
460
515
|
#parseTsxIslandText() {
|
|
@@ -466,7 +521,7 @@ export function TSRXPlugin(config) {
|
|
|
466
521
|
const ch = this.input.charCodeAt(this.pos);
|
|
467
522
|
|
|
468
523
|
// Stop at opening tag, expression, or the component-closing brace
|
|
469
|
-
if (ch ===
|
|
524
|
+
if (ch === CharCode.lessThan || ch === CharCode.openBrace || ch === CharCode.closeBrace) {
|
|
470
525
|
break;
|
|
471
526
|
}
|
|
472
527
|
|
|
@@ -496,24 +551,27 @@ export function TSRXPlugin(config) {
|
|
|
496
551
|
// fragment props like `content={<></>}` still need the JSX context.
|
|
497
552
|
while (index < this.input.length) {
|
|
498
553
|
const ch = this.input.charCodeAt(index);
|
|
499
|
-
if (ch ===
|
|
554
|
+
if (ch === CharCode.space || ch === CharCode.tab) {
|
|
500
555
|
index++;
|
|
501
|
-
} else if (ch ===
|
|
556
|
+
} else if (ch === CharCode.lineFeed || ch === CharCode.carriageReturn) {
|
|
502
557
|
has_newline = true;
|
|
503
558
|
index++;
|
|
504
|
-
} else if (
|
|
559
|
+
} else if (
|
|
560
|
+
ch === CharCode.slash &&
|
|
561
|
+
this.input.charCodeAt(index + 1) === CharCode.asterisk
|
|
562
|
+
) {
|
|
505
563
|
const end = this.input.indexOf('*/', index + 2);
|
|
506
564
|
const comment_end = end === -1 ? this.input.length : end + 2;
|
|
507
565
|
if (this.input.slice(index, comment_end).match(regex_newline_characters)) {
|
|
508
566
|
has_newline = true;
|
|
509
567
|
}
|
|
510
568
|
index = comment_end;
|
|
511
|
-
} else if (ch ===
|
|
569
|
+
} else if (ch === CharCode.slash && this.input.charCodeAt(index + 1) === CharCode.slash) {
|
|
512
570
|
has_newline = true;
|
|
513
571
|
index += 2;
|
|
514
572
|
while (index < this.input.length) {
|
|
515
573
|
const comment_ch = this.input.charCodeAt(index);
|
|
516
|
-
if (comment_ch ===
|
|
574
|
+
if (comment_ch === CharCode.lineFeed || comment_ch === CharCode.carriageReturn) break;
|
|
517
575
|
index++;
|
|
518
576
|
}
|
|
519
577
|
} else {
|
|
@@ -521,7 +579,7 @@ export function TSRXPlugin(config) {
|
|
|
521
579
|
}
|
|
522
580
|
}
|
|
523
581
|
|
|
524
|
-
if (!has_newline || this.input.charCodeAt(index) !==
|
|
582
|
+
if (!has_newline || this.input.charCodeAt(index) !== CharCode.openBrace) {
|
|
525
583
|
return;
|
|
526
584
|
}
|
|
527
585
|
|
|
@@ -544,16 +602,24 @@ export function TSRXPlugin(config) {
|
|
|
544
602
|
#skipWhitespaceAndComments(index) {
|
|
545
603
|
while (index < this.input.length) {
|
|
546
604
|
const ch = this.input.charCodeAt(index);
|
|
547
|
-
if (
|
|
605
|
+
if (
|
|
606
|
+
ch === CharCode.space ||
|
|
607
|
+
ch === CharCode.tab ||
|
|
608
|
+
ch === CharCode.lineFeed ||
|
|
609
|
+
ch === CharCode.carriageReturn
|
|
610
|
+
) {
|
|
548
611
|
index++;
|
|
549
|
-
} else if (
|
|
612
|
+
} else if (
|
|
613
|
+
ch === CharCode.slash &&
|
|
614
|
+
this.input.charCodeAt(index + 1) === CharCode.asterisk
|
|
615
|
+
) {
|
|
550
616
|
const end = this.input.indexOf('*/', index + 2);
|
|
551
617
|
index = end === -1 ? this.input.length : end + 2;
|
|
552
|
-
} else if (ch ===
|
|
618
|
+
} else if (ch === CharCode.slash && this.input.charCodeAt(index + 1) === CharCode.slash) {
|
|
553
619
|
index += 2;
|
|
554
620
|
while (index < this.input.length) {
|
|
555
621
|
const comment_ch = this.input.charCodeAt(index);
|
|
556
|
-
if (comment_ch ===
|
|
622
|
+
if (comment_ch === CharCode.lineFeed || comment_ch === CharCode.carriageReturn) break;
|
|
557
623
|
index++;
|
|
558
624
|
}
|
|
559
625
|
} else {
|
|
@@ -569,7 +635,7 @@ export function TSRXPlugin(config) {
|
|
|
569
635
|
let count = 0;
|
|
570
636
|
while (index < this.input.length) {
|
|
571
637
|
index = this.#skipWhitespaceAndComments(index);
|
|
572
|
-
if (this.input.charCodeAt(index) !==
|
|
638
|
+
if (this.input.charCodeAt(index) !== CharCode.closeBrace) break;
|
|
573
639
|
count++;
|
|
574
640
|
index++;
|
|
575
641
|
}
|
|
@@ -701,11 +767,11 @@ export function TSRXPlugin(config) {
|
|
|
701
767
|
const prev = this.#previousNonWhitespaceChar();
|
|
702
768
|
return (
|
|
703
769
|
prev === null ||
|
|
704
|
-
prev ===
|
|
705
|
-
prev ===
|
|
706
|
-
prev ===
|
|
707
|
-
(prev ===
|
|
708
|
-
prev ===
|
|
770
|
+
prev === CharCode.doubleQuote ||
|
|
771
|
+
prev === CharCode.semicolon ||
|
|
772
|
+
prev === CharCode.greaterThan ||
|
|
773
|
+
(prev === CharCode.openBrace && this.#allowDoubleQuotedTextChildAfterBrace) ||
|
|
774
|
+
prev === CharCode.closeBrace
|
|
709
775
|
);
|
|
710
776
|
}
|
|
711
777
|
|
|
@@ -718,13 +784,13 @@ export function TSRXPlugin(config) {
|
|
|
718
784
|
while (this.pos < this.input.length) {
|
|
719
785
|
const ch = this.input.charCodeAt(this.pos);
|
|
720
786
|
|
|
721
|
-
if (ch ===
|
|
787
|
+
if (ch === CharCode.doubleQuote) {
|
|
722
788
|
out += this.input.slice(chunkStart, this.pos);
|
|
723
789
|
this.pos++;
|
|
724
790
|
return this.finishToken(tt.string, out);
|
|
725
791
|
}
|
|
726
792
|
|
|
727
|
-
if (ch ===
|
|
793
|
+
if (ch === CharCode.ampersand) {
|
|
728
794
|
out += this.input.slice(chunkStart, this.pos);
|
|
729
795
|
out += this.jsx_readEntity();
|
|
730
796
|
chunkStart = this.pos;
|
|
@@ -1050,7 +1116,7 @@ export function TSRXPlugin(config) {
|
|
|
1050
1116
|
* @type {Parse.Parser['readToken']}
|
|
1051
1117
|
*/
|
|
1052
1118
|
readToken(code) {
|
|
1053
|
-
if (code ===
|
|
1119
|
+
if (code === CharCode.lessThan && looks_like_generic_arrow(this.input, this.pos)) {
|
|
1054
1120
|
++this.pos;
|
|
1055
1121
|
return this.finishToken(tt.relational, '<');
|
|
1056
1122
|
}
|
|
@@ -1062,7 +1128,19 @@ export function TSRXPlugin(config) {
|
|
|
1062
1128
|
* @type {Parse.Parser['getTokenFromCode']}
|
|
1063
1129
|
*/
|
|
1064
1130
|
getTokenFromCode(code) {
|
|
1065
|
-
|
|
1131
|
+
// Callback props that return `<tsrx>...</tsrx>` without a semicolon can
|
|
1132
|
+
// leave the attribute expression context above the still-open tag. Drop
|
|
1133
|
+
// it before tokenizing `/>`, otherwise Acorn treats `/` as a regexp.
|
|
1134
|
+
if (
|
|
1135
|
+
code === CharCode.slash &&
|
|
1136
|
+
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan &&
|
|
1137
|
+
this.curContext() === b_expr &&
|
|
1138
|
+
this.context[this.context.length - 2] === tstc.tc_oTag
|
|
1139
|
+
) {
|
|
1140
|
+
this.context.pop();
|
|
1141
|
+
this.exprAllowed = false;
|
|
1142
|
+
}
|
|
1143
|
+
if (code === CharCode.doubleQuote) {
|
|
1066
1144
|
const is_double_quoted_text_child = this.#isDoubleQuotedTextChildStart();
|
|
1067
1145
|
this.#allowDoubleQuotedTextChildAfterBrace = false;
|
|
1068
1146
|
if (is_double_quoted_text_child) {
|
|
@@ -1072,11 +1150,11 @@ export function TSRXPlugin(config) {
|
|
|
1072
1150
|
this.#allowDoubleQuotedTextChildAfterBrace = false;
|
|
1073
1151
|
}
|
|
1074
1152
|
|
|
1075
|
-
if (code !==
|
|
1153
|
+
if (code !== CharCode.lessThan) {
|
|
1076
1154
|
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
1077
1155
|
}
|
|
1078
1156
|
|
|
1079
|
-
if (code ===
|
|
1157
|
+
if (code === CharCode.lessThan) {
|
|
1080
1158
|
// < character
|
|
1081
1159
|
const inComponent = this.#isInsideComponentTemplate();
|
|
1082
1160
|
/** @type {number | null} */
|
|
@@ -1093,7 +1171,7 @@ export function TSRXPlugin(config) {
|
|
|
1093
1171
|
// Skip whitespace backwards
|
|
1094
1172
|
while (lookback >= 0) {
|
|
1095
1173
|
const ch = this.input.charCodeAt(lookback);
|
|
1096
|
-
if (ch !==
|
|
1174
|
+
if (ch !== CharCode.space && ch !== CharCode.tab) break; // not space or tab
|
|
1097
1175
|
lookback--;
|
|
1098
1176
|
}
|
|
1099
1177
|
|
|
@@ -1105,12 +1183,12 @@ export function TSRXPlugin(config) {
|
|
|
1105
1183
|
// If preceded by identifier character (letter, digit, _, $) or closing paren,
|
|
1106
1184
|
// this is likely TypeScript generics, not JSX
|
|
1107
1185
|
const isIdentifierChar =
|
|
1108
|
-
(prevChar >=
|
|
1109
|
-
(prevChar >=
|
|
1110
|
-
(prevChar >=
|
|
1111
|
-
prevChar ===
|
|
1112
|
-
prevChar ===
|
|
1113
|
-
prevChar ===
|
|
1186
|
+
(prevChar >= CharCode.uppercaseA && prevChar <= CharCode.uppercaseZ) ||
|
|
1187
|
+
(prevChar >= CharCode.lowercaseA && prevChar <= CharCode.lowercaseZ) ||
|
|
1188
|
+
(prevChar >= CharCode.digit0 && prevChar <= CharCode.digit9) ||
|
|
1189
|
+
prevChar === CharCode.underscore ||
|
|
1190
|
+
prevChar === CharCode.dollar ||
|
|
1191
|
+
prevChar === CharCode.closeParen;
|
|
1114
1192
|
|
|
1115
1193
|
if (isIdentifierChar) {
|
|
1116
1194
|
return super.getTokenFromCode(code);
|
|
@@ -1125,23 +1203,26 @@ export function TSRXPlugin(config) {
|
|
|
1125
1203
|
const nextChar =
|
|
1126
1204
|
this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
|
|
1127
1205
|
const isWhitespaceAfterLt =
|
|
1128
|
-
nextChar ===
|
|
1206
|
+
nextChar === CharCode.space ||
|
|
1207
|
+
nextChar === CharCode.tab ||
|
|
1208
|
+
nextChar === CharCode.lineFeed ||
|
|
1209
|
+
nextChar === CharCode.carriageReturn;
|
|
1129
1210
|
const isTagLikeAfterLt =
|
|
1130
1211
|
!isWhitespaceAfterLt &&
|
|
1131
|
-
(nextChar ===
|
|
1132
|
-
nextChar ===
|
|
1133
|
-
nextChar ===
|
|
1134
|
-
nextChar ===
|
|
1135
|
-
nextChar ===
|
|
1136
|
-
(nextChar >=
|
|
1137
|
-
(nextChar >=
|
|
1212
|
+
(nextChar === CharCode.slash ||
|
|
1213
|
+
nextChar === CharCode.greaterThan ||
|
|
1214
|
+
nextChar === CharCode.at ||
|
|
1215
|
+
nextChar === CharCode.dollar ||
|
|
1216
|
+
nextChar === CharCode.underscore ||
|
|
1217
|
+
(nextChar >= CharCode.uppercaseA && nextChar <= CharCode.uppercaseZ) ||
|
|
1218
|
+
(nextChar >= CharCode.lowercaseA && nextChar <= CharCode.lowercaseZ));
|
|
1138
1219
|
const prevAllowsTagStart =
|
|
1139
1220
|
prevNonWhitespaceChar === null ||
|
|
1140
|
-
prevNonWhitespaceChar ===
|
|
1141
|
-
prevNonWhitespaceChar ===
|
|
1142
|
-
prevNonWhitespaceChar ===
|
|
1143
|
-
prevNonWhitespaceChar ===
|
|
1144
|
-
prevNonWhitespaceChar ===
|
|
1221
|
+
prevNonWhitespaceChar === CharCode.lineFeed || // '\n'
|
|
1222
|
+
prevNonWhitespaceChar === CharCode.carriageReturn || // '\r'
|
|
1223
|
+
prevNonWhitespaceChar === CharCode.openBrace ||
|
|
1224
|
+
prevNonWhitespaceChar === CharCode.closeBrace ||
|
|
1225
|
+
prevNonWhitespaceChar === CharCode.greaterThan;
|
|
1145
1226
|
|
|
1146
1227
|
if (!inComponent && prevAllowsTagStart && isTagLikeAfterLt) {
|
|
1147
1228
|
++this.pos;
|
|
@@ -1153,10 +1234,10 @@ export function TSRXPlugin(config) {
|
|
|
1153
1234
|
// a newline/indentation before the next '<'. This is important for inputs
|
|
1154
1235
|
// like `<div />` and `</div><style>...</style>` which Prettier formats.
|
|
1155
1236
|
if (
|
|
1156
|
-
(prevNonWhitespaceChar ===
|
|
1237
|
+
(prevNonWhitespaceChar === CharCode.doubleQuote &&
|
|
1157
1238
|
this.#allowTagStartAfterDoubleQuotedText) ||
|
|
1158
|
-
prevNonWhitespaceChar ===
|
|
1159
|
-
prevNonWhitespaceChar ===
|
|
1239
|
+
prevNonWhitespaceChar === CharCode.openBrace ||
|
|
1240
|
+
prevNonWhitespaceChar === CharCode.greaterThan
|
|
1160
1241
|
) {
|
|
1161
1242
|
if (!isWhitespaceAfterLt) {
|
|
1162
1243
|
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
@@ -1172,8 +1253,8 @@ export function TSRXPlugin(config) {
|
|
|
1172
1253
|
let lineStart = this.pos - 1;
|
|
1173
1254
|
while (
|
|
1174
1255
|
lineStart >= 0 &&
|
|
1175
|
-
this.input.charCodeAt(lineStart) !==
|
|
1176
|
-
this.input.charCodeAt(lineStart) !==
|
|
1256
|
+
this.input.charCodeAt(lineStart) !== CharCode.lineFeed &&
|
|
1257
|
+
this.input.charCodeAt(lineStart) !== CharCode.carriageReturn
|
|
1177
1258
|
) {
|
|
1178
1259
|
lineStart--;
|
|
1179
1260
|
}
|
|
@@ -1183,7 +1264,7 @@ export function TSRXPlugin(config) {
|
|
|
1183
1264
|
let allWhitespace = true;
|
|
1184
1265
|
for (let i = lineStart; i < this.pos; i++) {
|
|
1185
1266
|
const ch = this.input.charCodeAt(i);
|
|
1186
|
-
if (ch !==
|
|
1267
|
+
if (ch !== CharCode.space && ch !== CharCode.tab) {
|
|
1187
1268
|
allWhitespace = false;
|
|
1188
1269
|
break;
|
|
1189
1270
|
}
|
|
@@ -1205,7 +1286,7 @@ export function TSRXPlugin(config) {
|
|
|
1205
1286
|
/**
|
|
1206
1287
|
* Override isLet to recognize `let &{` and `let &[` as variable declarations.
|
|
1207
1288
|
* Acorn's isLet checks the char after `let` and only recognizes `{`, `[`, or identifiers.
|
|
1208
|
-
* The `&`
|
|
1289
|
+
* The `&` character is not in that set, so `let &{...}` would not be parsed as a declaration.
|
|
1209
1290
|
* @type {Parse.Parser['isLet']}
|
|
1210
1291
|
*/
|
|
1211
1292
|
isLet(context) {
|
|
@@ -1217,9 +1298,9 @@ export function TSRXPlugin(config) {
|
|
|
1217
1298
|
const next = this.pos + match[0].length;
|
|
1218
1299
|
const nextCh = this.input.charCodeAt(next);
|
|
1219
1300
|
// If next char is &, check if char after & is { or [
|
|
1220
|
-
if (nextCh ===
|
|
1301
|
+
if (nextCh === CharCode.ampersand) {
|
|
1221
1302
|
const afterAmp = this.input.charCodeAt(next + 1);
|
|
1222
|
-
if (afterAmp ===
|
|
1303
|
+
if (afterAmp === CharCode.openBrace || afterAmp === CharCode.openBracket) return true;
|
|
1223
1304
|
}
|
|
1224
1305
|
return super.isLet(context);
|
|
1225
1306
|
}
|
|
@@ -1233,7 +1314,7 @@ export function TSRXPlugin(config) {
|
|
|
1233
1314
|
if (this.type === tt.bitwiseAND) {
|
|
1234
1315
|
// Check that the char immediately after & is { or [ (no whitespace)
|
|
1235
1316
|
const charAfterAmp = this.input.charCodeAt(this.end);
|
|
1236
|
-
if (charAfterAmp ===
|
|
1317
|
+
if (charAfterAmp === CharCode.openBrace || charAfterAmp === CharCode.openBracket) {
|
|
1237
1318
|
// & directly followed by { or [ — lazy destructuring
|
|
1238
1319
|
this.next(); // consume &, now current token is { or [
|
|
1239
1320
|
const pattern = super.parseBindingAtom();
|
|
@@ -1890,6 +1971,27 @@ export function TSRXPlugin(config) {
|
|
|
1890
1971
|
/** @type {AST.RefAttribute} */ (node).argument = this.parseMaybeAssign();
|
|
1891
1972
|
this.expect(tt.braceR);
|
|
1892
1973
|
return /** @type {AST.RefAttribute} */ (this.finishNode(node, 'RefAttribute'));
|
|
1974
|
+
} else if (this.type === tt.name && this.value === 'html') {
|
|
1975
|
+
// {html ...}
|
|
1976
|
+
// The support is purely for better error messages to avoid
|
|
1977
|
+
// the parser throw an unexpected token error
|
|
1978
|
+
const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
|
|
1979
|
+
id.tracked = false;
|
|
1980
|
+
this.finishNode(id, 'Identifier');
|
|
1981
|
+
this.next();
|
|
1982
|
+
const value = this.type === tt.braceR ? id : this.parseMaybeAssign();
|
|
1983
|
+
const report_end = this.type === tt.braceR ? this.end : (value.end ?? this.end);
|
|
1984
|
+
this.#report_recoverable_error_range(
|
|
1985
|
+
node.start ?? id.start ?? this.start,
|
|
1986
|
+
report_end,
|
|
1987
|
+
HTML_ATTRIBUTE_VALUE_ERROR,
|
|
1988
|
+
DIAGNOSTIC_CODES.HTML_DIRECTIVE_AS_ATTRIBUTE_VALUE,
|
|
1989
|
+
);
|
|
1990
|
+
/** @type {AST.Attribute} */ (node).name = id;
|
|
1991
|
+
/** @type {AST.Attribute} */ (node).value = value;
|
|
1992
|
+
/** @type {AST.Attribute} */ (node).shorthand = false;
|
|
1993
|
+
this.expect(tt.braceR);
|
|
1994
|
+
return this.finishNode(node, 'Attribute');
|
|
1893
1995
|
} else if (this.type === tt.ellipsis) {
|
|
1894
1996
|
this.expect(tt.ellipsis);
|
|
1895
1997
|
/** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
|
|
@@ -1913,10 +2015,18 @@ export function TSRXPlugin(config) {
|
|
|
1913
2015
|
}
|
|
1914
2016
|
}
|
|
1915
2017
|
/** @type {ESTreeJSX.JSXAttribute} */ (node).name = this.jsx_parseNamespacedName();
|
|
1916
|
-
/** @type {ESTreeJSX.JSXAttribute} */ (
|
|
1917
|
-
|
|
1918
|
-
|
|
2018
|
+
const value = /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
|
|
2019
|
+
this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
|
|
2020
|
+
);
|
|
2021
|
+
if (value?.type === 'JSXExpressionContainer' && value.html) {
|
|
2022
|
+
this.#report_recoverable_error_range(
|
|
2023
|
+
value.start ?? node.start ?? this.start,
|
|
2024
|
+
value.end ?? node.end ?? this.end,
|
|
2025
|
+
HTML_ATTRIBUTE_VALUE_ERROR,
|
|
2026
|
+
DIAGNOSTIC_CODES.HTML_DIRECTIVE_AS_ATTRIBUTE_VALUE,
|
|
1919
2027
|
);
|
|
2028
|
+
}
|
|
2029
|
+
/** @type {ESTreeJSX.JSXAttribute} */ (node).value = value;
|
|
1920
2030
|
return this.finishNode(node, 'JSXAttribute');
|
|
1921
2031
|
}
|
|
1922
2032
|
|
|
@@ -2121,20 +2231,22 @@ export function TSRXPlugin(config) {
|
|
|
2121
2231
|
let ch = this.input.charCodeAt(this.pos);
|
|
2122
2232
|
|
|
2123
2233
|
switch (ch) {
|
|
2124
|
-
case
|
|
2125
|
-
case
|
|
2234
|
+
case CharCode.lessThan:
|
|
2235
|
+
case CharCode.openBrace:
|
|
2126
2236
|
// In JSX text mode, '<' and '{' always start a tag/expression container.
|
|
2127
2237
|
// `exprAllowed` can be false here due to surrounding parser state, but
|
|
2128
2238
|
// throwing breaks valid templates (e.g. sibling tags after a close).
|
|
2129
|
-
|
|
2239
|
+
this.start = this.pos;
|
|
2240
|
+
this.startLoc = this.curPosition();
|
|
2241
|
+
if (ch === CharCode.lessThan) {
|
|
2130
2242
|
++this.pos;
|
|
2131
2243
|
return this.finishToken(tstt.jsxTagStart);
|
|
2132
2244
|
}
|
|
2133
2245
|
return this.getTokenFromCode(ch);
|
|
2134
2246
|
|
|
2135
|
-
case
|
|
2247
|
+
case CharCode.slash:
|
|
2136
2248
|
// Check if this is a comment (// or /*)
|
|
2137
|
-
if (this.input.charCodeAt(this.pos + 1) ===
|
|
2249
|
+
if (this.input.charCodeAt(this.pos + 1) === CharCode.slash) {
|
|
2138
2250
|
// '//'
|
|
2139
2251
|
// Line comment - handle it properly
|
|
2140
2252
|
const commentStart = this.pos;
|
|
@@ -2168,7 +2280,7 @@ export function TSRXPlugin(config) {
|
|
|
2168
2280
|
|
|
2169
2281
|
// Continue processing from current position
|
|
2170
2282
|
break;
|
|
2171
|
-
} else if (this.input.charCodeAt(this.pos + 1) ===
|
|
2283
|
+
} else if (this.input.charCodeAt(this.pos + 1) === CharCode.asterisk) {
|
|
2172
2284
|
// '/*'
|
|
2173
2285
|
// Block comment - handle it properly
|
|
2174
2286
|
const commentStart = this.pos;
|
|
@@ -2178,8 +2290,8 @@ export function TSRXPlugin(config) {
|
|
|
2178
2290
|
let commentText = '';
|
|
2179
2291
|
while (this.pos < this.input.length - 1) {
|
|
2180
2292
|
if (
|
|
2181
|
-
this.input.charCodeAt(this.pos) ===
|
|
2182
|
-
this.input.charCodeAt(this.pos + 1) ===
|
|
2293
|
+
this.input.charCodeAt(this.pos) === CharCode.asterisk &&
|
|
2294
|
+
this.input.charCodeAt(this.pos + 1) === CharCode.slash
|
|
2183
2295
|
) {
|
|
2184
2296
|
this.pos += 2;
|
|
2185
2297
|
break;
|
|
@@ -2214,17 +2326,16 @@ export function TSRXPlugin(config) {
|
|
|
2214
2326
|
this.exprAllowed = true;
|
|
2215
2327
|
return original.readToken.call(this, ch);
|
|
2216
2328
|
|
|
2217
|
-
case
|
|
2329
|
+
case CharCode.ampersand:
|
|
2218
2330
|
out += this.input.slice(chunkStart, this.pos);
|
|
2219
2331
|
out += this.jsx_readEntity();
|
|
2220
2332
|
chunkStart = this.pos;
|
|
2221
2333
|
break;
|
|
2222
2334
|
|
|
2223
|
-
case
|
|
2224
|
-
case
|
|
2225
|
-
// '}'
|
|
2335
|
+
case CharCode.greaterThan:
|
|
2336
|
+
case CharCode.closeBrace: {
|
|
2226
2337
|
if (
|
|
2227
|
-
ch ===
|
|
2338
|
+
ch === CharCode.closeBrace &&
|
|
2228
2339
|
(this.#path.length === 0 ||
|
|
2229
2340
|
this.#path.at(-1)?.type === 'Component' ||
|
|
2230
2341
|
this.#path.at(-1)?.type === 'Element' ||
|
|
@@ -2238,7 +2349,7 @@ export function TSRXPlugin(config) {
|
|
|
2238
2349
|
'Unexpected token `' +
|
|
2239
2350
|
this.input[this.pos] +
|
|
2240
2351
|
'`. Did you mean `' +
|
|
2241
|
-
(ch ===
|
|
2352
|
+
(ch === CharCode.greaterThan ? '>' : '}') +
|
|
2242
2353
|
'` or ' +
|
|
2243
2354
|
'`{"' +
|
|
2244
2355
|
this.input[this.pos] +
|
|
@@ -2252,7 +2363,7 @@ export function TSRXPlugin(config) {
|
|
|
2252
2363
|
out += this.input.slice(chunkStart, this.pos);
|
|
2253
2364
|
out += this.jsx_readNewLine(true);
|
|
2254
2365
|
chunkStart = this.pos;
|
|
2255
|
-
} else if (ch ===
|
|
2366
|
+
} else if (ch === CharCode.space || ch === CharCode.tab) {
|
|
2256
2367
|
++this.pos;
|
|
2257
2368
|
} else {
|
|
2258
2369
|
this.#resetTokenStartToCurrentPosition();
|
|
@@ -2283,28 +2394,28 @@ export function TSRXPlugin(config) {
|
|
|
2283
2394
|
// Check if the element being parsed IS a <tsx>, <tsrx>, or <tsx:*> tag
|
|
2284
2395
|
// Current token is jsxTagStart, this.end is position after '<'
|
|
2285
2396
|
const tag_name_start = this.end;
|
|
2286
|
-
const is_fragment_tag = this.input.charCodeAt(tag_name_start) ===
|
|
2397
|
+
const is_fragment_tag = this.input.charCodeAt(tag_name_start) === CharCode.greaterThan;
|
|
2287
2398
|
const char_after_tsx = this.input.charCodeAt(tag_name_start + 3);
|
|
2288
2399
|
const char_after_tsrx = this.input.charCodeAt(tag_name_start + 4);
|
|
2289
2400
|
const is_tsx_tag =
|
|
2290
2401
|
this.input.startsWith('tsx', tag_name_start) &&
|
|
2291
2402
|
(tag_name_start + 3 >= this.input.length ||
|
|
2292
|
-
char_after_tsx ===
|
|
2293
|
-
char_after_tsx ===
|
|
2294
|
-
char_after_tsx ===
|
|
2295
|
-
char_after_tsx ===
|
|
2296
|
-
char_after_tsx ===
|
|
2297
|
-
char_after_tsx ===
|
|
2298
|
-
char_after_tsx ===
|
|
2403
|
+
char_after_tsx === CharCode.greaterThan ||
|
|
2404
|
+
char_after_tsx === CharCode.slash ||
|
|
2405
|
+
char_after_tsx === CharCode.space ||
|
|
2406
|
+
char_after_tsx === CharCode.tab ||
|
|
2407
|
+
char_after_tsx === CharCode.lineFeed ||
|
|
2408
|
+
char_after_tsx === CharCode.carriageReturn ||
|
|
2409
|
+
char_after_tsx === CharCode.colon);
|
|
2299
2410
|
const is_tsrx_tag =
|
|
2300
2411
|
this.input.startsWith('tsrx', tag_name_start) &&
|
|
2301
2412
|
(tag_name_start + 4 >= this.input.length ||
|
|
2302
|
-
char_after_tsrx ===
|
|
2303
|
-
char_after_tsrx ===
|
|
2304
|
-
char_after_tsrx ===
|
|
2305
|
-
char_after_tsrx ===
|
|
2306
|
-
char_after_tsrx ===
|
|
2307
|
-
char_after_tsrx ===
|
|
2413
|
+
char_after_tsrx === CharCode.greaterThan ||
|
|
2414
|
+
char_after_tsrx === CharCode.slash ||
|
|
2415
|
+
char_after_tsrx === CharCode.space ||
|
|
2416
|
+
char_after_tsrx === CharCode.tab ||
|
|
2417
|
+
char_after_tsrx === CharCode.lineFeed ||
|
|
2418
|
+
char_after_tsrx === CharCode.carriageReturn);
|
|
2308
2419
|
|
|
2309
2420
|
if (is_fragment_tag || is_tsx_tag || is_tsrx_tag) {
|
|
2310
2421
|
// Use Ripple's parseElement to create a Tsx/Tsrx/TsxCompat node.
|
|
@@ -2356,6 +2467,8 @@ export function TSRXPlugin(config) {
|
|
|
2356
2467
|
/** @type {AST.NodeWithLocation} */ (element).loc.start = position;
|
|
2357
2468
|
element.metadata = { path: [] };
|
|
2358
2469
|
element.children = [];
|
|
2470
|
+
element.type = 'Element';
|
|
2471
|
+
this.#path.push(element);
|
|
2359
2472
|
|
|
2360
2473
|
const open = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
|
|
2361
2474
|
this.jsx_parseOpeningElementAt(start, position)
|
|
@@ -2414,8 +2527,6 @@ export function TSRXPlugin(config) {
|
|
|
2414
2527
|
element.type = 'Element';
|
|
2415
2528
|
}
|
|
2416
2529
|
|
|
2417
|
-
this.#path.push(element);
|
|
2418
|
-
|
|
2419
2530
|
for (const attr of open.attributes) {
|
|
2420
2531
|
if (attr.type === 'JSXAttribute') {
|
|
2421
2532
|
/** @type {AST.Attribute} */ (/** @type {unknown} */ (attr)).type = 'Attribute';
|
|
@@ -2456,7 +2567,12 @@ export function TSRXPlugin(config) {
|
|
|
2456
2567
|
|
|
2457
2568
|
element.attributes = open.attributes;
|
|
2458
2569
|
element.metadata ??= { path: [] };
|
|
2459
|
-
|
|
2570
|
+
// Opening-tag parsing can tokenize comments that appear before the first
|
|
2571
|
+
// child. Preserve that early container id so the comment stays associated
|
|
2572
|
+
// with this element during comment attachment/printing.
|
|
2573
|
+
if (element.metadata.commentContainerId === undefined) {
|
|
2574
|
+
element.metadata.commentContainerId = ++this.#commentContextId;
|
|
2575
|
+
}
|
|
2460
2576
|
|
|
2461
2577
|
if (element.selfClosing) {
|
|
2462
2578
|
this.#path.pop();
|
|
@@ -2470,7 +2586,7 @@ export function TSRXPlugin(config) {
|
|
|
2470
2586
|
enterScope: true,
|
|
2471
2587
|
});
|
|
2472
2588
|
|
|
2473
|
-
if (element.type === 'Tsx') {
|
|
2589
|
+
if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
|
|
2474
2590
|
this.#path.pop();
|
|
2475
2591
|
|
|
2476
2592
|
if (!element.unclosed) {
|
|
@@ -2647,7 +2763,7 @@ export function TSRXPlugin(config) {
|
|
|
2647
2763
|
enterScope: true,
|
|
2648
2764
|
});
|
|
2649
2765
|
|
|
2650
|
-
if (element.type === 'Tsx') {
|
|
2766
|
+
if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
|
|
2651
2767
|
this.#path.pop();
|
|
2652
2768
|
|
|
2653
2769
|
if (!element.unclosed) {
|
|
@@ -2671,12 +2787,15 @@ export function TSRXPlugin(config) {
|
|
|
2671
2787
|
this.#popTsxTokenContextBeforeTemplateExpressionChild();
|
|
2672
2788
|
this.next();
|
|
2673
2789
|
}
|
|
2674
|
-
} else if (element.type === 'TsxCompat') {
|
|
2790
|
+
} else if (/** @type {AST.TsxCompat} */ (element).type === 'TsxCompat') {
|
|
2675
2791
|
this.#path.pop();
|
|
2676
2792
|
|
|
2677
2793
|
if (!element.unclosed) {
|
|
2678
2794
|
const raise_error = () => {
|
|
2679
|
-
this.raise(
|
|
2795
|
+
this.raise(
|
|
2796
|
+
this.start,
|
|
2797
|
+
`Expected closing tag '</tsx:${/** @type {AST.TsxCompat} */ (element).kind}>'`,
|
|
2798
|
+
);
|
|
2680
2799
|
};
|
|
2681
2800
|
|
|
2682
2801
|
this.next();
|
|
@@ -2693,7 +2812,7 @@ export function TSRXPlugin(config) {
|
|
|
2693
2812
|
raise_error();
|
|
2694
2813
|
}
|
|
2695
2814
|
this.next();
|
|
2696
|
-
if (this.value !== element.kind) {
|
|
2815
|
+
if (this.value !== /** @type {AST.TsxCompat} */ (element).kind) {
|
|
2697
2816
|
raise_error();
|
|
2698
2817
|
}
|
|
2699
2818
|
this.next();
|
|
@@ -2703,7 +2822,10 @@ export function TSRXPlugin(config) {
|
|
|
2703
2822
|
this.#popTsxTokenContextBeforeTemplateExpressionChild();
|
|
2704
2823
|
this.next();
|
|
2705
2824
|
}
|
|
2706
|
-
} else if (
|
|
2825
|
+
} else if (
|
|
2826
|
+
/** @type {AST.Tsrx} */ (element).type === 'Tsrx' &&
|
|
2827
|
+
this.#path[this.#path.length - 1] === element
|
|
2828
|
+
) {
|
|
2707
2829
|
this.#report_broken_markup_error(
|
|
2708
2830
|
this.start,
|
|
2709
2831
|
"Unclosed tag '<tsrx>'. Expected '</tsrx>' before end of component.",
|
|
@@ -2787,7 +2909,10 @@ export function TSRXPlugin(config) {
|
|
|
2787
2909
|
}
|
|
2788
2910
|
if (this.type === tt.braceL) {
|
|
2789
2911
|
body.push(this.#parseNativeTemplateExpressionContainer());
|
|
2790
|
-
} else if (
|
|
2912
|
+
} else if (
|
|
2913
|
+
this.type === tt.string &&
|
|
2914
|
+
this.input.charCodeAt(this.start) === CharCode.doubleQuote
|
|
2915
|
+
) {
|
|
2791
2916
|
body.push(this.parseDoubleQuotedTextChild());
|
|
2792
2917
|
} else if (this.type === tt.braceR) {
|
|
2793
2918
|
// Leaving a component/template body. We may still be in TSX/JSX tokenization
|
|
@@ -2800,8 +2925,8 @@ export function TSRXPlugin(config) {
|
|
|
2800
2925
|
return;
|
|
2801
2926
|
} else if (
|
|
2802
2927
|
this.type === tstt.jsxTagStart ||
|
|
2803
|
-
(this.input.charCodeAt(this.start) ===
|
|
2804
|
-
this.input.charCodeAt(this.start + 1) ===
|
|
2928
|
+
(this.input.charCodeAt(this.start) === CharCode.lessThan &&
|
|
2929
|
+
this.input.charCodeAt(this.start + 1) === CharCode.slash)
|
|
2805
2930
|
) {
|
|
2806
2931
|
const startPos = this.start;
|
|
2807
2932
|
const startLoc = this.startLoc;
|
|
@@ -3082,7 +3207,7 @@ export function TSRXPlugin(config) {
|
|
|
3082
3207
|
if (
|
|
3083
3208
|
this.#functionBodyDepth === 0 &&
|
|
3084
3209
|
this.type === tt.string &&
|
|
3085
|
-
this.input.charCodeAt(this.start) ===
|
|
3210
|
+
this.input.charCodeAt(this.start) === CharCode.doubleQuote &&
|
|
3086
3211
|
(this.#path.at(-1)?.type === 'Component' || this.#path.at(-1)?.type === 'Element')
|
|
3087
3212
|
) {
|
|
3088
3213
|
this.pos = this.start;
|
|
@@ -3096,7 +3221,7 @@ export function TSRXPlugin(config) {
|
|
|
3096
3221
|
// e.g., &[data] = track(0); or &{x, y} = obj;
|
|
3097
3222
|
if (this.type === tt.bitwiseAND) {
|
|
3098
3223
|
const charAfterAmp = this.input.charCodeAt(this.end);
|
|
3099
|
-
if (charAfterAmp ===
|
|
3224
|
+
if (charAfterAmp === CharCode.openBrace || charAfterAmp === CharCode.openBracket) {
|
|
3100
3225
|
const node = /** @type {AST.ExpressionStatement} */ (this.startNode());
|
|
3101
3226
|
const assign_node = /** @type {AST.AssignmentExpression} */ (this.startNode());
|
|
3102
3227
|
this.next(); // consume &
|
|
@@ -60,20 +60,43 @@ const HOOK_CALLBACK_OUTER_MUTATION_ERROR =
|
|
|
60
60
|
const TEMPLATE_FRAGMENT_ERROR =
|
|
61
61
|
'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only within <tsx>...</tsx>.';
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* @param {TransformContext} transform_context
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
export function get_invalid_html_child_error_message(transform_context) {
|
|
68
|
+
return `\`{html ...}\` is only supported as the sole child of an element in ${transform_context.platform.name}.`;
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
/**
|
|
64
72
|
* @param {AST.Node} node
|
|
65
73
|
* @param {TransformContext} transform_context
|
|
66
74
|
*/
|
|
67
|
-
function
|
|
68
|
-
// this should be a fatal error so we don't pass the errors collection,
|
|
69
|
-
// since we don't have a transform for the Html node
|
|
75
|
+
function report_invalid_html_child_error(node, transform_context) {
|
|
70
76
|
error(
|
|
71
|
-
|
|
77
|
+
get_invalid_html_child_error_message(transform_context),
|
|
72
78
|
transform_context.filename,
|
|
73
79
|
node,
|
|
80
|
+
transform_context.errors,
|
|
81
|
+
transform_context.comments,
|
|
74
82
|
);
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
/**
|
|
86
|
+
* In loose/editor mode `error(...)` records the diagnostic and continues, so an
|
|
87
|
+
* invalid standalone `{html ...}` child still needs a valid expression node for
|
|
88
|
+
* the virtual TSX output.
|
|
89
|
+
*
|
|
90
|
+
* @param {any} node
|
|
91
|
+
* @param {TransformContext} transform_context
|
|
92
|
+
* @returns {ESTreeJSX.JSXExpressionContainer}
|
|
93
|
+
*/
|
|
94
|
+
export function recover_invalid_html_child(node, transform_context) {
|
|
95
|
+
report_invalid_html_child_error(node, transform_context);
|
|
96
|
+
const expression = set_loc(clone_expression_node(node.expression), node);
|
|
97
|
+
return to_jsx_expression_container(expression, node);
|
|
98
|
+
}
|
|
99
|
+
|
|
77
100
|
/**
|
|
78
101
|
* @param {AST.Node} node
|
|
79
102
|
* @param {TransformContext} transform_context
|
|
@@ -2310,10 +2333,19 @@ function to_jsx_element(node, transform_context, raw_children = node.children ||
|
|
|
2310
2333
|
selfClosing = child_transform.selfClosing;
|
|
2311
2334
|
}
|
|
2312
2335
|
} else {
|
|
2313
|
-
|
|
2314
|
-
|
|
2336
|
+
const html_child_transform = rewrite_host_html_children(
|
|
2337
|
+
node,
|
|
2338
|
+
walked_children,
|
|
2339
|
+
raw_children,
|
|
2340
|
+
attributes,
|
|
2341
|
+
transform_context,
|
|
2342
|
+
);
|
|
2343
|
+
if (html_child_transform) {
|
|
2344
|
+
children = html_child_transform.children;
|
|
2345
|
+
selfClosing = html_child_transform.selfClosing;
|
|
2346
|
+
} else {
|
|
2347
|
+
children = create_element_children(walked_children, transform_context);
|
|
2315
2348
|
}
|
|
2316
|
-
children = create_element_children(walked_children, transform_context);
|
|
2317
2349
|
}
|
|
2318
2350
|
const has_unmappable_attribute = attributes.some(
|
|
2319
2351
|
(/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
|
|
@@ -2341,6 +2373,117 @@ function to_jsx_element(node, transform_context, raw_children = node.children ||
|
|
|
2341
2373
|
return set_loc(b.jsx_element_fresh(openingElement, closingElement, children), node);
|
|
2342
2374
|
}
|
|
2343
2375
|
|
|
2376
|
+
/**
|
|
2377
|
+
* @param {any} node
|
|
2378
|
+
* @param {any[]} walked_children
|
|
2379
|
+
* @param {any[]} raw_children
|
|
2380
|
+
* @param {any[]} attributes
|
|
2381
|
+
* @param {TransformContext} transform_context
|
|
2382
|
+
* @returns {{ children: any[]; selfClosing: boolean } | null}
|
|
2383
|
+
*/
|
|
2384
|
+
export function rewrite_host_html_children(
|
|
2385
|
+
node,
|
|
2386
|
+
walked_children,
|
|
2387
|
+
raw_children,
|
|
2388
|
+
attributes,
|
|
2389
|
+
transform_context,
|
|
2390
|
+
) {
|
|
2391
|
+
const source_children = raw_children || walked_children;
|
|
2392
|
+
const source_html_index = source_children.findIndex((child) => child?.type === 'Html');
|
|
2393
|
+
if (source_html_index === -1) {
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
const source_html = source_children[source_html_index];
|
|
2397
|
+
const walked_html =
|
|
2398
|
+
walked_children[source_html_index]?.type === 'Html'
|
|
2399
|
+
? walked_children[source_html_index]
|
|
2400
|
+
: source_html;
|
|
2401
|
+
|
|
2402
|
+
if (is_component_like_element(node) || source_children.length !== 1) {
|
|
2403
|
+
report_invalid_html_child_error(source_html, transform_context);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
const conflicting_attribute = get_host_html_conflicting_attribute(attributes, transform_context);
|
|
2407
|
+
if (conflicting_attribute !== null) {
|
|
2408
|
+
error(
|
|
2409
|
+
create_host_html_conflict_error(conflicting_attribute, transform_context),
|
|
2410
|
+
transform_context.filename,
|
|
2411
|
+
source_html,
|
|
2412
|
+
transform_context.errors,
|
|
2413
|
+
transform_context.comments,
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
attributes.push(create_host_html_attribute(walked_html, source_html, transform_context));
|
|
2418
|
+
|
|
2419
|
+
return { children: [], selfClosing: true };
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
/**
|
|
2423
|
+
* @param {any[]} attributes
|
|
2424
|
+
* @param {TransformContext} transform_context
|
|
2425
|
+
* @returns {{ kind: 'attribute'; name: string } | null}
|
|
2426
|
+
*/
|
|
2427
|
+
export function get_host_html_conflicting_attribute(attributes, transform_context) {
|
|
2428
|
+
const conflicting_attributes = get_host_html_conflicting_attribute_names(transform_context);
|
|
2429
|
+
for (const name of conflicting_attributes) {
|
|
2430
|
+
if (has_jsx_attribute(attributes, name)) {
|
|
2431
|
+
return { kind: 'attribute', name };
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
return null;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
/**
|
|
2439
|
+
* @param {{ kind: 'attribute'; name: string }} conflicting_attribute
|
|
2440
|
+
* @param {TransformContext} transform_context
|
|
2441
|
+
* @returns {string}
|
|
2442
|
+
*/
|
|
2443
|
+
export function create_host_html_conflict_error(conflicting_attribute, transform_context) {
|
|
2444
|
+
const html_attribute = get_host_html_attribute_name(transform_context);
|
|
2445
|
+
return `\`{html ...}\` lowers to \`${html_attribute}\` on the ${transform_context.platform.name} target and cannot be combined with an existing \`${conflicting_attribute.name}\` attribute.`;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
/**
|
|
2449
|
+
* @param {TransformContext} transform_context
|
|
2450
|
+
* @returns {string[]}
|
|
2451
|
+
*/
|
|
2452
|
+
function get_host_html_conflicting_attribute_names(transform_context) {
|
|
2453
|
+
switch (transform_context.platform.name) {
|
|
2454
|
+
case 'Solid':
|
|
2455
|
+
return ['innerHTML', 'textContent'];
|
|
2456
|
+
case 'Vue':
|
|
2457
|
+
return ['innerHTML'];
|
|
2458
|
+
default:
|
|
2459
|
+
return [get_host_html_attribute_name(transform_context)];
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/**
|
|
2464
|
+
* @param {TransformContext} transform_context
|
|
2465
|
+
* @returns {'dangerouslySetInnerHTML' | 'innerHTML'}
|
|
2466
|
+
*/
|
|
2467
|
+
function get_host_html_attribute_name(transform_context) {
|
|
2468
|
+
return transform_context.platform.jsx?.htmlProp === 'dangerouslySetInnerHTML'
|
|
2469
|
+
? 'dangerouslySetInnerHTML'
|
|
2470
|
+
: 'innerHTML';
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
/**
|
|
2474
|
+
* @param {any[]} attributes
|
|
2475
|
+
* @param {string} name
|
|
2476
|
+
* @returns {boolean}
|
|
2477
|
+
*/
|
|
2478
|
+
function has_jsx_attribute(attributes, name) {
|
|
2479
|
+
return attributes.some(
|
|
2480
|
+
(attr) =>
|
|
2481
|
+
attr?.type === 'JSXAttribute' &&
|
|
2482
|
+
attr.name?.type === 'JSXIdentifier' &&
|
|
2483
|
+
attr.name.name === name,
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2344
2487
|
/**
|
|
2345
2488
|
* @param {any[]} children
|
|
2346
2489
|
* @param {TransformContext} transform_context
|
|
@@ -3580,7 +3723,7 @@ function to_jsx_child(node, transform_context) {
|
|
|
3580
3723
|
case 'TSRXExpression':
|
|
3581
3724
|
return to_jsx_expression_container(node.expression, node);
|
|
3582
3725
|
case 'Html':
|
|
3583
|
-
return
|
|
3726
|
+
return recover_invalid_html_child(node, transform_context);
|
|
3584
3727
|
case 'IfStatement':
|
|
3585
3728
|
return (
|
|
3586
3729
|
transform_context.platform.hooks?.controlFlow?.ifStatement ?? if_statement_to_jsx_child
|
|
@@ -4918,7 +5061,7 @@ function transform_element_attributes_dispatch(attrs, transform_context, element
|
|
|
4918
5061
|
* @param {any} element
|
|
4919
5062
|
* @returns {boolean}
|
|
4920
5063
|
*/
|
|
4921
|
-
function is_component_like_element(element) {
|
|
5064
|
+
export function is_component_like_element(element) {
|
|
4922
5065
|
const id = element?.id;
|
|
4923
5066
|
if (!id) return false;
|
|
4924
5067
|
if (id.type === 'Identifier') return /^[A-Z]/.test(id.name);
|
|
@@ -5127,6 +5270,31 @@ function is_named_ref_attribute(attr) {
|
|
|
5127
5270
|
);
|
|
5128
5271
|
}
|
|
5129
5272
|
|
|
5273
|
+
/**
|
|
5274
|
+
* @param {any} html_expression
|
|
5275
|
+
* @param {any} source_attr
|
|
5276
|
+
* @param {TransformContext} transform_context
|
|
5277
|
+
* @returns {any}
|
|
5278
|
+
*/
|
|
5279
|
+
export function create_host_html_attribute(html_expression, source_attr, transform_context) {
|
|
5280
|
+
const expression =
|
|
5281
|
+
html_expression?.type === 'Html' ? html_expression.expression : html_expression;
|
|
5282
|
+
const name = get_host_html_attribute_name(transform_context);
|
|
5283
|
+
const value =
|
|
5284
|
+
name === 'dangerouslySetInnerHTML'
|
|
5285
|
+
? set_loc(b.object([b.prop('init', b.id('__html'), expression)]), source_attr)
|
|
5286
|
+
: expression;
|
|
5287
|
+
const value_container = to_jsx_expression_container(value, source_attr);
|
|
5288
|
+
if (name !== 'dangerouslySetInnerHTML') {
|
|
5289
|
+
setLocation(value_container, source_attr, true);
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
return set_loc(
|
|
5293
|
+
build_jsx_attribute(b.jsx_id(name), value_container, false, source_attr),
|
|
5294
|
+
source_attr,
|
|
5295
|
+
);
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5130
5298
|
/**
|
|
5131
5299
|
* @param {any} expression
|
|
5132
5300
|
* @returns {boolean}
|
package/types/jsx-platform.d.ts
CHANGED
|
@@ -345,6 +345,8 @@ export interface JsxPlatform {
|
|
|
345
345
|
* explicit `ref={normalized.ref}` attribute.
|
|
346
346
|
*/
|
|
347
347
|
hostSpreadRefStrategy?: 'explicit-ref-attr';
|
|
348
|
+
/** Native host prop used when lowering a sole child `{html ...}`. */
|
|
349
|
+
htmlProp?: 'dangerouslySetInnerHTML' | 'innerHTML';
|
|
348
350
|
};
|
|
349
351
|
|
|
350
352
|
validation: {
|