@tsrx/core 0.1.20 → 0.1.24
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 +3 -3
- package/src/analyze/prune.js +117 -70
- package/src/analyze/validation.js +122 -5
- package/src/diagnostics.js +1 -0
- package/src/index.js +10 -4
- package/src/parse/index.js +157 -99
- package/src/plugin.js +2540 -867
- package/src/runtime/html.js +1 -1
- package/src/runtime/iterable.js +15 -13
- package/src/runtime/language-helpers.js +39 -0
- package/src/scope.js +25 -10
- package/src/source-map-utils.js +1 -1
- package/src/transform/await.js +5 -1
- package/src/transform/jsx/ast-builders.js +6 -81
- package/src/transform/jsx/helpers.js +8 -5
- package/src/transform/jsx/index.js +1440 -451
- package/src/transform/jsx-interleave.js +1 -2
- package/src/transform/scoping.js +26 -63
- package/src/transform/segments.js +66 -48
- package/src/transform/style-ref.js +3 -11
- package/src/utils/builders.js +2 -3
- package/types/index.d.ts +181 -115
- package/types/jsx-platform.d.ts +14 -11
- package/types/parse.d.ts +36 -10
- package/types/runtime/html.d.ts +1 -0
- package/types/runtime/language-helpers.d.ts +4 -0
- package/types/runtime/ref.d.ts +1 -0
package/src/plugin.js
CHANGED
|
@@ -5,22 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as acorn from 'acorn';
|
|
8
|
-
import { parse_style } from './parse/style.js';
|
|
9
8
|
import {
|
|
10
|
-
convert_from_jsx,
|
|
11
9
|
skipWhitespace,
|
|
12
10
|
isWhitespaceTextNode,
|
|
13
11
|
BINDING_TYPES,
|
|
14
12
|
DestructuringErrors,
|
|
15
13
|
} from './parse/index.js';
|
|
14
|
+
import { parse_style } from './parse/style.js';
|
|
16
15
|
import { regex_newline_characters } from './utils/patterns.js';
|
|
17
16
|
import { error } from './errors.js';
|
|
18
17
|
import { DIAGNOSTIC_CODES } from './diagnostics.js';
|
|
19
18
|
import { TSRX_RETURN_STATEMENT_ERROR } from './analyze/validation.js';
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const DYNAMIC_ATTRIBUTE_NAME_ERROR =
|
|
23
|
-
'Dynamic component / element syntax (`@`) is only supported on native TSRX element names, not attribute names.';
|
|
19
|
+
const FORGOTTEN_STATEMENT_CONTAINER_ERROR =
|
|
20
|
+
"This function body contains TSRX template output, but it is a normal JavaScript block. Add '@' before the opening brace to use a TSRX statement container.";
|
|
24
21
|
|
|
25
22
|
const CharCode = Object.freeze({
|
|
26
23
|
tab: 9,
|
|
@@ -28,12 +25,14 @@ const CharCode = Object.freeze({
|
|
|
28
25
|
carriageReturn: 13,
|
|
29
26
|
space: 32,
|
|
30
27
|
doubleQuote: 34,
|
|
28
|
+
numberSign: 35,
|
|
31
29
|
dollar: 36,
|
|
32
30
|
ampersand: 38,
|
|
33
31
|
singleQuote: 39,
|
|
34
32
|
openParen: 40,
|
|
35
33
|
closeParen: 41,
|
|
36
34
|
asterisk: 42,
|
|
35
|
+
dash: 45,
|
|
37
36
|
slash: 47,
|
|
38
37
|
colon: 58,
|
|
39
38
|
semicolon: 59,
|
|
@@ -46,6 +45,7 @@ const CharCode = Object.freeze({
|
|
|
46
45
|
uppercaseA: 65,
|
|
47
46
|
uppercaseZ: 90,
|
|
48
47
|
openBracket: 91,
|
|
48
|
+
closeBracket: 93,
|
|
49
49
|
backslash: 92,
|
|
50
50
|
underscore: 95,
|
|
51
51
|
backtick: 96,
|
|
@@ -55,6 +55,27 @@ const CharCode = Object.freeze({
|
|
|
55
55
|
closeBrace: 125,
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Keywords after which a `/` begins a regex literal rather than division, used
|
|
60
|
+
* by the look-ahead scanners to track expression position in script content.
|
|
61
|
+
*/
|
|
62
|
+
const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
63
|
+
'return',
|
|
64
|
+
'typeof',
|
|
65
|
+
'instanceof',
|
|
66
|
+
'in',
|
|
67
|
+
'of',
|
|
68
|
+
'new',
|
|
69
|
+
'delete',
|
|
70
|
+
'void',
|
|
71
|
+
'do',
|
|
72
|
+
'else',
|
|
73
|
+
'yield',
|
|
74
|
+
'await',
|
|
75
|
+
'case',
|
|
76
|
+
'throw',
|
|
77
|
+
]);
|
|
78
|
+
|
|
58
79
|
/** @type {WeakMap<Record<string, boolean>, Map<string, number>>} */
|
|
59
80
|
const argument_clash_first_positions = new WeakMap();
|
|
60
81
|
/** @type {WeakMap<Record<string, boolean>, Set<string>>} */
|
|
@@ -224,8 +245,6 @@ export function TSRXPlugin(config) {
|
|
|
224
245
|
class TSRXParser extends Parser {
|
|
225
246
|
/** @type {AST.Node[]} */
|
|
226
247
|
#path = [];
|
|
227
|
-
#allowTagStartAfterDoubleQuotedText = false;
|
|
228
|
-
#allowDoubleQuotedTextChildAfterBrace = false;
|
|
229
248
|
#commentContextId = 0;
|
|
230
249
|
#collect = false;
|
|
231
250
|
#loose = false;
|
|
@@ -235,8 +254,33 @@ export function TSRXPlugin(config) {
|
|
|
235
254
|
#filename = null;
|
|
236
255
|
#functionBodyDepth = 0;
|
|
237
256
|
#allowExpressionContainerTrailingSemicolon = false;
|
|
238
|
-
#tsxIslandExpressionDepth = 0;
|
|
239
257
|
#jsxAttributeValueExpressionDepth = 0;
|
|
258
|
+
#jsxExpressionContainerDepth = 0;
|
|
259
|
+
// Context-stack length at the start of each open `{ … }` expression container.
|
|
260
|
+
// A control-flow directive (`@if`/`@for`/…) parsed inside a container strips
|
|
261
|
+
// JSX contexts so its header/body tokenize as JS; without a floor it would also
|
|
262
|
+
// strip the enclosing element's and container's contexts (which nothing rebuilds),
|
|
263
|
+
// underflowing the context stack when the surrounding markup closes. The directive
|
|
264
|
+
// filter preserves everything below the innermost baseline. See
|
|
265
|
+
// `#filterTemplateScriptContexts`.
|
|
266
|
+
/** @type {number[]} */
|
|
267
|
+
#expressionContainerContextBaselines = [];
|
|
268
|
+
#consumeContainerBraceAfterScope = false;
|
|
269
|
+
#scriptJSXElementDepth = 0;
|
|
270
|
+
#forceScriptJSXElementDepth = 0;
|
|
271
|
+
#suppressTemplateRawTextToken = false;
|
|
272
|
+
#templateScriptParsingDepth = 0;
|
|
273
|
+
#controlFlowBlockAllowsNativeReturn = false;
|
|
274
|
+
#parsingJSXSwitchCaseScriptStatementDepth = 0;
|
|
275
|
+
#templateControlFlowBlockDepth = 0;
|
|
276
|
+
#templateControlFlowTryDepth = 0;
|
|
277
|
+
/** @type {Parse.Parser['context']} */
|
|
278
|
+
context = [b_stat];
|
|
279
|
+
/** @type {AST.Node | null} */
|
|
280
|
+
#openingNativeTemplateNode = null;
|
|
281
|
+
#closingNativeTemplateNode = false;
|
|
282
|
+
#readingJSXControlFlowDirectiveKeyword = false;
|
|
283
|
+
#readingJSXControlFlowHeader = false;
|
|
240
284
|
|
|
241
285
|
/**
|
|
242
286
|
* @type {Parse.Parser['finishNode']}
|
|
@@ -260,6 +304,7 @@ export function TSRXPlugin(config) {
|
|
|
260
304
|
*/
|
|
261
305
|
constructor(options, input) {
|
|
262
306
|
super(options, input);
|
|
307
|
+
this.context ??= [b_stat];
|
|
263
308
|
const tsrx_options = options?.tsrxOptions ?? options?.rippleOptions;
|
|
264
309
|
this.#collect = tsrx_options?.collect === true || tsrx_options?.loose === true;
|
|
265
310
|
this.#loose = tsrx_options?.loose === true;
|
|
@@ -267,6 +312,7 @@ export function TSRXPlugin(config) {
|
|
|
267
312
|
this.#filename = tsrx_options?.filename || null;
|
|
268
313
|
}
|
|
269
314
|
|
|
315
|
+
/** @this {Parse.Parser} */
|
|
270
316
|
#resetTokenStartToCurrentPosition() {
|
|
271
317
|
if (this.start !== this.pos) {
|
|
272
318
|
this.start = this.pos;
|
|
@@ -274,29 +320,12 @@ export function TSRXPlugin(config) {
|
|
|
274
320
|
}
|
|
275
321
|
}
|
|
276
322
|
|
|
277
|
-
#previousNonWhitespaceChar() {
|
|
278
|
-
let index = this.pos - 1;
|
|
279
|
-
while (index >= 0) {
|
|
280
|
-
const ch = this.input.charCodeAt(index);
|
|
281
|
-
if (
|
|
282
|
-
ch !== CharCode.space &&
|
|
283
|
-
ch !== CharCode.tab &&
|
|
284
|
-
ch !== CharCode.lineFeed &&
|
|
285
|
-
ch !== CharCode.carriageReturn
|
|
286
|
-
) {
|
|
287
|
-
return ch;
|
|
288
|
-
}
|
|
289
|
-
index--;
|
|
290
|
-
}
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
323
|
/**
|
|
295
324
|
* Native TSRX template bodies share one grammar across elements and fragments.
|
|
296
325
|
* This helper keeps the parser-state setup in one place while callers keep
|
|
297
326
|
* ownership of their distinct closing delimiter handling (`}` vs `</tag>`).
|
|
298
327
|
*
|
|
299
|
-
* @param {AST.Node} node
|
|
328
|
+
* @param {AST.Node & { body?: AST.Node }} node
|
|
300
329
|
* @param {AST.Node[]} body
|
|
301
330
|
* @param {{
|
|
302
331
|
* enterScope?: boolean,
|
|
@@ -336,205 +365,1700 @@ export function TSRXPlugin(config) {
|
|
|
336
365
|
}
|
|
337
366
|
}
|
|
338
367
|
|
|
368
|
+
/**
|
|
369
|
+
* @param {boolean} [createNewLexicalScope]
|
|
370
|
+
* @param {AST.BlockStatement} [node]
|
|
371
|
+
* @param {boolean} [exitStrict]
|
|
372
|
+
* @returns {AST.BlockStatement}
|
|
373
|
+
*/
|
|
374
|
+
#parseTemplateControlFlowBlock(createNewLexicalScope = true, node, exitStrict) {
|
|
375
|
+
node ??= /** @type {AST.BlockStatement} */ (this.startNode());
|
|
376
|
+
// Consume the flag for this block only; nested control-flow blocks
|
|
377
|
+
// parsed inside the body must not inherit it.
|
|
378
|
+
const allows_native_return = this.#controlFlowBlockAllowsNativeReturn;
|
|
379
|
+
this.#controlFlowBlockAllowsNativeReturn = false;
|
|
380
|
+
node.body = [];
|
|
381
|
+
node.metadata = {
|
|
382
|
+
...node.metadata,
|
|
383
|
+
path: [],
|
|
384
|
+
native_tsrx_template_block: true,
|
|
385
|
+
templateMode: 'script',
|
|
386
|
+
allows_native_return,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// A directive's `{ }` IS a code block (§2 rule 8): setup statements then
|
|
390
|
+
// at most one render node. Code-only blocks are allowed (§2 rule 6). Hide
|
|
391
|
+
// the enclosing template from `#path` so the body tokenizes as code (not
|
|
392
|
+
// JSX raw text); render nodes re-establish their own path via `parseElement`.
|
|
393
|
+
const enclosing_context = this.context;
|
|
394
|
+
const enclosing_path = this.#path;
|
|
395
|
+
this.context = enclosing_context.filter(
|
|
396
|
+
(context) =>
|
|
397
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
398
|
+
);
|
|
399
|
+
if (this.curContext() !== b_stat) {
|
|
400
|
+
this.context.push(b_stat);
|
|
401
|
+
}
|
|
402
|
+
this.#path = [];
|
|
403
|
+
if (createNewLexicalScope) {
|
|
404
|
+
this.enterScope(0);
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
this.expect(tt.braceL);
|
|
408
|
+
this.#parseCodeBlockBody(node.body);
|
|
409
|
+
} finally {
|
|
410
|
+
if (createNewLexicalScope) {
|
|
411
|
+
this.exitScope();
|
|
412
|
+
}
|
|
413
|
+
this.#path = enclosing_path;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (exitStrict) {
|
|
417
|
+
this.strict = false;
|
|
418
|
+
}
|
|
419
|
+
this.exprAllowed = true;
|
|
420
|
+
this.context = enclosing_context;
|
|
421
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
422
|
+
this.#readingJSXControlFlowHeader = true;
|
|
423
|
+
try {
|
|
424
|
+
this.next();
|
|
425
|
+
} finally {
|
|
426
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
427
|
+
}
|
|
428
|
+
return this.finishNode(node, 'BlockStatement');
|
|
429
|
+
}
|
|
430
|
+
|
|
339
431
|
/**
|
|
340
432
|
* @param {AST.Node | undefined} node
|
|
341
433
|
*/
|
|
342
434
|
#isNativeTemplateNode(node) {
|
|
343
435
|
return (
|
|
344
|
-
node?.
|
|
436
|
+
node?.metadata?.native_tsrx_template_block ||
|
|
437
|
+
(node?.type === 'JSXElement' && node.metadata?.native_tsrx) ||
|
|
438
|
+
(node?.type === 'JSXFragment' && node.metadata?.native_tsrx) ||
|
|
439
|
+
(node?.type === 'JSXStyleElement' && node.metadata?.native_tsrx)
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
#currentNativeTemplateNode() {
|
|
444
|
+
return (
|
|
445
|
+
this.#openingNativeTemplateNode ??
|
|
446
|
+
this.#path.findLast((node) => this.#isNativeTemplateNode(node))
|
|
345
447
|
);
|
|
346
448
|
}
|
|
347
449
|
|
|
348
450
|
/**
|
|
349
|
-
* @param {AST.Node
|
|
451
|
+
* @param {AST.Node | undefined} node
|
|
452
|
+
* @param {string} name
|
|
350
453
|
*/
|
|
351
|
-
#
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
454
|
+
#isNativeElementNamed(node, name) {
|
|
455
|
+
return (
|
|
456
|
+
(node?.type === 'JSXElement' || node?.type === 'JSXStyleElement') &&
|
|
457
|
+
node.metadata?.native_tsrx &&
|
|
458
|
+
this.getElementName(node.openingElement?.name) === name
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#isInsideNativeTemplateScriptSection() {
|
|
463
|
+
const node = this.#currentNativeTemplateNode();
|
|
464
|
+
return !!node && node.metadata?.templateMode !== 'template';
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#isStyleOpeningTagStart() {
|
|
468
|
+
let index = this.start + 1;
|
|
469
|
+
if (this.input.charCodeAt(index) === CharCode.slash) return false;
|
|
470
|
+
if (this.input.slice(index, index + 'style'.length) !== 'style') return false;
|
|
471
|
+
|
|
472
|
+
const after = this.input.charCodeAt(index + 'style'.length);
|
|
473
|
+
return (
|
|
474
|
+
after === CharCode.greaterThan ||
|
|
475
|
+
after === CharCode.slash ||
|
|
476
|
+
after === CharCode.space ||
|
|
477
|
+
after === CharCode.tab ||
|
|
478
|
+
after === CharCode.lineFeed ||
|
|
479
|
+
after === CharCode.carriageReturn
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* @param {number} index
|
|
485
|
+
*/
|
|
486
|
+
#isLineStartPosition(index) {
|
|
487
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
488
|
+
const ch = this.input.charCodeAt(i);
|
|
489
|
+
if (ch === CharCode.lineFeed || ch === CharCode.carriageReturn) return true;
|
|
490
|
+
if (ch !== CharCode.space && ch !== CharCode.tab) return false;
|
|
491
|
+
}
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* @param {number} index
|
|
497
|
+
*/
|
|
498
|
+
#previousNonSpaceTabIndex(index) {
|
|
499
|
+
let cursor = index - 1;
|
|
500
|
+
while (
|
|
501
|
+
cursor >= 0 &&
|
|
502
|
+
(this.input.charCodeAt(cursor) === CharCode.space ||
|
|
503
|
+
this.input.charCodeAt(cursor) === CharCode.tab)
|
|
504
|
+
) {
|
|
505
|
+
cursor--;
|
|
506
|
+
}
|
|
507
|
+
return cursor;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* @param {number} end_index Inclusive index of the keyword's last character.
|
|
512
|
+
* @param {string} keyword
|
|
513
|
+
*/
|
|
514
|
+
#keywordEndsAt(end_index, keyword) {
|
|
515
|
+
const start = end_index - keyword.length + 1;
|
|
516
|
+
if (start < 0) return false;
|
|
517
|
+
if (this.input.slice(start, end_index + 1) !== keyword) return false;
|
|
518
|
+
return !this.#isIdentifierChar(this.input.charCodeAt(start - 1));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Returns true when a `<` at `index` can start TypeScript type
|
|
523
|
+
* parameters/arguments in expression-like code rather than a JSX tag.
|
|
524
|
+
* Most type argument lists are adjacent to the previous token (`foo<T>`,
|
|
525
|
+
* `build<T>()`, `Map<K, V>`). The whitespace-separated form is valid for
|
|
526
|
+
* anonymous generic function expressions (`function <T>() {}`); generic
|
|
527
|
+
* arrows are handled separately by `looks_like_generic_arrow`.
|
|
528
|
+
*
|
|
529
|
+
* @param {number} index
|
|
530
|
+
*/
|
|
531
|
+
#canStartTypeParameterOrArgumentList(index) {
|
|
532
|
+
const previous = this.#previousNonSpaceTabIndex(index);
|
|
533
|
+
if (previous < 0) return false;
|
|
534
|
+
if (previous === index - 1) {
|
|
535
|
+
return this.#canPrecedeTypeArgumentList(this.input.charCodeAt(previous));
|
|
536
|
+
}
|
|
537
|
+
return this.#keywordEndsAt(previous, 'function');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
#parseTemplateRawText() {
|
|
541
|
+
const start = this.start;
|
|
542
|
+
// The current jsxText token spans `[start, token_end]`. Comments inside
|
|
543
|
+
// that span were already consumed and recorded by the tokenizer
|
|
544
|
+
// (`jsx_readToken`); only comments at/after `token_end` (e.g. a body that
|
|
545
|
+
// opens with a comment, where the raw-text token stops before it) still
|
|
546
|
+
// need recording here. Either way we drop `//` lines from the JSXText value
|
|
547
|
+
// and always advance past them so the scan can't re-tokenize the same spot.
|
|
548
|
+
const token_end = this.end;
|
|
549
|
+
let index = start;
|
|
550
|
+
let value = '';
|
|
551
|
+
while (index < this.input.length) {
|
|
552
|
+
if (this.#isTemplateLineCommentStart(index)) {
|
|
553
|
+
const comment_start = index;
|
|
554
|
+
const comment_start_loc = acorn.getLineInfo(this.input, comment_start);
|
|
555
|
+
index += 2;
|
|
556
|
+
while (
|
|
557
|
+
index < this.input.length &&
|
|
558
|
+
this.input.charCodeAt(index) !== CharCode.lineFeed &&
|
|
559
|
+
this.input.charCodeAt(index) !== CharCode.carriageReturn
|
|
560
|
+
) {
|
|
561
|
+
index++;
|
|
562
|
+
}
|
|
563
|
+
if (this.options.onComment && comment_start >= token_end) {
|
|
564
|
+
const comment_end_loc = acorn.getLineInfo(this.input, index);
|
|
565
|
+
// Pass null metadata so position-based attachment places the comment
|
|
566
|
+
// as a leading comment on the following child (which the JSX printers
|
|
567
|
+
// emit), rather than on the container's `elementLeadingComments`.
|
|
568
|
+
this.options.onComment(
|
|
569
|
+
false,
|
|
570
|
+
this.input.slice(comment_start + 2, index),
|
|
571
|
+
comment_start,
|
|
572
|
+
index,
|
|
573
|
+
new acorn.Position(comment_start_loc.line, comment_start_loc.column),
|
|
574
|
+
new acorn.Position(comment_end_loc.line, comment_end_loc.column),
|
|
575
|
+
/** @type {any} */ (null),
|
|
365
576
|
);
|
|
366
577
|
}
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const ch = this.input.charCodeAt(index);
|
|
581
|
+
if (
|
|
582
|
+
ch === CharCode.lessThan ||
|
|
583
|
+
ch === CharCode.openBrace ||
|
|
584
|
+
ch === CharCode.closeBrace ||
|
|
585
|
+
this.#isJSXControlFlowDirectiveAt(index)
|
|
586
|
+
) {
|
|
587
|
+
break;
|
|
370
588
|
}
|
|
589
|
+
value += this.input[index];
|
|
590
|
+
index++;
|
|
371
591
|
}
|
|
372
|
-
}
|
|
373
592
|
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
this
|
|
382
|
-
}
|
|
383
|
-
// Keep JSXEmptyExpression as-is (for prettier to handle comments)
|
|
384
|
-
// but convert other expressions to native TSRX child nodes.
|
|
385
|
-
if (node.expression.type !== 'JSXEmptyExpression') {
|
|
386
|
-
/** @type {AST.TSRXExpression | AST.TextNode} */ (/** @type {unknown} */ (node)).type =
|
|
387
|
-
'TSRXExpression';
|
|
593
|
+
const endLoc = acorn.getLineInfo(this.input, index);
|
|
594
|
+
const node = /** @type {ESTreeJSX.JSXText} */ (this.startNodeAt(start, this.startLoc));
|
|
595
|
+
node.value = value;
|
|
596
|
+
node.raw = this.input.slice(start, index);
|
|
597
|
+
|
|
598
|
+
if (node.raw.match(regex_newline_characters)) {
|
|
599
|
+
this.curLine = endLoc.line;
|
|
600
|
+
this.lineStart = index - endLoc.column;
|
|
388
601
|
}
|
|
602
|
+
this.pos = index;
|
|
603
|
+
this.#popTemplateLiteralTokenContext();
|
|
604
|
+
this.next();
|
|
389
605
|
|
|
390
|
-
return
|
|
391
|
-
/** @type {unknown} */ (node)
|
|
392
|
-
);
|
|
606
|
+
return this.finishNodeAt(node, 'JSXText', index, endLoc);
|
|
393
607
|
}
|
|
394
608
|
|
|
395
609
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
610
|
+
* JSX significant-whitespace rule for a template text child. Non-whitespace
|
|
611
|
+
* text is always kept; whitespace-only text is kept only when it is an
|
|
612
|
+
* intentional inline space (no newline) separating two siblings, and dropped
|
|
613
|
+
* when it is layout indentation (contains a newline).
|
|
614
|
+
*
|
|
615
|
+
* @param {ESTreeJSX.JSXText} node
|
|
398
616
|
*/
|
|
399
|
-
#
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
617
|
+
#shouldKeepTemplateTextNode(node) {
|
|
618
|
+
if (!isWhitespaceTextNode(node)) {
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
return node.value !== '' && !regex_newline_characters.test(node.value);
|
|
622
|
+
}
|
|
403
623
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const displayTag = tagName || '';
|
|
407
|
-
this.#report_broken_markup_error(
|
|
408
|
-
this.start,
|
|
409
|
-
`Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
|
|
410
|
-
);
|
|
411
|
-
island.unclosed = true;
|
|
412
|
-
/** @type {AST.NodeWithLocation} */ (island).loc.end = {
|
|
413
|
-
.../** @type {AST.SourceLocation} */ (island.openingElement.loc).end,
|
|
414
|
-
};
|
|
415
|
-
island.end = island.openingElement.end;
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
624
|
+
#isSwitchCaseScriptStatementStart() {
|
|
625
|
+
let index = skip_whitespace_from(this.input, this.start);
|
|
418
626
|
|
|
419
|
-
|
|
420
|
-
this.exprAllowed = false;
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
627
|
+
const first = this.input.charCodeAt(index);
|
|
423
628
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
629
|
+
if (first === CharCode.openBracket || first === CharCode.openBrace) {
|
|
630
|
+
let depth = 0;
|
|
631
|
+
let i = index;
|
|
632
|
+
for (; i < this.input.length; i++) {
|
|
633
|
+
const ch = this.input.charCodeAt(i);
|
|
634
|
+
if (
|
|
635
|
+
ch === CharCode.openBracket ||
|
|
636
|
+
ch === CharCode.openBrace ||
|
|
637
|
+
ch === CharCode.openParen
|
|
638
|
+
) {
|
|
639
|
+
depth++;
|
|
640
|
+
} else if (
|
|
641
|
+
ch === CharCode.closeBracket ||
|
|
642
|
+
ch === CharCode.closeBrace ||
|
|
643
|
+
ch === CharCode.closeParen
|
|
644
|
+
) {
|
|
645
|
+
depth--;
|
|
646
|
+
if (depth === 0) {
|
|
647
|
+
i++;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
432
650
|
}
|
|
433
|
-
this.#popTemplateLiteralTokenContext();
|
|
434
|
-
this.next();
|
|
435
651
|
}
|
|
652
|
+
if (depth !== 0) return false;
|
|
653
|
+
i = skip_whitespace_from(this.input, i);
|
|
654
|
+
if (this.input.charCodeAt(i) !== CharCode.equals) return false;
|
|
655
|
+
const next = this.input.charCodeAt(i + 1);
|
|
656
|
+
return next !== CharCode.equals && next !== CharCode.greaterThan;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (
|
|
660
|
+
!this.#isIdentifierChar(first) ||
|
|
661
|
+
(first >= CharCode.digit0 && first <= CharCode.digit9)
|
|
662
|
+
) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const word_start = index;
|
|
667
|
+
index++;
|
|
668
|
+
while (this.#isIdentifierChar(this.input.charCodeAt(index))) {
|
|
669
|
+
index++;
|
|
670
|
+
}
|
|
671
|
+
const word = this.input.slice(word_start, index);
|
|
672
|
+
if (
|
|
673
|
+
word === 'const' ||
|
|
674
|
+
word === 'let' ||
|
|
675
|
+
word === 'var' ||
|
|
676
|
+
word === 'function' ||
|
|
677
|
+
word === 'class' ||
|
|
678
|
+
word === 'if' ||
|
|
679
|
+
word === 'for' ||
|
|
680
|
+
word === 'switch' ||
|
|
681
|
+
word === 'try' ||
|
|
682
|
+
word === 'throw'
|
|
683
|
+
) {
|
|
684
|
+
return true;
|
|
436
685
|
}
|
|
686
|
+
|
|
687
|
+
index = skip_whitespace_from(this.input, index);
|
|
688
|
+
if (this.input.charCodeAt(index) !== CharCode.equals) return false;
|
|
689
|
+
const next = this.input.charCodeAt(index + 1);
|
|
690
|
+
return next !== CharCode.equals && next !== CharCode.greaterThan;
|
|
437
691
|
}
|
|
438
692
|
|
|
439
|
-
#
|
|
440
|
-
this
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
|
|
693
|
+
#switchCaseLabelStart(index = this.start) {
|
|
694
|
+
while (index < this.input.length) {
|
|
695
|
+
const ch = this.input.charCodeAt(index);
|
|
696
|
+
if (
|
|
697
|
+
ch !== CharCode.space &&
|
|
698
|
+
ch !== CharCode.tab &&
|
|
699
|
+
ch !== CharCode.lineFeed &&
|
|
700
|
+
ch !== CharCode.carriageReturn
|
|
701
|
+
) {
|
|
702
|
+
break;
|
|
444
703
|
}
|
|
704
|
+
index++;
|
|
705
|
+
}
|
|
706
|
+
if (!this.#isLineStartPosition(index)) return -1;
|
|
707
|
+
if (this.input.charCodeAt(index) !== CharCode.at) return -1;
|
|
708
|
+
index++;
|
|
709
|
+
if (
|
|
710
|
+
this.input.slice(index, index + 4) === 'case' &&
|
|
711
|
+
!this.#isIdentifierChar(this.input.charCodeAt(index + 4))
|
|
712
|
+
) {
|
|
713
|
+
return index;
|
|
714
|
+
}
|
|
715
|
+
if (
|
|
716
|
+
this.input.slice(index, index + 7) === 'default' &&
|
|
717
|
+
!this.#isIdentifierChar(this.input.charCodeAt(index + 7))
|
|
718
|
+
) {
|
|
719
|
+
return index;
|
|
720
|
+
}
|
|
721
|
+
return -1;
|
|
722
|
+
}
|
|
445
723
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
);
|
|
452
|
-
node.expression = expression;
|
|
453
|
-
this.#popTokenContextsAfterTemplateExpressionElement(
|
|
454
|
-
/** @type {AST.TsrxFragment | AST.TsxCompat} */ (/** @type {unknown} */ (expression)),
|
|
455
|
-
);
|
|
456
|
-
this.expect(tt.braceR);
|
|
457
|
-
return this.finishNode(node, 'JSXExpressionContainer');
|
|
458
|
-
} finally {
|
|
459
|
-
this.#tsxIslandExpressionDepth--;
|
|
724
|
+
#rewindToSwitchCaseLabel() {
|
|
725
|
+
const start = this.#switchCaseLabelStart();
|
|
726
|
+
if (start === -1) return false;
|
|
727
|
+
while (this.curContext() === tstc.tc_expr) {
|
|
728
|
+
this.context.pop();
|
|
460
729
|
}
|
|
730
|
+
this.pos = start;
|
|
731
|
+
this.start = start;
|
|
732
|
+
this.startLoc = acorn.getLineInfo(this.input, start);
|
|
733
|
+
this.exprAllowed = true;
|
|
734
|
+
this.#suppressTemplateRawTextToken = true;
|
|
735
|
+
this.next();
|
|
736
|
+
return true;
|
|
461
737
|
}
|
|
462
738
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
739
|
+
/**
|
|
740
|
+
* @param {number} index
|
|
741
|
+
*/
|
|
742
|
+
#switchCaseBoundaryStart(index) {
|
|
743
|
+
if (!this.#isLineStartPosition(index)) return -1;
|
|
744
|
+
let wordStart = index;
|
|
745
|
+
while (wordStart < this.input.length) {
|
|
746
|
+
const ch = this.input.charCodeAt(wordStart);
|
|
747
|
+
if (ch !== CharCode.space && ch !== CharCode.tab) break;
|
|
748
|
+
wordStart++;
|
|
466
749
|
}
|
|
467
750
|
|
|
468
|
-
|
|
751
|
+
const ch = this.input.charCodeAt(wordStart);
|
|
752
|
+
if (ch === CharCode.closeBrace) return index;
|
|
753
|
+
if (ch === CharCode.at) {
|
|
754
|
+
const keywordStart = wordStart + 1;
|
|
755
|
+
if (
|
|
756
|
+
this.input.slice(keywordStart, keywordStart + 4) === 'case' &&
|
|
757
|
+
!this.#isIdentifierChar(this.input.charCodeAt(keywordStart + 4))
|
|
758
|
+
) {
|
|
759
|
+
return index;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (
|
|
763
|
+
this.input.slice(keywordStart, keywordStart + 7) === 'default' &&
|
|
764
|
+
!this.#isIdentifierChar(this.input.charCodeAt(keywordStart + 7))
|
|
765
|
+
) {
|
|
766
|
+
return index;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
for (const keyword of ['break', 'continue', 'return', 'throw']) {
|
|
771
|
+
if (
|
|
772
|
+
this.input.slice(wordStart, wordStart + keyword.length) === keyword &&
|
|
773
|
+
!this.#isIdentifierChar(this.input.charCodeAt(wordStart + keyword.length))
|
|
774
|
+
) {
|
|
775
|
+
return index;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return -1;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* @param {number} ch
|
|
784
|
+
*/
|
|
785
|
+
#isIdentifierChar(ch) {
|
|
786
|
+
return (
|
|
787
|
+
(ch >= CharCode.uppercaseA && ch <= CharCode.uppercaseZ) ||
|
|
788
|
+
(ch >= CharCode.lowercaseA && ch <= CharCode.lowercaseZ) ||
|
|
789
|
+
(ch >= CharCode.digit0 && ch <= CharCode.digit9) ||
|
|
790
|
+
ch === CharCode.underscore ||
|
|
791
|
+
ch === CharCode.dollar
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* @param {number} ch
|
|
797
|
+
*/
|
|
798
|
+
#canPrecedeTypeArgumentList(ch) {
|
|
799
|
+
return this.#isIdentifierChar(ch) || ch === CharCode.closeParen;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** @this {TSRXParser & Parse.Parser} */
|
|
803
|
+
#parseJSXSwitchCaseRawText() {
|
|
804
|
+
const start = this.start;
|
|
805
|
+
let index = start;
|
|
806
|
+
let found_boundary = false;
|
|
469
807
|
while (index < this.input.length) {
|
|
808
|
+
const boundary = this.#switchCaseBoundaryStart(index);
|
|
809
|
+
if (boundary !== -1) {
|
|
810
|
+
index = boundary;
|
|
811
|
+
found_boundary = true;
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
|
|
470
815
|
const ch = this.input.charCodeAt(index);
|
|
471
816
|
if (
|
|
472
|
-
ch === CharCode.
|
|
473
|
-
ch === CharCode.
|
|
474
|
-
ch === CharCode.
|
|
475
|
-
ch === CharCode.
|
|
817
|
+
ch === CharCode.lessThan ||
|
|
818
|
+
ch === CharCode.openBrace ||
|
|
819
|
+
ch === CharCode.closeBrace ||
|
|
820
|
+
ch === CharCode.at
|
|
476
821
|
) {
|
|
477
|
-
index++;
|
|
478
|
-
} else {
|
|
479
822
|
break;
|
|
480
823
|
}
|
|
824
|
+
index++;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const endLoc = acorn.getLineInfo(this.input, index);
|
|
828
|
+
const node = /** @type {ESTreeJSX.JSXText} */ (this.startNodeAt(start, this.startLoc));
|
|
829
|
+
node.value = this.input.slice(start, index);
|
|
830
|
+
node.raw = node.value;
|
|
831
|
+
|
|
832
|
+
if (node.value.match(regex_newline_characters)) {
|
|
833
|
+
this.curLine = endLoc.line;
|
|
834
|
+
this.lineStart = index - endLoc.column;
|
|
835
|
+
}
|
|
836
|
+
this.pos = index;
|
|
837
|
+
if (found_boundary) {
|
|
838
|
+
this.#filterTemplateScriptContexts();
|
|
839
|
+
if (this.curContext() !== b_stat) {
|
|
840
|
+
this.context.push(b_stat);
|
|
841
|
+
}
|
|
842
|
+
this.exprAllowed = true;
|
|
843
|
+
this.#suppressTemplateRawTextToken = true;
|
|
481
844
|
}
|
|
845
|
+
this.next();
|
|
846
|
+
|
|
847
|
+
return this.finishNodeAt(node, 'JSXText', index, endLoc);
|
|
848
|
+
}
|
|
482
849
|
|
|
483
|
-
|
|
850
|
+
/**
|
|
851
|
+
* @param {boolean} [allow_inside_expression_container] When set, do not bail
|
|
852
|
+
* purely because we are inside a `{ … }` expression container. A JSX
|
|
853
|
+
* element nested in a container (e.g. `{<div> a</div>}`) is still a
|
|
854
|
+
* template-mode element whose text children are raw JSX text; the rest of
|
|
855
|
+
* the directive/comment/boundary checks below still apply, so a directive
|
|
856
|
+
* body inside an expression container is correctly excluded.
|
|
857
|
+
*/
|
|
858
|
+
#shouldReadTemplateRawTextToken(allow_inside_expression_container = false) {
|
|
859
|
+
if (
|
|
860
|
+
this.#closingNativeTemplateNode ||
|
|
861
|
+
this.#readingJSXControlFlowDirectiveKeyword ||
|
|
862
|
+
this.#readingJSXControlFlowHeader ||
|
|
863
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth > 0 ||
|
|
864
|
+
this.#templateScriptParsingDepth > 0 ||
|
|
865
|
+
(!allow_inside_expression_container && this.#jsxExpressionContainerDepth > 0)
|
|
866
|
+
) {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
const current_context_token = this.curContext()?.token;
|
|
870
|
+
if (current_context_token === '<tag' || current_context_token === '</tag') {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
if (this.labels.some((label) => label.kind === 'switch')) {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
const current_template_node = this.#currentNativeTemplateNode();
|
|
877
|
+
if (!current_template_node || this.#isJSXControlFlowDirectiveAt(this.pos)) {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
if (this.#isTemplateLineCommentStart(this.pos)) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
if (this.#switchCaseLabelStart(this.pos) !== -1) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
if (this.input.charCodeAt(this.pos - 1) === CharCode.lessThan) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
if (
|
|
890
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.slash &&
|
|
891
|
+
this.input.charCodeAt(this.pos - 2) === CharCode.lessThan
|
|
892
|
+
) {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
if (
|
|
896
|
+
this.input.charCodeAt(this.pos) === CharCode.slash &&
|
|
897
|
+
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
|
|
898
|
+
) {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
if (
|
|
902
|
+
this.input.charCodeAt(this.pos) === CharCode.greaterThan &&
|
|
903
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.slash &&
|
|
904
|
+
this.input.charCodeAt(this.pos - 2) === CharCode.lessThan
|
|
905
|
+
) {
|
|
484
906
|
return false;
|
|
485
907
|
}
|
|
908
|
+
// Just past a self-closing tag's `/>`: that element has no body, so any
|
|
909
|
+
// following raw text belongs to an enclosing template, not to it. With no
|
|
910
|
+
// enclosing template (e.g. a top-level `return <div />`), the trailing
|
|
911
|
+
// text is plain JS and must not be read as template raw text.
|
|
912
|
+
const opening = this.#openingNativeTemplateNode;
|
|
913
|
+
if (
|
|
914
|
+
opening &&
|
|
915
|
+
current_template_node === opening &&
|
|
916
|
+
/** @type {any} */ (opening).openingElement?.selfClosing &&
|
|
917
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.greaterThan &&
|
|
918
|
+
this.input.charCodeAt(this.pos - 2) === CharCode.slash
|
|
919
|
+
) {
|
|
920
|
+
const enclosing = this.#path.findLast(
|
|
921
|
+
(node) => node !== opening && this.#isNativeTemplateNode(node),
|
|
922
|
+
);
|
|
923
|
+
if (!enclosing) {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
#readTemplateRawTextToken() {
|
|
932
|
+
const start = this.pos;
|
|
933
|
+
const index = this.#templateRawTextEnd(start);
|
|
934
|
+
|
|
935
|
+
const endLoc = acorn.getLineInfo(this.input, index);
|
|
936
|
+
const value = this.input.slice(start, index);
|
|
937
|
+
if (value.match(regex_newline_characters)) {
|
|
938
|
+
this.curLine = endLoc.line;
|
|
939
|
+
this.lineStart = index - endLoc.column;
|
|
940
|
+
}
|
|
941
|
+
this.pos = index;
|
|
942
|
+
return this.finishToken(tstt.jsxText, value);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* @param {number} index
|
|
947
|
+
*/
|
|
948
|
+
#isTemplateLineCommentStart(index) {
|
|
949
|
+
return (
|
|
950
|
+
this.input.charCodeAt(index) === CharCode.slash &&
|
|
951
|
+
this.input.charCodeAt(index + 1) === CharCode.slash &&
|
|
952
|
+
this.#isLineStartPosition(index)
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* @param {number} start
|
|
958
|
+
*/
|
|
959
|
+
#templateRawTextEnd(start) {
|
|
960
|
+
let index = start;
|
|
961
|
+
while (index < this.input.length) {
|
|
962
|
+
const ch = this.input.charCodeAt(index);
|
|
963
|
+
if (
|
|
964
|
+
ch === CharCode.lessThan ||
|
|
965
|
+
ch === CharCode.openBrace ||
|
|
966
|
+
ch === CharCode.closeBrace ||
|
|
967
|
+
this.#isJSXControlFlowDirectiveAt(index) ||
|
|
968
|
+
this.#isTemplateLineCommentStart(index)
|
|
969
|
+
) {
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
index++;
|
|
973
|
+
}
|
|
974
|
+
return index;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* @param {number} index
|
|
979
|
+
*/
|
|
980
|
+
#isJSXControlFlowDirectiveAt(index) {
|
|
981
|
+
if (this.input.charCodeAt(index) !== CharCode.at) return false;
|
|
982
|
+
|
|
983
|
+
let cursor = index + 1;
|
|
984
|
+
if (!this.#isIdentifierChar(this.input.charCodeAt(cursor))) return false;
|
|
985
|
+
|
|
986
|
+
const word_start = cursor;
|
|
987
|
+
cursor++;
|
|
988
|
+
while (this.#isIdentifierChar(this.input.charCodeAt(cursor))) {
|
|
989
|
+
cursor++;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const word = this.input.slice(word_start, cursor);
|
|
993
|
+
const next_non_whitespace = skip_whitespace_from(this.input, cursor);
|
|
994
|
+
const next = this.input.charCodeAt(next_non_whitespace);
|
|
995
|
+
if (this.#isIdentifierChar(this.input.charCodeAt(cursor))) {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
if (word === 'try') {
|
|
999
|
+
return next === CharCode.openBrace;
|
|
1000
|
+
}
|
|
1001
|
+
if (word === 'for') {
|
|
1002
|
+
if (next === CharCode.openParen) return true;
|
|
1003
|
+
if (
|
|
1004
|
+
this.input.slice(next_non_whitespace, next_non_whitespace + 5) === 'await' &&
|
|
1005
|
+
!this.#isIdentifierChar(this.input.charCodeAt(next_non_whitespace + 5))
|
|
1006
|
+
) {
|
|
1007
|
+
const after_await = skip_whitespace_from(this.input, next_non_whitespace + 5);
|
|
1008
|
+
return this.input.charCodeAt(after_await) === CharCode.openParen;
|
|
1009
|
+
}
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
return (word === 'if' || word === 'switch') && next === CharCode.openParen;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
#isJSXControlFlowDirectiveStart() {
|
|
1016
|
+
return this.#isJSXControlFlowDirectiveAt(this.start);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* `@{ … }` code block: an `@` immediately followed by `{` at child/body
|
|
1021
|
+
* position. This is the marker that switches a body from plain JSX to a JS
|
|
1022
|
+
* code block (§2). Whitespace between `@` and `{` is not allowed — they must
|
|
1023
|
+
* be adjacent so it can never be confused with an `@directive` or a literal
|
|
1024
|
+
* `@` followed by an expression container.
|
|
1025
|
+
* @param {number} index
|
|
1026
|
+
*/
|
|
1027
|
+
#isCodeBlockStart(index) {
|
|
1028
|
+
return (
|
|
1029
|
+
this.input.charCodeAt(index) === CharCode.at &&
|
|
1030
|
+
this.input.charCodeAt(index + 1) === CharCode.openBrace
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* True when the body position starting at `this.start` opens a `@{ … }`
|
|
1036
|
+
* code block, skipping leading whitespace.
|
|
1037
|
+
*/
|
|
1038
|
+
#atCodeBlockStart() {
|
|
1039
|
+
const index = skip_whitespace_from(this.input, this.start);
|
|
1040
|
+
return this.#isCodeBlockStart(index);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* @param {AST.Node | null | undefined} node
|
|
1045
|
+
*/
|
|
1046
|
+
#isRenderOutputNode(node) {
|
|
1047
|
+
if (!node) return false;
|
|
1048
|
+
switch (node.type) {
|
|
1049
|
+
case 'JSXElement':
|
|
1050
|
+
case 'JSXFragment':
|
|
1051
|
+
case 'JSXStyleElement':
|
|
1052
|
+
case 'JSXCodeBlock':
|
|
1053
|
+
case 'JSXIfExpression':
|
|
1054
|
+
case 'JSXForExpression':
|
|
1055
|
+
case 'JSXSwitchExpression':
|
|
1056
|
+
case 'JSXTryExpression':
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* @param {AST.Node | null | undefined} node
|
|
1064
|
+
*/
|
|
1065
|
+
#isForgottenStatementContainerOutputNode(node) {
|
|
1066
|
+
return this.#isRenderOutputNode(node) && node?.type !== 'JSXCodeBlock';
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* @param {AST.Node | null | undefined} node
|
|
1071
|
+
*/
|
|
1072
|
+
#isIgnoredForgottenStatementContainerStatement(node) {
|
|
1073
|
+
return !node || node.type === 'EmptyStatement';
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* A normal function body that directly contains a bare JSX/control-flow node
|
|
1078
|
+
* almost always means the author wrote `{ ... <div /> }` but intended
|
|
1079
|
+
* `@{ ... <div /> }`. Only report when adding `@` would produce a valid
|
|
1080
|
+
* statement container: setup statements first, followed by one final render
|
|
1081
|
+
* output. Report only direct body children so ordinary nested callbacks/branches
|
|
1082
|
+
* are diagnosed by their own function body, not their parent.
|
|
1083
|
+
* @param {AST.Node} node
|
|
1084
|
+
*/
|
|
1085
|
+
#reportForgottenStatementContainerBody(node) {
|
|
1086
|
+
if (!this.#collect) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const body = /** @type {{ body?: AST.Node }} */ (node).body;
|
|
1091
|
+
if (body?.type !== 'BlockStatement') {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const statements = /** @type {AST.BlockStatement} */ (body).body || [];
|
|
1096
|
+
const has_return_type = Boolean(/** @type {{ returnType?: AST.Node }} */ (node).returnType);
|
|
1097
|
+
if (!has_return_type) {
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
let target = null;
|
|
1102
|
+
let target_index = -1;
|
|
1103
|
+
for (let index = 0; index < statements.length; index++) {
|
|
1104
|
+
const statement = statements[index];
|
|
1105
|
+
const output =
|
|
1106
|
+
this.#isForgottenStatementContainerOutputNode(statement) ||
|
|
1107
|
+
(statement.type === 'ExpressionStatement' &&
|
|
1108
|
+
this.#isForgottenStatementContainerOutputNode(statement.expression))
|
|
1109
|
+
? statement
|
|
1110
|
+
: null;
|
|
1111
|
+
|
|
1112
|
+
if (!output) {
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (target_index !== -1) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
target_index = index;
|
|
1120
|
+
target = output;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (!target) {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
for (const statement of statements.slice(target_index + 1)) {
|
|
1128
|
+
if (!this.#isIgnoredForgottenStatementContainerStatement(statement)) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
this.#report_recoverable_error_range(
|
|
1134
|
+
/** @type {number} */ (target.start),
|
|
1135
|
+
/** @type {number} */ (target.end),
|
|
1136
|
+
FORGOTTEN_STATEMENT_CONTAINER_ERROR,
|
|
1137
|
+
DIAGNOSTIC_CODES.FORGOTTEN_STATEMENT_CONTAINER,
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Inside a code block (`@{ … }` or a directive's `{ }`), decides whether the
|
|
1143
|
+
* next thing is the single bare render node (`<tag …>`, `<>…</>`, or an
|
|
1144
|
+
* `@if`/`@for`/`@switch`/`@try` directive) rather than a setup statement.
|
|
1145
|
+
*
|
|
1146
|
+
* Render output that begins with `<` is recognized by the tokenizer
|
|
1147
|
+
* (`getTokenFromCode`): it emits `jsxTagStart` for a `<` that opens a tag — at
|
|
1148
|
+
* the start of a line, or in an expression position such as after `;`/`{`/`=>` —
|
|
1149
|
+
* which the `jsxTagStart` fast path below covers. The char-based fallback for a
|
|
1150
|
+
* raw `<` therefore only treats it as render output when the tag starts its own
|
|
1151
|
+
* line or follows a `;` on the same line (so one-liners such as
|
|
1152
|
+
* `@{ const foo = 1; <>{foo}</> }` work). A `<` the tokenizer left as a
|
|
1153
|
+
* relational operator while trailing a value on the same line is the comparison
|
|
1154
|
+
* it looks like (`aaa <b` is `aaa < b`, never a `<b>` tag), so it stays setup
|
|
1155
|
+
* code rather than being mistaken for render output.
|
|
1156
|
+
*/
|
|
1157
|
+
#atRenderNodeStart() {
|
|
1158
|
+
if (this.type === tstt.jsxTagStart) return true;
|
|
1159
|
+
const index = skip_whitespace_from(this.input, this.start);
|
|
1160
|
+
const ch = this.input.charCodeAt(index);
|
|
1161
|
+
if (ch === CharCode.lessThan) {
|
|
1162
|
+
const next = this.input.charCodeAt(index + 1);
|
|
1163
|
+
if (next === CharCode.slash) return false;
|
|
1164
|
+
const tagLike =
|
|
1165
|
+
next === CharCode.greaterThan ||
|
|
1166
|
+
next === CharCode.at ||
|
|
1167
|
+
next === CharCode.dollar ||
|
|
1168
|
+
next === CharCode.underscore ||
|
|
1169
|
+
(next >= CharCode.uppercaseA && next <= CharCode.uppercaseZ) ||
|
|
1170
|
+
(next >= CharCode.lowercaseA && next <= CharCode.lowercaseZ);
|
|
1171
|
+
const previous = this.#previousNonSpaceTabIndex(index);
|
|
1172
|
+
const afterSemicolon =
|
|
1173
|
+
previous >= 0 && this.input.charCodeAt(previous) === CharCode.semicolon;
|
|
1174
|
+
return tagLike && (this.#isLineStartPosition(index) || afterSemicolon);
|
|
1175
|
+
}
|
|
1176
|
+
return this.#isCodeBlockStart(index) || this.#isJSXControlFlowDirectiveAt(index);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Parse one setup statement inside a code block as ordinary TS, with the
|
|
1181
|
+
* native-template path hidden so `<` reads as a relational/type operator
|
|
1182
|
+
* (`value < limit`, `foo<T>()`) rather than a JSX tag, and any JSX value
|
|
1183
|
+
* (`const x = <div/>`) parses as a plain JSX expression.
|
|
1184
|
+
*/
|
|
1185
|
+
#parseCodeBlockSetupStatement() {
|
|
1186
|
+
const previous_context = this.context;
|
|
1187
|
+
this.context = previous_context.filter(
|
|
1188
|
+
(context) =>
|
|
1189
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
1190
|
+
);
|
|
1191
|
+
let pushed_statement_context = false;
|
|
1192
|
+
if (this.curContext() !== b_stat) {
|
|
1193
|
+
this.context.push(b_stat);
|
|
1194
|
+
pushed_statement_context = true;
|
|
1195
|
+
}
|
|
1196
|
+
this.exprAllowed = true;
|
|
1197
|
+
const previous_path = this.#path;
|
|
1198
|
+
this.#path = [];
|
|
1199
|
+
this.#templateScriptParsingDepth++;
|
|
1200
|
+
let node;
|
|
1201
|
+
try {
|
|
1202
|
+
// A code-block/directive body is statements plus at most one render node —
|
|
1203
|
+
// never bare text or markup tokens. If the tokenizer mis-read trailing
|
|
1204
|
+
// code as JSX (raw text or a tag-name token — both can happen for a
|
|
1205
|
+
// statement following the render node, depending on the leftover context),
|
|
1206
|
+
// reposition to the token start and re-read it as code now that the
|
|
1207
|
+
// template path is hidden. It then parses as a statement so the
|
|
1208
|
+
// one-render-node rule reports a clear "statements cannot follow" error
|
|
1209
|
+
// instead of a generic parse fault.
|
|
1210
|
+
if (this.type === tstt.jsxText || this.type === tstt.jsxName) {
|
|
1211
|
+
// Rewinding `pos` to the mis-read token's start must also rewind the
|
|
1212
|
+
// line counter: a `jsxText` token can span newlines (e.g. the blank
|
|
1213
|
+
// line before a following render node), and reading it already
|
|
1214
|
+
// advanced `curLine`/`lineStart` to its end. Resetting only `pos`
|
|
1215
|
+
// would leave the line counter ahead of `pos`, inflating the `loc`
|
|
1216
|
+
// of this statement and every node after it (which crashes source-map
|
|
1217
|
+
// mapping when the inflated end line runs past the file).
|
|
1218
|
+
const loc = acorn.getLineInfo(this.input, this.start);
|
|
1219
|
+
this.pos = this.start;
|
|
1220
|
+
this.curLine = loc.line;
|
|
1221
|
+
this.lineStart = this.start - loc.column;
|
|
1222
|
+
this.nextToken();
|
|
1223
|
+
}
|
|
1224
|
+
node = this.parseStatement(null);
|
|
1225
|
+
} finally {
|
|
1226
|
+
this.#templateScriptParsingDepth--;
|
|
1227
|
+
this.#path = previous_path;
|
|
1228
|
+
if (pushed_statement_context && this.curContext() === b_stat) {
|
|
1229
|
+
this.context.pop();
|
|
1230
|
+
}
|
|
1231
|
+
this.context = previous_context;
|
|
1232
|
+
}
|
|
1233
|
+
if (this.curContext() === tstc.tc_expr) {
|
|
1234
|
+
this.context.pop();
|
|
1235
|
+
}
|
|
1236
|
+
return node;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Parse the single bare render node of a code block — a JSX element/fragment
|
|
1241
|
+
* (parsed as a native TSRX element so its own body may again be plain JSX or
|
|
1242
|
+
* a nested `@{ … }`) or an `@if`/`@for`/`@switch`/`@try` directive.
|
|
1243
|
+
*/
|
|
1244
|
+
#parseCodeBlockRenderNode() {
|
|
1245
|
+
const at_index = skip_whitespace_from(this.input, this.start);
|
|
1246
|
+
// Reposition onto the render token so it re-tokenizes in a clean context
|
|
1247
|
+
// (a preceding setup statement's context restore can strip the JSX tag
|
|
1248
|
+
// contexts the trailing `<`/`@` token first pushed).
|
|
1249
|
+
if (this.start !== at_index) {
|
|
1250
|
+
const loc = acorn.getLineInfo(this.input, at_index);
|
|
1251
|
+
this.pos = at_index;
|
|
1252
|
+
this.start = at_index;
|
|
1253
|
+
this.startLoc = new acorn.Position(loc.line, loc.column);
|
|
1254
|
+
this.curLine = loc.line;
|
|
1255
|
+
this.lineStart = at_index - loc.column;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (this.#isCodeBlockStart(at_index)) {
|
|
1259
|
+
return /** @type {AST.Node} */ (/** @type {unknown} */ (this.#parseCodeBlock()));
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (this.#isJSXControlFlowDirectiveAt(at_index)) {
|
|
1263
|
+
return /** @type {AST.Node} */ (
|
|
1264
|
+
/** @type {unknown} */ (this.#parseJSXControlFlowExpression())
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Re-read the `<` so its `jsxTagStart` pushes the opening-tag contexts.
|
|
1269
|
+
this.pos = at_index;
|
|
1270
|
+
this.exprAllowed = true;
|
|
1271
|
+
this.next();
|
|
1272
|
+
if (this.type !== tstt.jsxTagStart) {
|
|
1273
|
+
this.unexpected();
|
|
1274
|
+
}
|
|
1275
|
+
this.next();
|
|
1276
|
+
if (this.value === '/' || this.type === tt.slash) {
|
|
1277
|
+
this.unexpected();
|
|
1278
|
+
}
|
|
1279
|
+
const node = this.parseElement();
|
|
1280
|
+
if (!node) {
|
|
1281
|
+
this.unexpected();
|
|
1282
|
+
}
|
|
1283
|
+
if (this.curContext() === tstc.tc_expr) {
|
|
1284
|
+
this.context.pop();
|
|
1285
|
+
}
|
|
1286
|
+
return /** @type {AST.Node} */ (/** @type {unknown} */ (node));
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Shared `Statement* RenderOutput?` grammar for the body of a `@{ … }` code
|
|
1291
|
+
* block and the `{ }` of an `@if`/`@for`/`@switch`/`@try` directive (§2
|
|
1292
|
+
* rules 4–8). Fills `flat` with the setup statements followed by at most one
|
|
1293
|
+
* trailing render node. Leaves the tokenizer positioned at the closing `}`.
|
|
1294
|
+
* @param {AST.Node[]} flat
|
|
1295
|
+
*/
|
|
1296
|
+
#parseCodeBlockBody(flat) {
|
|
1297
|
+
let render_seen = false;
|
|
1298
|
+
while (this.type !== tt.braceR && this.type !== tt.eof) {
|
|
1299
|
+
// A bare `;` is an empty statement carrying no meaning. JSX render
|
|
1300
|
+
// output does not consume a trailing `;`, so one written after the
|
|
1301
|
+
// render node (`<>…</>;`) would otherwise parse as a statement and
|
|
1302
|
+
// trip the "statements cannot follow the rendered output" rule. Skip
|
|
1303
|
+
// stray semicolons silently here; prettier strips them on format.
|
|
1304
|
+
if (this.type === tt.semi) {
|
|
1305
|
+
this.next();
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (this.#atRenderNodeStart()) {
|
|
1309
|
+
const render_node = this.#parseCodeBlockRenderNode();
|
|
1310
|
+
if (render_seen) {
|
|
1311
|
+
this.#report_recoverable_error_range(
|
|
1312
|
+
/** @type {number} */ (render_node.start),
|
|
1313
|
+
/** @type {number} */ (render_node.end),
|
|
1314
|
+
"A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
flat.push(render_node);
|
|
1318
|
+
render_seen = true;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const statement = this.#parseCodeBlockSetupStatement();
|
|
1322
|
+
if (statement) {
|
|
1323
|
+
if (render_seen) {
|
|
1324
|
+
// A statement after the rendered output: code must come first.
|
|
1325
|
+
this.#report_recoverable_error_range(
|
|
1326
|
+
/** @type {number} */ (statement.start),
|
|
1327
|
+
/** @type {number} */ (statement.end),
|
|
1328
|
+
"Code must be at the top of '@{ }'; statements cannot follow the rendered output.",
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
flat.push(statement);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Parse an explicit `@{ … }` code block (`this.start` at `@`). Returns a
|
|
1338
|
+
* `JSXCodeBlock` whose `body` holds the setup statements and `render` the
|
|
1339
|
+
* single optional render output (§9).
|
|
1340
|
+
*/
|
|
1341
|
+
#parseCodeBlock({ allowReturnStatements = false } = {}) {
|
|
1342
|
+
const start = this.start;
|
|
1343
|
+
const startLoc = this.startLoc;
|
|
1344
|
+
const node = /** @type {AST.JSXCodeBlock} */ (this.startNodeAt(start, startLoc));
|
|
1345
|
+
node.body = [];
|
|
1346
|
+
node.render = null;
|
|
1347
|
+
node.metadata = { path: [] };
|
|
1348
|
+
|
|
1349
|
+
// The body parses as JS, so swap the surrounding JSX/template token
|
|
1350
|
+
// contexts for a clean statement context and hide the enclosing template
|
|
1351
|
+
// from `#path` so the body tokenizes as code (not JSX raw text). Both are
|
|
1352
|
+
// restored before the closing `}` is consumed so the following `</tag>`
|
|
1353
|
+
// tokenizes against the same template context the body opened in.
|
|
1354
|
+
const enclosing_context = this.context;
|
|
1355
|
+
const enclosing_path = this.#path;
|
|
1356
|
+
const braceStart = start + 1;
|
|
1357
|
+
this.context = enclosing_context.filter(
|
|
1358
|
+
(context) =>
|
|
1359
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
1360
|
+
);
|
|
1361
|
+
if (this.curContext() !== b_stat) {
|
|
1362
|
+
this.context.push(b_stat);
|
|
1363
|
+
}
|
|
1364
|
+
const braceLoc = acorn.getLineInfo(this.input, braceStart);
|
|
1365
|
+
this.pos = braceStart;
|
|
1366
|
+
this.start = braceStart;
|
|
1367
|
+
this.startLoc = new acorn.Position(braceLoc.line, braceLoc.column);
|
|
1368
|
+
this.curLine = braceLoc.line;
|
|
1369
|
+
this.lineStart = braceStart - braceLoc.column;
|
|
1370
|
+
this.exprAllowed = true;
|
|
1371
|
+
this.#path = [];
|
|
1372
|
+
this.next();
|
|
1373
|
+
this.expect(tt.braceL);
|
|
1374
|
+
|
|
1375
|
+
/** @type {AST.Node[]} */
|
|
1376
|
+
const flat = [];
|
|
1377
|
+
this.enterScope(0);
|
|
1378
|
+
try {
|
|
1379
|
+
this.#parseCodeBlockBody(flat);
|
|
1380
|
+
} finally {
|
|
1381
|
+
this.exitScope();
|
|
1382
|
+
this.#path = enclosing_path;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const last = flat[flat.length - 1];
|
|
1386
|
+
if (this.#isRenderOutputNode(last)) {
|
|
1387
|
+
node.render = last;
|
|
1388
|
+
node.body = /** @type {AST.Statement[]} */ (flat.slice(0, -1));
|
|
1389
|
+
} else {
|
|
1390
|
+
node.body = /** @type {AST.Statement[]} */ (flat);
|
|
1391
|
+
}
|
|
1392
|
+
if (!allowReturnStatements) {
|
|
1393
|
+
this.#report_invalid_template_return_statements(node.body);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (this.type !== tt.braceR) {
|
|
1397
|
+
this.unexpected();
|
|
1398
|
+
}
|
|
1399
|
+
// Restore the enclosing template context, then consume `}` and read the
|
|
1400
|
+
// following token (typically the parent's `</tag>`) against it. Finish the
|
|
1401
|
+
// node after the `}` so its range spans the whole `@{ … }` (this is what
|
|
1402
|
+
// lets trailing comments before `}` attach to the block, not the parent's
|
|
1403
|
+
// closing tag).
|
|
1404
|
+
const brace_close_end = this.end;
|
|
1405
|
+
const brace_close_end_loc = this.endLoc;
|
|
1406
|
+
this.context = enclosing_context;
|
|
1407
|
+
this.next();
|
|
1408
|
+
this.finishNodeAt(node, 'JSXCodeBlock', brace_close_end, brace_close_end_loc);
|
|
1409
|
+
return node;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* At-sign constructs are expressions (§6a, §2 rule 9): code blocks and the
|
|
1414
|
+
* if/for/switch/try directive forms may be returned, assigned, or passed
|
|
1415
|
+
* anywhere an expression is expected. Only code blocks and the four reserved
|
|
1416
|
+
* control-flow keywords are intercepted; any other at-sign form, such as a
|
|
1417
|
+
* decorated class expression, falls through so decorators keep working.
|
|
1418
|
+
* @type {Parse.Parser['parseExprAtom']}
|
|
1419
|
+
*/
|
|
1420
|
+
parseExprAtom(refDestructuringErrors, forInit, forNew) {
|
|
1421
|
+
if (this.input.charCodeAt(this.start) === CharCode.at) {
|
|
1422
|
+
if (this.#isCodeBlockStart(this.start)) {
|
|
1423
|
+
return /** @type {any} */ (this.#parseCodeBlock());
|
|
1424
|
+
}
|
|
1425
|
+
if (this.#isJSXControlFlowDirectiveAt(this.start)) {
|
|
1426
|
+
return /** @type {any} */ (this.#parseJSXControlFlowExpression());
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return super.parseExprAtom(refDestructuringErrors, forInit, forNew);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* @param {AST.Node} node
|
|
1434
|
+
* @param {string} type
|
|
1435
|
+
* @param {number} start
|
|
1436
|
+
* @param {AST.Position} startLoc
|
|
1437
|
+
*/
|
|
1438
|
+
#finishJSXControlFlowExpression(node, type, start, startLoc) {
|
|
1439
|
+
node.start = start;
|
|
1440
|
+
/** @type {AST.NodeWithLocation} */ (node).loc.start = startLoc;
|
|
1441
|
+
node.metadata ??= { path: [] };
|
|
1442
|
+
/** @type {any} */ (node).statementType = node.type;
|
|
1443
|
+
/** @type {any} */ (node).type = type;
|
|
1444
|
+
return node;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Drop the JSX tokenizer contexts (`tc_expr`/`tc_oTag`/`tc_cTag`) so the
|
|
1449
|
+
* directive header/body tokenizes as JavaScript, while preserving every
|
|
1450
|
+
* context below the innermost open `{ … }` expression container. Those lower
|
|
1451
|
+
* contexts belong to the enclosing markup (the container brace, the element
|
|
1452
|
+
* that holds the `{ … }`, any outer fragment); a plain filter would drop them
|
|
1453
|
+
* too and underflow the context stack when that markup later closes. Outside
|
|
1454
|
+
* any expression container the baseline is 0, so this matches the original
|
|
1455
|
+
* "strip everything" behavior the bare-template path relies on.
|
|
1456
|
+
*/
|
|
1457
|
+
#filterTemplateScriptContexts() {
|
|
1458
|
+
const baseline = this.#expressionContainerContextBaselines.at(-1) ?? 0;
|
|
1459
|
+
this.context = this.context.filter(
|
|
1460
|
+
(context, index) =>
|
|
1461
|
+
index < baseline ||
|
|
1462
|
+
(context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag),
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
#parseJSXControlFlowExpression() {
|
|
1467
|
+
const start = this.start;
|
|
1468
|
+
const startLoc = this.startLoc;
|
|
1469
|
+
const keywordStart = start + 1;
|
|
1470
|
+
this.pos = keywordStart;
|
|
1471
|
+
this.start = keywordStart;
|
|
1472
|
+
this.startLoc = acorn.getLineInfo(this.input, keywordStart);
|
|
1473
|
+
this.curLine = this.startLoc.line;
|
|
1474
|
+
this.lineStart = keywordStart - this.startLoc.column;
|
|
1475
|
+
this.#filterTemplateScriptContexts();
|
|
1476
|
+
if (this.curContext() !== b_stat) {
|
|
1477
|
+
this.context.push(b_stat);
|
|
1478
|
+
}
|
|
1479
|
+
this.exprAllowed = true;
|
|
1480
|
+
this.#readingJSXControlFlowDirectiveKeyword = true;
|
|
1481
|
+
try {
|
|
1482
|
+
this.nextToken();
|
|
1483
|
+
} finally {
|
|
1484
|
+
this.#readingJSXControlFlowDirectiveKeyword = false;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const label = this.type.keyword || this.type.label || this.value;
|
|
1488
|
+
if (label === 'if') {
|
|
1489
|
+
return this.#finishJSXControlFlowExpression(
|
|
1490
|
+
this.#parseTemplateIfStatement(),
|
|
1491
|
+
'JSXIfExpression',
|
|
1492
|
+
start,
|
|
1493
|
+
startLoc,
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
if (label === 'for') {
|
|
1498
|
+
this.#templateControlFlowBlockDepth++;
|
|
1499
|
+
let node;
|
|
1500
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
1501
|
+
this.#readingJSXControlFlowHeader = true;
|
|
1502
|
+
try {
|
|
1503
|
+
node = this.#finishJSXControlFlowExpression(
|
|
1504
|
+
this.parseStatement(null),
|
|
1505
|
+
'JSXForExpression',
|
|
1506
|
+
start,
|
|
1507
|
+
startLoc,
|
|
1508
|
+
);
|
|
1509
|
+
} finally {
|
|
1510
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
1511
|
+
this.#templateControlFlowBlockDepth--;
|
|
1512
|
+
}
|
|
1513
|
+
if (
|
|
1514
|
+
/** @type {any} */ (node).statementType !== 'ForOfStatement' &&
|
|
1515
|
+
/** @type {any} */ (node).statementType !== 'ForInStatement' &&
|
|
1516
|
+
/** @type {any} */ (node).statementType !== 'ForStatement'
|
|
1517
|
+
) {
|
|
1518
|
+
this.raise(start, 'Expected `for` after `@`.');
|
|
1519
|
+
}
|
|
1520
|
+
if (/** @type {any} */ (node).body?.type !== 'BlockStatement') {
|
|
1521
|
+
this.raise(
|
|
1522
|
+
/** @type {any} */ (node).body?.start ?? start,
|
|
1523
|
+
'Expected `{` after JSX control-flow directive.',
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
if (this.#eatJSXForEmptyKeyword()) {
|
|
1527
|
+
if (this.type !== tt.braceL) {
|
|
1528
|
+
this.raise(this.start, 'Expected `{` after JSX control-flow directive.');
|
|
1529
|
+
}
|
|
1530
|
+
this.#templateControlFlowBlockDepth++;
|
|
1531
|
+
try {
|
|
1532
|
+
/** @type {any} */ (node).empty = this.parseBlock();
|
|
1533
|
+
} finally {
|
|
1534
|
+
this.#templateControlFlowBlockDepth--;
|
|
1535
|
+
}
|
|
1536
|
+
} else if (this.#isUnprefixedDirectiveClauseContinuation('empty', ['{'])) {
|
|
1537
|
+
this.raise(this.start, 'Expected `@empty` after `@for` block.');
|
|
1538
|
+
} else {
|
|
1539
|
+
/** @type {any} */ (node).empty = null;
|
|
1540
|
+
}
|
|
1541
|
+
return node;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (label === 'switch') {
|
|
1545
|
+
return this.#parseJSXSwitchExpression(start, startLoc);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
if (label === 'try') {
|
|
1549
|
+
this.#templateControlFlowTryDepth++;
|
|
1550
|
+
try {
|
|
1551
|
+
return this.#finishJSXControlFlowExpression(
|
|
1552
|
+
this.parseStatement(null),
|
|
1553
|
+
'JSXTryExpression',
|
|
1554
|
+
start,
|
|
1555
|
+
startLoc,
|
|
1556
|
+
);
|
|
1557
|
+
} finally {
|
|
1558
|
+
this.#templateControlFlowTryDepth--;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
this.raise(start, 'Expected `@if`, `@for`, `@switch`, or `@try`.');
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* @param {string} keyword
|
|
1567
|
+
*/
|
|
1568
|
+
#eatJSXDirectiveClauseKeyword(keyword) {
|
|
1569
|
+
const keywordStart = skip_whitespace_from(this.input, this.start);
|
|
1570
|
+
if (this.input.charCodeAt(keywordStart) !== CharCode.at) {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
const wordStart = keywordStart + 1;
|
|
1574
|
+
if (
|
|
1575
|
+
this.input.slice(wordStart, wordStart + keyword.length) !== keyword ||
|
|
1576
|
+
this.#isIdentifierChar(this.input.charCodeAt(wordStart + keyword.length))
|
|
1577
|
+
) {
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
this.pos = wordStart;
|
|
1582
|
+
this.start = wordStart;
|
|
1583
|
+
this.startLoc = acorn.getLineInfo(this.input, wordStart);
|
|
1584
|
+
this.curLine = this.startLoc.line;
|
|
1585
|
+
this.lineStart = wordStart - this.startLoc.column;
|
|
1586
|
+
this.#filterTemplateScriptContexts();
|
|
1587
|
+
if (this.curContext() !== b_stat) {
|
|
1588
|
+
this.context.push(b_stat);
|
|
1589
|
+
}
|
|
1590
|
+
this.exprAllowed = true;
|
|
1591
|
+
this.#readingJSXControlFlowDirectiveKeyword = true;
|
|
1592
|
+
try {
|
|
1593
|
+
this.nextToken();
|
|
1594
|
+
} finally {
|
|
1595
|
+
this.#readingJSXControlFlowDirectiveKeyword = false;
|
|
1596
|
+
}
|
|
1597
|
+
this.next();
|
|
1598
|
+
return true;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
#eatJSXForEmptyKeyword() {
|
|
1602
|
+
return this.#eatJSXDirectiveClauseKeyword('empty');
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* @param {string} keyword
|
|
1607
|
+
*/
|
|
1608
|
+
#eatJSXDirectiveBareClauseKeyword(keyword) {
|
|
1609
|
+
const wordStart = skip_whitespace_from(this.input, this.start);
|
|
1610
|
+
if (
|
|
1611
|
+
this.input.slice(wordStart, wordStart + keyword.length) !== keyword ||
|
|
1612
|
+
this.#isIdentifierChar(this.input.charCodeAt(wordStart + keyword.length))
|
|
1613
|
+
) {
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
this.pos = wordStart;
|
|
1618
|
+
this.start = wordStart;
|
|
1619
|
+
this.startLoc = acorn.getLineInfo(this.input, wordStart);
|
|
1620
|
+
this.curLine = this.startLoc.line;
|
|
1621
|
+
this.lineStart = wordStart - this.startLoc.column;
|
|
1622
|
+
this.#filterTemplateScriptContexts();
|
|
1623
|
+
if (this.curContext() !== b_stat) {
|
|
1624
|
+
this.context.push(b_stat);
|
|
1625
|
+
}
|
|
1626
|
+
this.exprAllowed = true;
|
|
1627
|
+
this.#readingJSXControlFlowDirectiveKeyword = true;
|
|
1628
|
+
try {
|
|
1629
|
+
this.nextToken();
|
|
1630
|
+
} finally {
|
|
1631
|
+
this.#readingJSXControlFlowDirectiveKeyword = false;
|
|
1632
|
+
}
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* @param {string} keyword
|
|
1638
|
+
* @param {string[]} continuations
|
|
1639
|
+
*/
|
|
1640
|
+
#isUnprefixedDirectiveClauseContinuation(keyword, continuations) {
|
|
1641
|
+
const keywordStart = skip_whitespace_from(this.input, this.start);
|
|
1642
|
+
if (
|
|
1643
|
+
this.input.slice(keywordStart, keywordStart + keyword.length) !== keyword ||
|
|
1644
|
+
this.#isIdentifierChar(this.input.charCodeAt(keywordStart + keyword.length))
|
|
1645
|
+
) {
|
|
1646
|
+
return false;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const continuationStart = skip_whitespace_from(this.input, keywordStart + keyword.length);
|
|
1650
|
+
for (const continuation of continuations) {
|
|
1651
|
+
if (continuation.length === 1 && this.input[continuationStart] === continuation) {
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
if (
|
|
1655
|
+
this.input.slice(continuationStart, continuationStart + continuation.length) ===
|
|
1656
|
+
continuation &&
|
|
1657
|
+
!this.#isIdentifierChar(this.input.charCodeAt(continuationStart + continuation.length))
|
|
1658
|
+
) {
|
|
1659
|
+
return true;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/**
|
|
1666
|
+
* @returns {'case' | 'default' | null}
|
|
1667
|
+
*/
|
|
1668
|
+
#eatJSXSwitchCaseClauseKeyword() {
|
|
1669
|
+
if (this.#eatJSXDirectiveClauseKeyword('case')) {
|
|
1670
|
+
return 'case';
|
|
1671
|
+
}
|
|
1672
|
+
if (this.#eatJSXDirectiveClauseKeyword('default')) {
|
|
1673
|
+
return 'default';
|
|
1674
|
+
}
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
#parseTemplateControlFlowStatement() {
|
|
1679
|
+
if (this.type !== tt.braceL) {
|
|
1680
|
+
this.raise(this.start, 'Expected `{` after JSX control-flow directive.');
|
|
1681
|
+
}
|
|
1682
|
+
return this.#parseTemplateControlFlowBlock();
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
#parseTemplateIfStatement() {
|
|
1686
|
+
const node = /** @type {AST.IfStatement} */ (this.startNode());
|
|
1687
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
1688
|
+
this.#readingJSXControlFlowHeader = true;
|
|
1689
|
+
try {
|
|
1690
|
+
this.next();
|
|
1691
|
+
node.test = this.parseParenExpression();
|
|
1692
|
+
} finally {
|
|
1693
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
1694
|
+
}
|
|
1695
|
+
node.consequent = /** @type {AST.Statement} */ (this.#parseTemplateControlFlowStatement());
|
|
1696
|
+
node.alternate = null;
|
|
1697
|
+
|
|
1698
|
+
if (this.#eatJSXDirectiveClauseKeyword('else')) {
|
|
1699
|
+
node.alternate = this.#eatJSXDirectiveBareClauseKeyword('if')
|
|
1700
|
+
? this.#parseTemplateIfStatement()
|
|
1701
|
+
: /** @type {AST.Statement} */ (this.#parseTemplateControlFlowStatement());
|
|
1702
|
+
} else if (this.#isUnprefixedDirectiveClauseContinuation('else', ['{', 'if'])) {
|
|
1703
|
+
this.raise(this.start, 'Expected `@else` after `@if` block.');
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
return this.finishNode(node, 'IfStatement');
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* @param {number} start
|
|
1711
|
+
* @param {AST.Position} startLoc
|
|
1712
|
+
*/
|
|
1713
|
+
#parseJSXSwitchExpression(start, startLoc) {
|
|
1714
|
+
const node = /** @type {AST.SwitchStatement} */ (this.startNodeAt(start, startLoc));
|
|
1715
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
1716
|
+
this.#readingJSXControlFlowHeader = true;
|
|
1717
|
+
try {
|
|
1718
|
+
this.next();
|
|
1719
|
+
node.discriminant = this.parseParenExpression();
|
|
1720
|
+
} finally {
|
|
1721
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
1722
|
+
}
|
|
1723
|
+
node.cases = [];
|
|
1724
|
+
this.expect(tt.braceL);
|
|
1725
|
+
this.labels.push({ kind: 'switch' });
|
|
1726
|
+
this.enterScope(0);
|
|
1727
|
+
|
|
1728
|
+
let sawDefault = false;
|
|
1729
|
+
while (this.type !== tt.braceR) {
|
|
1730
|
+
if (this.type === tstt.jsxText && this.#rewindToSwitchCaseLabel()) {
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const clauseStart = this.start;
|
|
1735
|
+
const clauseStartLoc = this.startLoc;
|
|
1736
|
+
const clause = this.#eatJSXSwitchCaseClauseKeyword();
|
|
1737
|
+
if (clause) {
|
|
1738
|
+
const isCase = clause === 'case';
|
|
1739
|
+
const current = /** @type {AST.SwitchCase} */ (
|
|
1740
|
+
this.startNodeAt(clauseStart, clauseStartLoc)
|
|
1741
|
+
);
|
|
1742
|
+
current.consequent = [];
|
|
1743
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
1744
|
+
this.#readingJSXControlFlowHeader = true;
|
|
1745
|
+
try {
|
|
1746
|
+
if (isCase) {
|
|
1747
|
+
current.test = this.parseExpression();
|
|
1748
|
+
} else {
|
|
1749
|
+
if (sawDefault) {
|
|
1750
|
+
this.raiseRecoverable(this.lastTokStart, 'Multiple default clauses');
|
|
1751
|
+
}
|
|
1752
|
+
sawDefault = true;
|
|
1753
|
+
current.test = null;
|
|
1754
|
+
}
|
|
1755
|
+
this.expect(tt.colon);
|
|
1756
|
+
} finally {
|
|
1757
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
1758
|
+
}
|
|
1759
|
+
this.expect(tt.braceL);
|
|
1760
|
+
while (this.type !== tt.braceR) {
|
|
1761
|
+
this.#parseJSXSwitchCaseConsequent(current.consequent);
|
|
1762
|
+
}
|
|
1763
|
+
this.expect(tt.braceR);
|
|
1764
|
+
node.cases.push(this.finishNode(current, 'SwitchCase'));
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
this.unexpected();
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
this.exitScope();
|
|
1772
|
+
this.next();
|
|
1773
|
+
this.labels.pop();
|
|
1774
|
+
return this.#finishJSXControlFlowExpression(
|
|
1775
|
+
this.finishNode(node, 'SwitchStatement'),
|
|
1776
|
+
'JSXSwitchExpression',
|
|
1777
|
+
start,
|
|
1778
|
+
startLoc,
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* @param {AST.Node[]} consequent
|
|
1784
|
+
* @this {TSRXParser & Parse.Parser}
|
|
1785
|
+
*/
|
|
1786
|
+
#parseJSXSwitchCaseConsequent(consequent) {
|
|
1787
|
+
if (this.type === tt.braceL) {
|
|
1788
|
+
consequent.push(this.#parseNativeTemplateExpressionContainer());
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// A non-whitespace, non-directive case consequent that the tokenizer read
|
|
1793
|
+
// as raw text is a setup statement (in the new design bare text must be
|
|
1794
|
+
// wrapped in `<>`, so anything left here is code, e.g.
|
|
1795
|
+
// `props.status satisfies never`, `doThing()`, `x = 1`). Re-tokenize it as
|
|
1796
|
+
// JS and parse it as a statement instead of treating it as text.
|
|
1797
|
+
if (
|
|
1798
|
+
this.type === tstt.jsxText &&
|
|
1799
|
+
String(this.value ?? '').trim() !== '' &&
|
|
1800
|
+
!this.#isJSXControlFlowDirectiveStart() &&
|
|
1801
|
+
this.#switchCaseLabelStart(this.start) === -1
|
|
1802
|
+
) {
|
|
1803
|
+
const raw = String(this.value ?? '').trimStart();
|
|
1804
|
+
if (/^break\b/.test(raw)) {
|
|
1805
|
+
this.raise(this.start, '`break` is invalid inside `@switch` cases.');
|
|
1806
|
+
}
|
|
1807
|
+
if (/^return\b/.test(raw)) {
|
|
1808
|
+
this.raise(this.start, '`return` is invalid inside `@switch` cases.');
|
|
1809
|
+
}
|
|
1810
|
+
this.#filterTemplateScriptContexts();
|
|
1811
|
+
this.pos = this.start;
|
|
1812
|
+
this.startLoc = this.curPosition();
|
|
1813
|
+
if (this.curContext() !== b_stat) {
|
|
1814
|
+
this.context.push(b_stat);
|
|
1815
|
+
}
|
|
1816
|
+
this.exprAllowed = true;
|
|
1817
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth++;
|
|
1818
|
+
try {
|
|
1819
|
+
this.#suppressTemplateRawTextToken = true;
|
|
1820
|
+
this.next();
|
|
1821
|
+
consequent.push(this.parseStatement(null));
|
|
1822
|
+
} finally {
|
|
1823
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth--;
|
|
1824
|
+
}
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
if (this.type === tstt.jsxText) {
|
|
1829
|
+
const text = this.#parseJSXSwitchCaseRawText();
|
|
1830
|
+
if (!isWhitespaceTextNode(text)) {
|
|
1831
|
+
consequent.push(/** @type {any} */ (text));
|
|
1832
|
+
}
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
if (
|
|
1837
|
+
this.type === tstt.jsxTagStart ||
|
|
1838
|
+
this.input.charCodeAt(this.start) === CharCode.lessThan
|
|
1839
|
+
) {
|
|
1840
|
+
const startPos = this.start;
|
|
1841
|
+
const startLoc = this.startLoc;
|
|
1842
|
+
if (this.type === tstt.jsxTagStart) {
|
|
1843
|
+
this.next();
|
|
1844
|
+
} else {
|
|
1845
|
+
this.pos = startPos + 1;
|
|
1846
|
+
this.type = tstt.jsxTagStart;
|
|
1847
|
+
this.start = startPos;
|
|
1848
|
+
this.startLoc = startLoc;
|
|
1849
|
+
this.exprAllowed = false;
|
|
1850
|
+
this.next();
|
|
1851
|
+
}
|
|
1852
|
+
if (this.value === '/' || this.type === tt.slash) {
|
|
1853
|
+
this.unexpected();
|
|
1854
|
+
}
|
|
1855
|
+
const node = this.parseElement();
|
|
1856
|
+
if (!node) {
|
|
1857
|
+
this.unexpected();
|
|
1858
|
+
}
|
|
1859
|
+
consequent.push(/** @type {any} */ (node));
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
if (this.#isJSXControlFlowDirectiveStart()) {
|
|
1864
|
+
consequent.push(/** @type {any} */ (this.#parseJSXControlFlowExpression()));
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
if (this.#isSwitchCaseScriptStatementStart()) {
|
|
1869
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth++;
|
|
1870
|
+
try {
|
|
1871
|
+
consequent.push(this.parseStatement(null));
|
|
1872
|
+
} finally {
|
|
1873
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth--;
|
|
1874
|
+
}
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
const label = this.type.keyword || this.type.label;
|
|
1879
|
+
if (label === 'break') {
|
|
1880
|
+
this.raise(this.start, '`break` is invalid inside `@switch` cases.');
|
|
1881
|
+
}
|
|
1882
|
+
if (label === 'return') {
|
|
1883
|
+
this.raise(this.start, '`return` is invalid inside `@switch` cases.');
|
|
1884
|
+
}
|
|
1885
|
+
if (label === 'continue' || label === 'throw') {
|
|
1886
|
+
consequent.push(this.parseStatement(null));
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// Anything else here is JS read as ordinary tokens (e.g.
|
|
1891
|
+
// `props.status satisfies never`, `doThing()`): a setup statement, not text
|
|
1892
|
+
// (bare text in a case must be wrapped in `<>`). Clear the JSX/template
|
|
1893
|
+
// token contexts so the statement and the following `}`/`case` tokenize as
|
|
1894
|
+
// code.
|
|
1895
|
+
if (this.type !== tstt.jsxText && this.type !== tt.eof) {
|
|
1896
|
+
this.#filterTemplateScriptContexts();
|
|
1897
|
+
if (this.curContext() !== b_stat) {
|
|
1898
|
+
this.context.push(b_stat);
|
|
1899
|
+
}
|
|
1900
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth++;
|
|
1901
|
+
try {
|
|
1902
|
+
consequent.push(this.parseStatement(null));
|
|
1903
|
+
} finally {
|
|
1904
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth--;
|
|
1905
|
+
}
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
486
1908
|
|
|
487
|
-
|
|
1909
|
+
const text = this.#parseJSXSwitchCaseRawText();
|
|
1910
|
+
if (!isWhitespaceTextNode(text)) {
|
|
1911
|
+
consequent.push(text);
|
|
1912
|
+
}
|
|
488
1913
|
}
|
|
489
1914
|
|
|
490
1915
|
/**
|
|
491
|
-
* @param {
|
|
1916
|
+
* @param {ESTreeJSX.JSXOpeningElement} openingElement
|
|
1917
|
+
* @returns {ESTreeJSX.JSXOpeningFragment}
|
|
492
1918
|
*/
|
|
493
|
-
#
|
|
494
|
-
|
|
1919
|
+
#toOpeningFragment(openingElement) {
|
|
1920
|
+
const openingFragment = /** @type {ESTreeJSX.JSXOpeningFragment} */ (
|
|
1921
|
+
/** @type {unknown} */ (openingElement)
|
|
1922
|
+
);
|
|
1923
|
+
openingFragment.type = 'JSXOpeningFragment';
|
|
1924
|
+
delete (/** @type {any} */ (openingFragment).name);
|
|
1925
|
+
delete (/** @type {any} */ (openingFragment).attributes);
|
|
1926
|
+
delete (/** @type {any} */ (openingFragment).selfClosing);
|
|
1927
|
+
return openingFragment;
|
|
495
1928
|
}
|
|
496
1929
|
|
|
497
1930
|
/**
|
|
1931
|
+
* @param {ESTreeJSX.JSXClosingElement} closingElement
|
|
1932
|
+
* @returns {ESTreeJSX.JSXClosingFragment}
|
|
498
1933
|
*/
|
|
499
|
-
#
|
|
500
|
-
|
|
1934
|
+
#toClosingFragment(closingElement) {
|
|
1935
|
+
const closingFragment = /** @type {ESTreeJSX.JSXClosingFragment} */ (
|
|
1936
|
+
/** @type {unknown} */ (closingElement)
|
|
1937
|
+
);
|
|
1938
|
+
closingFragment.type = 'JSXClosingFragment';
|
|
1939
|
+
delete (/** @type {any} */ (closingFragment).name);
|
|
1940
|
+
return closingFragment;
|
|
501
1941
|
}
|
|
502
1942
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1943
|
+
/**
|
|
1944
|
+
* @param {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} open
|
|
1945
|
+
* @param {AST.JSXStyleElement} node
|
|
1946
|
+
* @param {boolean} insideHead
|
|
1947
|
+
*/
|
|
1948
|
+
#parseStyleElement(open, node, insideHead) {
|
|
1949
|
+
const contentStart = open.end;
|
|
1950
|
+
const input = this.input.slice(contentStart);
|
|
1951
|
+
const relativeCloseStart = input.indexOf('</style>');
|
|
1952
|
+
const content = relativeCloseStart === -1 ? input : input.slice(0, relativeCloseStart);
|
|
1953
|
+
const parsedCss = parse_style(content, { loose: this.#loose });
|
|
1954
|
+
|
|
1955
|
+
if (!insideHead) {
|
|
1956
|
+
node.metadata.styleScopeHash = parsedCss.hash;
|
|
1957
|
+
}
|
|
507
1958
|
|
|
508
|
-
|
|
509
|
-
|
|
1959
|
+
const newLines = content.match(regex_newline_characters)?.length;
|
|
1960
|
+
if (newLines) {
|
|
1961
|
+
this.curLine = open.loc.end.line + newLines;
|
|
1962
|
+
this.lineStart = contentStart + content.lastIndexOf('\n') + 1;
|
|
1963
|
+
}
|
|
510
1964
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
1965
|
+
if (relativeCloseStart !== -1) {
|
|
1966
|
+
const closingStart = contentStart + content.length;
|
|
1967
|
+
const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
|
|
1968
|
+
const closingStartLoc = new acorn.Position(closingLineInfo.line, closingLineInfo.column);
|
|
1969
|
+
const nameStart = closingStart + 2;
|
|
1970
|
+
const nameEnd = nameStart + 'style'.length;
|
|
1971
|
+
const nameStartInfo = acorn.getLineInfo(this.input, nameStart);
|
|
1972
|
+
const nameEndInfo = acorn.getLineInfo(this.input, nameEnd);
|
|
1973
|
+
const name = /** @type {ESTreeJSX.JSXIdentifier} */ (
|
|
1974
|
+
this.startNodeAt(
|
|
1975
|
+
nameStart,
|
|
1976
|
+
new acorn.Position(nameStartInfo.line, nameStartInfo.column),
|
|
1977
|
+
)
|
|
1978
|
+
);
|
|
1979
|
+
name.name = 'style';
|
|
1980
|
+
this.finishNodeAt(
|
|
1981
|
+
name,
|
|
1982
|
+
'JSXIdentifier',
|
|
1983
|
+
nameEnd,
|
|
1984
|
+
new acorn.Position(nameEndInfo.line, nameEndInfo.column),
|
|
1985
|
+
);
|
|
1986
|
+
const closingEnd = closingStart + '</style>'.length;
|
|
1987
|
+
const closingEndInfo = acorn.getLineInfo(this.input, closingEnd);
|
|
1988
|
+
const closingElement = /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
|
|
1989
|
+
this.startNodeAt(closingStart, closingStartLoc)
|
|
1990
|
+
);
|
|
1991
|
+
closingElement.name = name;
|
|
1992
|
+
this.finishNodeAt(
|
|
1993
|
+
closingElement,
|
|
1994
|
+
'JSXClosingElement',
|
|
1995
|
+
closingEnd,
|
|
1996
|
+
new acorn.Position(closingEndInfo.line, closingEndInfo.column),
|
|
1997
|
+
);
|
|
1998
|
+
node.closingElement = closingElement;
|
|
1999
|
+
const parent = this.#path.at(-2);
|
|
2000
|
+
const insideTemplate = this.#isNativeTemplateNode(parent);
|
|
2001
|
+
if (this.curContext() === tstc.tc_expr && !insideTemplate) {
|
|
2002
|
+
this.context.pop();
|
|
514
2003
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
this.
|
|
2004
|
+
this.exprAllowed = false;
|
|
2005
|
+
this.pos = closingEnd;
|
|
2006
|
+
this.curLine = closingEndInfo.line;
|
|
2007
|
+
this.lineStart = closingEnd - closingEndInfo.column;
|
|
2008
|
+
if (insideTemplate && relativeCloseStart === 0) {
|
|
2009
|
+
// Acorn has already tokenized the adjacent </style>; TSRX synthesizes
|
|
2010
|
+
// that close manually, so drop the stale style tag context.
|
|
2011
|
+
if (this.curContext() === tstc.tc_oTag) {
|
|
2012
|
+
this.context.pop();
|
|
2013
|
+
}
|
|
2014
|
+
if (this.curContext() === tstc.tc_expr) {
|
|
2015
|
+
this.context.pop();
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
if (!insideTemplate && this.#path.at(-1) === node) {
|
|
2019
|
+
this.#path.pop();
|
|
2020
|
+
try {
|
|
2021
|
+
this.next();
|
|
2022
|
+
} finally {
|
|
2023
|
+
this.#path.push(node);
|
|
2024
|
+
}
|
|
2025
|
+
} else {
|
|
2026
|
+
this.next();
|
|
2027
|
+
}
|
|
2028
|
+
} else {
|
|
2029
|
+
this.#report_broken_markup_error(
|
|
2030
|
+
open.end,
|
|
2031
|
+
"Unclosed tag '<style>'. Expected '</style>' before end of template.",
|
|
2032
|
+
);
|
|
2033
|
+
node.unclosed = true;
|
|
518
2034
|
}
|
|
519
2035
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
2036
|
+
node.css = content;
|
|
2037
|
+
node.children = [parsedCss];
|
|
2038
|
+
}
|
|
523
2039
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
2040
|
+
#parseNativeTemplateExpressionContainer() {
|
|
2041
|
+
const allow_trailing_semicolon = this.#allowExpressionContainerTrailingSemicolon;
|
|
2042
|
+
this.#allowExpressionContainerTrailingSemicolon = true;
|
|
2043
|
+
// One-shot: marks this as a template *child* container (not an attribute
|
|
2044
|
+
// value or script-mode JSX child), so `jsx_parseExpressionContainer`
|
|
2045
|
+
// consumes the closing `}` after leaving container scope.
|
|
2046
|
+
this.#consumeContainerBraceAfterScope = true;
|
|
2047
|
+
let node;
|
|
2048
|
+
try {
|
|
2049
|
+
node = this.jsx_parseExpressionContainer();
|
|
2050
|
+
} finally {
|
|
2051
|
+
this.#allowExpressionContainerTrailingSemicolon = allow_trailing_semicolon;
|
|
2052
|
+
this.#consumeContainerBraceAfterScope = false;
|
|
2053
|
+
}
|
|
2054
|
+
return /** @type {ESTreeJSX.JSXExpressionContainer} */ (/** @type {unknown} */ (node));
|
|
531
2055
|
}
|
|
532
2056
|
|
|
533
|
-
#
|
|
2057
|
+
#popTemplateTokenContextBeforeExpressionChild() {
|
|
534
2058
|
let index = this.pos;
|
|
535
2059
|
let has_newline = false;
|
|
536
2060
|
|
|
537
|
-
//
|
|
2061
|
+
// JSXText-only template fragments can leave the tokenizer in JSX text mode.
|
|
538
2062
|
// Only unwind it for ASI before a following TSRX `{expr}` child;
|
|
539
2063
|
// fragment props like `content={<></>}` still need the JSX context.
|
|
540
2064
|
while (index < this.input.length) {
|
|
@@ -631,19 +2155,37 @@ export function TSRXPlugin(config) {
|
|
|
631
2155
|
}
|
|
632
2156
|
|
|
633
2157
|
/**
|
|
634
|
-
* @param {
|
|
2158
|
+
* @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
|
|
635
2159
|
* @returns {boolean}
|
|
636
2160
|
*/
|
|
637
2161
|
#hasDirectStatementChild(node) {
|
|
638
|
-
|
|
2162
|
+
const children = /** @type {AST.Node[]} */ (/** @type {unknown} */ (node.children ?? []));
|
|
2163
|
+
return children.some(
|
|
639
2164
|
(child) => child.type.endsWith('Statement') || child.type === 'VariableDeclaration',
|
|
640
2165
|
);
|
|
641
2166
|
}
|
|
642
2167
|
|
|
643
2168
|
/**
|
|
644
|
-
* @param {
|
|
2169
|
+
* @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
|
|
645
2170
|
*/
|
|
646
2171
|
#popTokenContextsAfterTemplateExpressionElement(node) {
|
|
2172
|
+
// A fragment in expression position (`() => <>…</>`) leaves the tokenizer
|
|
2173
|
+
// at `exprAllowed === false`, unlike a self-closing element. When the next
|
|
2174
|
+
// token is a `;` or ASI can insert one, the following statement may
|
|
2175
|
+
// legitimately open with a JSX tag (`<List/>`), so restore expression
|
|
2176
|
+
// position to match the element path.
|
|
2177
|
+
if ((this.type === tt.semi || this.canInsertSemicolon()) && node.type === 'JSXFragment') {
|
|
2178
|
+
this.exprAllowed = true;
|
|
2179
|
+
}
|
|
2180
|
+
// A JSX element/fragment used as a ternary consequent (`cond ? <a>…</a> : …`)
|
|
2181
|
+
// likewise leaves the tokenizer at `exprAllowed === false`, so the `<` after
|
|
2182
|
+
// the `:` would not start a tag. Restore expression position so the alternate
|
|
2183
|
+
// branch parses as JSX too. This applies to both elements and fragments,
|
|
2184
|
+
// unlike the `;`/ASI case above (a `:` only follows a value, so the next
|
|
2185
|
+
// token always begins the alternate expression).
|
|
2186
|
+
if (this.type === tt.colon) {
|
|
2187
|
+
this.exprAllowed = true;
|
|
2188
|
+
}
|
|
647
2189
|
const ctx = this.context;
|
|
648
2190
|
const ci = ctx.length - 1;
|
|
649
2191
|
const top = ctx[ci];
|
|
@@ -734,70 +2276,6 @@ export function TSRXPlugin(config) {
|
|
|
734
2276
|
}
|
|
735
2277
|
}
|
|
736
2278
|
|
|
737
|
-
#isDoubleQuotedTextChildStart() {
|
|
738
|
-
const current_template_node = this.#path.findLast(
|
|
739
|
-
(n) => n.type === 'Element' || n.type === 'TsrxFragment' || n.type === 'TsxCompat',
|
|
740
|
-
);
|
|
741
|
-
if (current_template_node?.type === 'TsxCompat') {
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const parent = this.#path.at(-1);
|
|
746
|
-
if (!parent || (parent.type !== 'Element' && parent.type !== 'TsrxFragment')) {
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const context = this.curContext();
|
|
751
|
-
if (context === tstc.tc_oTag || context === tstc.tc_cTag) {
|
|
752
|
-
return false;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const prev = this.#previousNonWhitespaceChar();
|
|
756
|
-
return (
|
|
757
|
-
prev === null ||
|
|
758
|
-
prev === CharCode.doubleQuote ||
|
|
759
|
-
prev === CharCode.semicolon ||
|
|
760
|
-
prev === CharCode.greaterThan ||
|
|
761
|
-
(prev === CharCode.openBrace && this.#allowDoubleQuotedTextChildAfterBrace) ||
|
|
762
|
-
prev === CharCode.closeBrace
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
#readDoubleQuotedTextChildToken() {
|
|
767
|
-
const start = this.pos;
|
|
768
|
-
let out = '';
|
|
769
|
-
this.pos++;
|
|
770
|
-
let chunkStart = this.pos;
|
|
771
|
-
|
|
772
|
-
while (this.pos < this.input.length) {
|
|
773
|
-
const ch = this.input.charCodeAt(this.pos);
|
|
774
|
-
|
|
775
|
-
if (ch === CharCode.doubleQuote) {
|
|
776
|
-
out += this.input.slice(chunkStart, this.pos);
|
|
777
|
-
this.pos++;
|
|
778
|
-
return this.finishToken(tt.string, out);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (ch === CharCode.ampersand) {
|
|
782
|
-
out += this.input.slice(chunkStart, this.pos);
|
|
783
|
-
out += this.jsx_readEntity();
|
|
784
|
-
chunkStart = this.pos;
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
if (acorn.isNewLine(ch)) {
|
|
789
|
-
out += this.input.slice(chunkStart, this.pos);
|
|
790
|
-
out += this.jsx_readNewLine(true);
|
|
791
|
-
chunkStart = this.pos;
|
|
792
|
-
continue;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
this.pos++;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
this.raise(start, 'Unterminated double-quoted text child');
|
|
799
|
-
}
|
|
800
|
-
|
|
801
2279
|
/**
|
|
802
2280
|
* @param {number} position
|
|
803
2281
|
* @param {number} end
|
|
@@ -888,8 +2366,9 @@ export function TSRXPlugin(config) {
|
|
|
888
2366
|
...node.metadata,
|
|
889
2367
|
invalid_tsrx_template_return: true,
|
|
890
2368
|
};
|
|
891
|
-
this.#
|
|
2369
|
+
this.#report_recoverable_error_range(
|
|
892
2370
|
/** @type {AST.NodeWithLocation} */ (node).start ?? this.start,
|
|
2371
|
+
/** @type {AST.NodeWithLocation} */ (node).end ?? this.start + 1,
|
|
893
2372
|
TSRX_RETURN_STATEMENT_ERROR,
|
|
894
2373
|
DIAGNOSTIC_CODES.TEMPLATE_RETURN_STATEMENT,
|
|
895
2374
|
);
|
|
@@ -1064,13 +2543,15 @@ export function TSRXPlugin(config) {
|
|
|
1064
2543
|
}
|
|
1065
2544
|
|
|
1066
2545
|
const container = this.#path[this.#path.length - 1];
|
|
1067
|
-
if (!container
|
|
2546
|
+
if (!this.#isNativeTemplateNode(container)) {
|
|
1068
2547
|
return null;
|
|
1069
2548
|
}
|
|
1070
2549
|
|
|
1071
|
-
const children = Array.isArray(
|
|
2550
|
+
const children = Array.isArray(/** @type {any} */ (container).children)
|
|
2551
|
+
? /** @type {any} */ (container).children
|
|
2552
|
+
: [];
|
|
1072
2553
|
const hasMeaningfulChildren = children.some(
|
|
1073
|
-
(child) => child && !isWhitespaceTextNode(child),
|
|
2554
|
+
(/** @type {any} */ child) => child && !isWhitespaceTextNode(child),
|
|
1074
2555
|
);
|
|
1075
2556
|
|
|
1076
2557
|
if (hasMeaningfulChildren) {
|
|
@@ -1115,9 +2596,58 @@ export function TSRXPlugin(config) {
|
|
|
1115
2596
|
* @type {Parse.Parser['readToken']}
|
|
1116
2597
|
*/
|
|
1117
2598
|
readToken(code) {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
2599
|
+
const suppressTemplateRawTextToken = this.#suppressTemplateRawTextToken;
|
|
2600
|
+
this.#suppressTemplateRawTextToken = false;
|
|
2601
|
+
const context = this.curContext();
|
|
2602
|
+
if (
|
|
2603
|
+
code !== CharCode.lessThan &&
|
|
2604
|
+
code !== CharCode.greaterThan &&
|
|
2605
|
+
code !== CharCode.openBrace &&
|
|
2606
|
+
code !== CharCode.closeBrace &&
|
|
2607
|
+
!suppressTemplateRawTextToken &&
|
|
2608
|
+
this.#shouldReadTemplateRawTextToken()
|
|
2609
|
+
) {
|
|
2610
|
+
return this.#readTemplateRawTextToken();
|
|
2611
|
+
}
|
|
2612
|
+
if (
|
|
2613
|
+
code === CharCode.greaterThan &&
|
|
2614
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.equals
|
|
2615
|
+
) {
|
|
2616
|
+
const start = this.pos - 1;
|
|
2617
|
+
const loc = acorn.getLineInfo(this.input, start);
|
|
2618
|
+
this.start = start;
|
|
2619
|
+
this.startLoc = loc;
|
|
2620
|
+
this.pos++;
|
|
2621
|
+
return this.finishToken(tt.arrow);
|
|
2622
|
+
}
|
|
2623
|
+
if (code === CharCode.lessThan) {
|
|
2624
|
+
const next = this.input.charCodeAt(this.pos + 1);
|
|
2625
|
+
if (
|
|
2626
|
+
next !== CharCode.slash &&
|
|
2627
|
+
(looks_like_generic_arrow(this.input, this.pos) ||
|
|
2628
|
+
this.#canStartTypeParameterOrArgumentList(this.pos))
|
|
2629
|
+
) {
|
|
2630
|
+
++this.pos;
|
|
2631
|
+
return this.finishToken(tt.relational, '<');
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if (context === tstc.tc_expr || context === tstc.tc_oTag || context === tstc.tc_cTag) {
|
|
2635
|
+
return super.readToken(code);
|
|
2636
|
+
}
|
|
2637
|
+
if (code === CharCode.lessThan) {
|
|
2638
|
+
const next = this.input.charCodeAt(this.pos + 1);
|
|
2639
|
+
const isTagLikeAfterLt =
|
|
2640
|
+
next === CharCode.slash ||
|
|
2641
|
+
next === CharCode.greaterThan ||
|
|
2642
|
+
next === CharCode.at ||
|
|
2643
|
+
next === CharCode.dollar ||
|
|
2644
|
+
next === CharCode.underscore ||
|
|
2645
|
+
(next >= CharCode.uppercaseA && next <= CharCode.uppercaseZ) ||
|
|
2646
|
+
(next >= CharCode.lowercaseA && next <= CharCode.lowercaseZ);
|
|
2647
|
+
if (this.exprAllowed && isTagLikeAfterLt) {
|
|
2648
|
+
++this.pos;
|
|
2649
|
+
return this.finishToken(tstt.jsxTagStart);
|
|
2650
|
+
}
|
|
1121
2651
|
}
|
|
1122
2652
|
return super.readToken(code);
|
|
1123
2653
|
}
|
|
@@ -1127,74 +2657,91 @@ export function TSRXPlugin(config) {
|
|
|
1127
2657
|
* @type {Parse.Parser['getTokenFromCode']}
|
|
1128
2658
|
*/
|
|
1129
2659
|
getTokenFromCode(code) {
|
|
2660
|
+
// acorn-typescript only recognizes `@` as the at-token when it is not
|
|
2661
|
+
// reading a type. A return-type annotation (`function f(): T @{ … }`)
|
|
2662
|
+
// finishes while still `inType`, so its trailing `@` reaches the base
|
|
2663
|
+
// tokenizer, which throws "Unexpected character '@'". Emit the at-token
|
|
2664
|
+
// here so the `@{ … }` code block that follows the type can be parsed.
|
|
2665
|
+
if (code === CharCode.at && this.inType) {
|
|
2666
|
+
++this.pos;
|
|
2667
|
+
return this.finishToken(tstt.at);
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
if (
|
|
2671
|
+
code === CharCode.greaterThan &&
|
|
2672
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.equals
|
|
2673
|
+
) {
|
|
2674
|
+
const start = this.pos - 1;
|
|
2675
|
+
const loc = acorn.getLineInfo(this.input, start);
|
|
2676
|
+
this.start = start;
|
|
2677
|
+
this.startLoc = loc;
|
|
2678
|
+
this.pos++;
|
|
2679
|
+
return this.finishToken(tt.arrow);
|
|
2680
|
+
}
|
|
2681
|
+
|
|
1130
2682
|
// Callback props that return native templates without a semicolon can
|
|
1131
2683
|
// leave the attribute expression context above the still-open tag. Drop
|
|
1132
2684
|
// it before tokenizing `/>`, otherwise Acorn treats `/` as a regexp.
|
|
1133
2685
|
if (
|
|
1134
2686
|
code === CharCode.slash &&
|
|
1135
|
-
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
|
|
1136
|
-
this.context.includes(tstc.tc_oTag)
|
|
2687
|
+
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
|
|
1137
2688
|
) {
|
|
1138
|
-
while (
|
|
2689
|
+
while (
|
|
2690
|
+
this.context.length > 0 &&
|
|
2691
|
+
this.curContext() !== tstc.tc_oTag &&
|
|
2692
|
+
this.curContext() !== tstc.tc_expr
|
|
2693
|
+
) {
|
|
1139
2694
|
this.context.pop();
|
|
1140
2695
|
}
|
|
1141
|
-
this.
|
|
1142
|
-
|
|
1143
|
-
if (code === CharCode.doubleQuote) {
|
|
1144
|
-
const is_double_quoted_text_child = this.#isDoubleQuotedTextChildStart();
|
|
1145
|
-
this.#allowDoubleQuotedTextChildAfterBrace = false;
|
|
1146
|
-
if (is_double_quoted_text_child) {
|
|
1147
|
-
return this.#readDoubleQuotedTextChildToken();
|
|
2696
|
+
if (this.curContext() !== tstc.tc_oTag) {
|
|
2697
|
+
this.context.push(tstc.tc_oTag);
|
|
1148
2698
|
}
|
|
1149
|
-
|
|
1150
|
-
this.#allowDoubleQuotedTextChildAfterBrace = false;
|
|
2699
|
+
this.exprAllowed = false;
|
|
1151
2700
|
}
|
|
1152
2701
|
|
|
1153
|
-
if (
|
|
1154
|
-
|
|
2702
|
+
if (
|
|
2703
|
+
(code === CharCode.numberSign || code === CharCode.slash) &&
|
|
2704
|
+
this.#functionBodyDepth === 0 &&
|
|
2705
|
+
this.#isNativeTemplateNode(this.#path.at(-1)) &&
|
|
2706
|
+
!(
|
|
2707
|
+
code === CharCode.slash &&
|
|
2708
|
+
(this.input.charCodeAt(this.pos - 1) === CharCode.lessThan ||
|
|
2709
|
+
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan)
|
|
2710
|
+
)
|
|
2711
|
+
) {
|
|
2712
|
+
++this.pos;
|
|
2713
|
+
return this.finishToken(tt.name, this.input.slice(this.start, this.pos));
|
|
1155
2714
|
}
|
|
1156
2715
|
|
|
1157
2716
|
if (code === CharCode.lessThan) {
|
|
1158
2717
|
// < character
|
|
1159
2718
|
const parent = this.#path.at(-1);
|
|
1160
2719
|
const inNativeTemplate =
|
|
1161
|
-
this.#functionBodyDepth === 0 &&
|
|
1162
|
-
(parent?.type === 'Element' || parent?.type === 'TsrxFragment');
|
|
2720
|
+
this.#functionBodyDepth === 0 && this.#isNativeTemplateNode(parent);
|
|
1163
2721
|
/** @type {number | null} */
|
|
1164
2722
|
let prevNonWhitespaceChar = null;
|
|
2723
|
+
const nextChar =
|
|
2724
|
+
this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
|
|
1165
2725
|
|
|
1166
2726
|
// Check if this could be TypeScript generics instead of JSX
|
|
1167
|
-
// TypeScript generics appear
|
|
1168
|
-
//
|
|
2727
|
+
// TypeScript generics usually appear adjacent to an expression token,
|
|
2728
|
+
// for example: Array<T>, func<T>(), new Map<K,V>(), method<T>().
|
|
1169
2729
|
// This check applies everywhere, not just inside components
|
|
1170
2730
|
|
|
1171
2731
|
// Look back to see what precedes the <
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
// Skip whitespace backwards
|
|
1175
|
-
while (lookback >= 0) {
|
|
1176
|
-
const ch = this.input.charCodeAt(lookback);
|
|
1177
|
-
if (ch !== CharCode.space && ch !== CharCode.tab) break; // not space or tab
|
|
1178
|
-
lookback--;
|
|
1179
|
-
}
|
|
2732
|
+
const lookback = this.#previousNonSpaceTabIndex(this.pos);
|
|
1180
2733
|
|
|
1181
2734
|
// Check what character/token precedes the <
|
|
1182
2735
|
if (lookback >= 0) {
|
|
1183
2736
|
const prevChar = this.input.charCodeAt(lookback);
|
|
1184
2737
|
prevNonWhitespaceChar = prevChar;
|
|
1185
2738
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
prevChar === CharCode.underscore ||
|
|
1193
|
-
prevChar === CharCode.dollar ||
|
|
1194
|
-
prevChar === CharCode.closeParen;
|
|
1195
|
-
|
|
1196
|
-
if (isIdentifierChar) {
|
|
1197
|
-
return super.getTokenFromCode(code);
|
|
2739
|
+
if (
|
|
2740
|
+
nextChar !== CharCode.slash &&
|
|
2741
|
+
this.#canStartTypeParameterOrArgumentList(this.pos)
|
|
2742
|
+
) {
|
|
2743
|
+
++this.pos;
|
|
2744
|
+
return this.finishToken(tt.relational, '<');
|
|
1198
2745
|
}
|
|
1199
2746
|
}
|
|
1200
2747
|
|
|
@@ -1203,8 +2750,6 @@ export function TSRXPlugin(config) {
|
|
|
1203
2750
|
// <Something>...</Something>\n\n<Child />
|
|
1204
2751
|
// <head><style>...</style></head>
|
|
1205
2752
|
// We only do this when '<' is in a tag-like position.
|
|
1206
|
-
const nextChar =
|
|
1207
|
-
this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
|
|
1208
2753
|
const isWhitespaceAfterLt =
|
|
1209
2754
|
nextChar === CharCode.space ||
|
|
1210
2755
|
nextChar === CharCode.tab ||
|
|
@@ -1227,6 +2772,11 @@ export function TSRXPlugin(config) {
|
|
|
1227
2772
|
prevNonWhitespaceChar === CharCode.closeBrace ||
|
|
1228
2773
|
prevNonWhitespaceChar === CharCode.greaterThan;
|
|
1229
2774
|
|
|
2775
|
+
if (!inNativeTemplate && this.exprAllowed && isTagLikeAfterLt) {
|
|
2776
|
+
++this.pos;
|
|
2777
|
+
return this.finishToken(tstt.jsxTagStart);
|
|
2778
|
+
}
|
|
2779
|
+
|
|
1230
2780
|
if (!inNativeTemplate && prevAllowsTagStart && isTagLikeAfterLt) {
|
|
1231
2781
|
++this.pos;
|
|
1232
2782
|
return this.finishToken(tstt.jsxTagStart);
|
|
@@ -1237,13 +2787,10 @@ export function TSRXPlugin(config) {
|
|
|
1237
2787
|
// a newline/indentation before the next '<'. This is important for inputs
|
|
1238
2788
|
// like `<div />` and `</div><style>...</style>` which Prettier formats.
|
|
1239
2789
|
if (
|
|
1240
|
-
(prevNonWhitespaceChar === CharCode.doubleQuote &&
|
|
1241
|
-
this.#allowTagStartAfterDoubleQuotedText) ||
|
|
1242
2790
|
prevNonWhitespaceChar === CharCode.openBrace ||
|
|
1243
2791
|
prevNonWhitespaceChar === CharCode.greaterThan
|
|
1244
2792
|
) {
|
|
1245
2793
|
if (!isWhitespaceAfterLt) {
|
|
1246
|
-
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
1247
2794
|
++this.pos;
|
|
1248
2795
|
return this.finishToken(tstt.jsxTagStart);
|
|
1249
2796
|
}
|
|
@@ -1282,7 +2829,6 @@ export function TSRXPlugin(config) {
|
|
|
1282
2829
|
}
|
|
1283
2830
|
}
|
|
1284
2831
|
|
|
1285
|
-
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
1286
2832
|
return super.getTokenFromCode(code);
|
|
1287
2833
|
}
|
|
1288
2834
|
|
|
@@ -1601,7 +3147,13 @@ export function TSRXPlugin(config) {
|
|
|
1601
3147
|
}
|
|
1602
3148
|
|
|
1603
3149
|
this.expect(tt.parenR);
|
|
1604
|
-
|
|
3150
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
3151
|
+
this.#readingJSXControlFlowHeader = false;
|
|
3152
|
+
try {
|
|
3153
|
+
node.body = /** @type {AST.BlockStatement} */ (this.parseStatement('for'));
|
|
3154
|
+
} finally {
|
|
3155
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
3156
|
+
}
|
|
1605
3157
|
this.exitScope();
|
|
1606
3158
|
this.labels.pop();
|
|
1607
3159
|
return this.finishNode(node, isForIn ? 'ForInStatement' : 'ForOfStatement');
|
|
@@ -1613,7 +3165,27 @@ export function TSRXPlugin(config) {
|
|
|
1613
3165
|
parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
|
|
1614
3166
|
this.#functionBodyDepth++;
|
|
1615
3167
|
try {
|
|
1616
|
-
|
|
3168
|
+
// Allow a `@{ … }` code block as the body of a function, method, or
|
|
3169
|
+
// arrow function, so components can be written as `function Something()
|
|
3170
|
+
// @{ … }`, `{ Render() @{ … } }`, or `const Something = () => @{ … }`.
|
|
3171
|
+
//
|
|
3172
|
+
// A return-type annotation sits between the params and the body
|
|
3173
|
+
// (`function f(): T @{ … }`). acorn-typescript parses it inside
|
|
3174
|
+
// `super.parseFunctionBody` and then demands a `{` block, so the `@{ … }`
|
|
3175
|
+
// would never be seen. Parse the return type here first (exactly as
|
|
3176
|
+
// acorn-typescript does) so `this.start` lands on the `@` that follows.
|
|
3177
|
+
if (!isArrowFunction && this.match(tt.colon)) {
|
|
3178
|
+
node.returnType = this.tsParseTypeOrTypePredicateAnnotation(tt.colon);
|
|
3179
|
+
}
|
|
3180
|
+
if (this.#isCodeBlockStart(this.start)) {
|
|
3181
|
+
node.body = this.#parseCodeBlock({ allowReturnStatements: true });
|
|
3182
|
+
this.checkParams(node, false);
|
|
3183
|
+
this.exitScope();
|
|
3184
|
+
return node;
|
|
3185
|
+
}
|
|
3186
|
+
const parsed = super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
|
|
3187
|
+
this.#reportForgottenStatementContainerBody(parsed);
|
|
3188
|
+
return parsed;
|
|
1617
3189
|
} finally {
|
|
1618
3190
|
this.#functionBodyDepth--;
|
|
1619
3191
|
}
|
|
@@ -1623,22 +3195,52 @@ export function TSRXPlugin(config) {
|
|
|
1623
3195
|
* @return {ESTreeJSX.JSXExpressionContainer}
|
|
1624
3196
|
*/
|
|
1625
3197
|
jsx_parseExpressionContainer() {
|
|
3198
|
+
// Template child containers consume `}` after leaving container scope, so
|
|
3199
|
+
// the following sibling — which may be raw template text — tokenizes
|
|
3200
|
+
// normally (acorn already preserves whitespace in the surrounding
|
|
3201
|
+
// `tc_expr` context). Attribute-value and script-mode JSX containers keep
|
|
3202
|
+
// consuming `}` in scope: their following token is part of the tag or JS,
|
|
3203
|
+
// never template text.
|
|
3204
|
+
const consumeBraceAfterScope = this.#consumeContainerBraceAfterScope;
|
|
3205
|
+
this.#consumeContainerBraceAfterScope = false;
|
|
1626
3206
|
let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
|
|
1627
|
-
this
|
|
3207
|
+
this.#jsxExpressionContainerDepth++;
|
|
3208
|
+
let pushed_context_baseline = false;
|
|
3209
|
+
try {
|
|
3210
|
+
this.next();
|
|
1628
3211
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
);
|
|
3212
|
+
// Record the context-stack depth now that the container's `{` brace
|
|
3213
|
+
// context is on the stack. A control-flow directive parsed inside this
|
|
3214
|
+
// container must not strip anything below this floor (see
|
|
3215
|
+
// `#filterTemplateScriptContexts`).
|
|
3216
|
+
this.#expressionContainerContextBaselines.push(this.context.length);
|
|
3217
|
+
pushed_context_baseline = true;
|
|
3218
|
+
|
|
3219
|
+
node.expression =
|
|
3220
|
+
this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
|
|
3221
|
+
if (this.#allowExpressionContainerTrailingSemicolon && this.type === tt.semi) {
|
|
3222
|
+
if (this.#collect) {
|
|
3223
|
+
this.#report_recoverable_error(
|
|
3224
|
+
this.start,
|
|
3225
|
+
'TSRX expression containers do not use semicolons. Remove this semicolon.',
|
|
3226
|
+
DIAGNOSTIC_CODES.TEMPLATE_EXPRESSION_TRAILING_SEMICOLON,
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
this.next();
|
|
1638
3230
|
}
|
|
1639
|
-
|
|
3231
|
+
if (!consumeBraceAfterScope) {
|
|
3232
|
+
this.expect(tt.braceR);
|
|
3233
|
+
}
|
|
3234
|
+
} finally {
|
|
3235
|
+
this.#jsxExpressionContainerDepth--;
|
|
3236
|
+
if (pushed_context_baseline) {
|
|
3237
|
+
this.#expressionContainerContextBaselines.pop();
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
if (consumeBraceAfterScope) {
|
|
3242
|
+
this.expect(tt.braceR);
|
|
1640
3243
|
}
|
|
1641
|
-
this.expect(tt.braceR);
|
|
1642
3244
|
|
|
1643
3245
|
return this.finishNode(node, 'JSXExpressionContainer');
|
|
1644
3246
|
}
|
|
@@ -1671,90 +3273,137 @@ export function TSRXPlugin(config) {
|
|
|
1671
3273
|
);
|
|
1672
3274
|
}
|
|
1673
3275
|
|
|
1674
|
-
/**
|
|
1675
|
-
* @returns {AST.TextNode}
|
|
1676
|
-
*/
|
|
1677
|
-
parseDoubleQuotedTextChild() {
|
|
1678
|
-
const node = /** @type {AST.TextNode} */ (this.startNode());
|
|
1679
|
-
const expression = /** @type {AST.Literal} */ (this.startNode());
|
|
1680
|
-
node.raw = this.input.slice(this.start, this.end);
|
|
1681
|
-
const end = this.end;
|
|
1682
|
-
const endLoc = this.endLoc;
|
|
1683
|
-
|
|
1684
|
-
expression.value = this.value;
|
|
1685
|
-
expression.raw = JSON.stringify(this.value);
|
|
1686
|
-
node.expression = this.finishNodeAt(expression, 'Literal', end, endLoc);
|
|
1687
|
-
|
|
1688
|
-
this.#allowTagStartAfterDoubleQuotedText = true;
|
|
1689
|
-
try {
|
|
1690
|
-
this.next();
|
|
1691
|
-
} finally {
|
|
1692
|
-
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
return this.finishNodeAt(node, 'Text', end, endLoc);
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
3276
|
/**
|
|
1699
3277
|
* @type {Parse.Parser['jsx_parseAttribute']}
|
|
1700
3278
|
*/
|
|
1701
3279
|
jsx_parseAttribute() {
|
|
1702
|
-
let node =
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
3280
|
+
let node = /** @type {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} */ (
|
|
3281
|
+
this.startNode()
|
|
3282
|
+
);
|
|
3283
|
+
|
|
3284
|
+
if (this.type === tt.braceL) {
|
|
3285
|
+
let name_start = skip_whitespace_from(this.input, this.start + 1);
|
|
3286
|
+
const first = this.input.charCodeAt(name_start);
|
|
3287
|
+
if (
|
|
3288
|
+
this.#isIdentifierChar(first) &&
|
|
3289
|
+
!(first >= CharCode.digit0 && first <= CharCode.digit9)
|
|
3290
|
+
) {
|
|
3291
|
+
let name_end = name_start + 1;
|
|
3292
|
+
while (this.#isIdentifierChar(this.input.charCodeAt(name_end))) {
|
|
3293
|
+
name_end++;
|
|
3294
|
+
}
|
|
3295
|
+
const brace_start = skip_whitespace_from(this.input, name_end);
|
|
3296
|
+
if (this.input.charCodeAt(brace_start) === CharCode.closeBrace) {
|
|
3297
|
+
const name_start_loc = acorn.getLineInfo(this.input, name_start);
|
|
3298
|
+
const name_end_loc = acorn.getLineInfo(this.input, name_end);
|
|
3299
|
+
const name_value = this.input.slice(name_start, name_end);
|
|
3300
|
+
const id = /** @type {ESTreeJSX.JSXIdentifier} */ (
|
|
3301
|
+
this.startNodeAt(name_start, name_start_loc)
|
|
3302
|
+
);
|
|
3303
|
+
id.name = name_value;
|
|
3304
|
+
this.finishNodeAt(id, 'JSXIdentifier', name_end, name_end_loc);
|
|
3305
|
+
const name = /** @type {AST.Identifier} */ (
|
|
3306
|
+
this.startNodeAt(name_start, name_start_loc)
|
|
3307
|
+
);
|
|
3308
|
+
name.name = name_value;
|
|
3309
|
+
this.finishNodeAt(name, 'Identifier', name_end, name_end_loc);
|
|
3310
|
+
const expression = /** @type {ESTreeJSX.JSXExpressionContainer} */ (
|
|
3311
|
+
this.startNodeAt(this.start, this.startLoc)
|
|
3312
|
+
);
|
|
3313
|
+
expression.expression = name;
|
|
3314
|
+
this.finishNodeAt(
|
|
3315
|
+
expression,
|
|
3316
|
+
'JSXExpressionContainer',
|
|
3317
|
+
brace_start + 1,
|
|
3318
|
+
acorn.getLineInfo(this.input, brace_start + 1),
|
|
3319
|
+
);
|
|
3320
|
+
/** @type {ESTreeJSX.JSXAttribute} */ (node).name = id;
|
|
3321
|
+
/** @type {any} */ (node).value = expression;
|
|
3322
|
+
/** @type {any} */ (node).shorthand = true;
|
|
3323
|
+
|
|
3324
|
+
const end = brace_start + 1;
|
|
3325
|
+
const endLoc = acorn.getLineInfo(this.input, end);
|
|
3326
|
+
this.pos = end;
|
|
3327
|
+
this.curLine = endLoc.line;
|
|
3328
|
+
this.lineStart = end - endLoc.column;
|
|
3329
|
+
if (this.curContext()?.token === '{') {
|
|
3330
|
+
this.context.pop();
|
|
3331
|
+
}
|
|
3332
|
+
this.exprAllowed = false;
|
|
3333
|
+
this.next();
|
|
3334
|
+
return this.finishNodeAt(node, 'JSXAttribute', end, endLoc);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
1706
3338
|
|
|
1707
3339
|
if (this.eat(tt.braceL)) {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
);
|
|
1711
|
-
if (current_template_node?.type === 'TsxCompat') {
|
|
3340
|
+
if (this.type === tt.ellipsis || this.input.slice(this.start, this.start + 3) === '...') {
|
|
3341
|
+
this.#suppressTemplateRawTextToken = true;
|
|
1712
3342
|
if (this.type === tt.ellipsis) {
|
|
1713
3343
|
this.expect(tt.ellipsis);
|
|
3344
|
+
} else {
|
|
3345
|
+
this.pos = this.start + 3;
|
|
3346
|
+
this.nextToken();
|
|
3347
|
+
}
|
|
3348
|
+
this.#templateScriptParsingDepth++;
|
|
3349
|
+
try {
|
|
1714
3350
|
/** @type {ESTreeJSX.JSXSpreadAttribute} */ (node).argument = this.parseMaybeAssign();
|
|
1715
|
-
|
|
1716
|
-
|
|
3351
|
+
} finally {
|
|
3352
|
+
this.#templateScriptParsingDepth--;
|
|
1717
3353
|
}
|
|
1718
|
-
this.unexpected();
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
if (this.type === tt.ellipsis) {
|
|
1722
|
-
this.expect(tt.ellipsis);
|
|
1723
|
-
/** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
|
|
1724
3354
|
this.expect(tt.braceR);
|
|
1725
|
-
return this.finishNode(node, '
|
|
3355
|
+
return this.finishNode(node, 'JSXSpreadAttribute');
|
|
1726
3356
|
} else if (this.lookahead().type === tt.ellipsis) {
|
|
3357
|
+
this.#suppressTemplateRawTextToken = true;
|
|
1727
3358
|
this.expect(tt.ellipsis);
|
|
1728
|
-
|
|
3359
|
+
this.#templateScriptParsingDepth++;
|
|
3360
|
+
try {
|
|
3361
|
+
/** @type {ESTreeJSX.JSXSpreadAttribute} */ (node).argument = this.parseMaybeAssign();
|
|
3362
|
+
} finally {
|
|
3363
|
+
this.#templateScriptParsingDepth--;
|
|
3364
|
+
}
|
|
1729
3365
|
this.expect(tt.braceR);
|
|
1730
|
-
return this.finishNode(node, '
|
|
3366
|
+
return this.finishNode(node, 'JSXSpreadAttribute');
|
|
1731
3367
|
} else {
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
3368
|
+
if (!(this.type === tt.name || this.type.keyword || this.type === tstt.jsxName)) {
|
|
3369
|
+
this.unexpected();
|
|
3370
|
+
}
|
|
3371
|
+
const name_start = this.start;
|
|
3372
|
+
const name_start_loc = this.startLoc;
|
|
3373
|
+
const name_end = this.end;
|
|
3374
|
+
const name_end_loc = this.endLoc;
|
|
3375
|
+
const name_value = /** @type {string} */ (this.value);
|
|
3376
|
+
const id = /** @type {ESTreeJSX.JSXIdentifier} */ (
|
|
3377
|
+
this.startNodeAt(name_start, name_start_loc)
|
|
3378
|
+
);
|
|
3379
|
+
id.name = name_value;
|
|
3380
|
+
this.finishNodeAt(id, 'JSXIdentifier', name_end, name_end_loc);
|
|
3381
|
+
const name = /** @type {AST.Identifier} */ (
|
|
3382
|
+
this.startNodeAt(name_start, name_start_loc)
|
|
3383
|
+
);
|
|
3384
|
+
name.name = name_value;
|
|
3385
|
+
this.finishNodeAt(name, 'Identifier', name_end, name_end_loc);
|
|
3386
|
+
const expression = /** @type {ESTreeJSX.JSXExpressionContainer} */ (
|
|
3387
|
+
this.startNodeAt(
|
|
3388
|
+
/** @type {number} */ (node.start),
|
|
3389
|
+
/** @type {AST.NodeWithLocation} */ (node).loc.start,
|
|
3390
|
+
)
|
|
3391
|
+
);
|
|
3392
|
+
expression.expression = name;
|
|
3393
|
+
/** @type {ESTreeJSX.JSXAttribute} */ (node).name = id;
|
|
3394
|
+
/** @type {any} */ (node).value = this.finishNodeAt(
|
|
3395
|
+
expression,
|
|
3396
|
+
'JSXExpressionContainer',
|
|
3397
|
+
this.end + 1,
|
|
3398
|
+
this.endLoc,
|
|
3399
|
+
);
|
|
3400
|
+
/** @type {any} */ (node).shorthand = true;
|
|
1738
3401
|
this.next();
|
|
1739
3402
|
this.expect(tt.braceR);
|
|
1740
|
-
return this.finishNode(node, '
|
|
3403
|
+
return this.finishNode(node, 'JSXAttribute');
|
|
1741
3404
|
}
|
|
1742
3405
|
}
|
|
1743
3406
|
/** @type {ESTreeJSX.JSXAttribute} */ (node).name = this.jsx_parseNamespacedName();
|
|
1744
|
-
if (
|
|
1745
|
-
/** @type {ESTreeJSX.JSXAttribute} */ (node).name.type === 'JSXIdentifier' &&
|
|
1746
|
-
/** @type {ESTreeJSX.JSXIdentifier} */ (/** @type {ESTreeJSX.JSXAttribute} */ (node).name)
|
|
1747
|
-
.tracked
|
|
1748
|
-
) {
|
|
1749
|
-
this.#report_recoverable_error_range(
|
|
1750
|
-
/** @type {AST.NodeWithLocation} */ (node).start,
|
|
1751
|
-
/** @type {AST.NodeWithLocation} */ (/** @type {ESTreeJSX.JSXAttribute} */ (node).name)
|
|
1752
|
-
.end ??
|
|
1753
|
-
node.end ??
|
|
1754
|
-
node.start,
|
|
1755
|
-
DYNAMIC_ATTRIBUTE_NAME_ERROR,
|
|
1756
|
-
);
|
|
1757
|
-
}
|
|
1758
3407
|
const value = /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
|
|
1759
3408
|
this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
|
|
1760
3409
|
);
|
|
@@ -1785,20 +3434,8 @@ export function TSRXPlugin(config) {
|
|
|
1785
3434
|
jsx_parseIdentifier() {
|
|
1786
3435
|
const node = /** @type {ESTreeJSX.JSXIdentifier} */ (this.startNode());
|
|
1787
3436
|
|
|
1788
|
-
if (this.type.
|
|
1789
|
-
this.next(); // consume @
|
|
1790
|
-
|
|
1791
|
-
if (this.type === tt.name || this.type === tstt.jsxName) {
|
|
1792
|
-
node.name = /** @type {string} */ (this.value);
|
|
1793
|
-
node.tracked = true;
|
|
1794
|
-
this.next();
|
|
1795
|
-
} else {
|
|
1796
|
-
// Unexpected token after @
|
|
1797
|
-
this.unexpected();
|
|
1798
|
-
}
|
|
1799
|
-
} else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
|
|
3437
|
+
if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
|
|
1800
3438
|
node.name = /** @type {string} */ (this.value);
|
|
1801
|
-
node.tracked = false; // Explicitly mark as not tracked
|
|
1802
3439
|
this.next();
|
|
1803
3440
|
} else {
|
|
1804
3441
|
return super.jsx_parseIdentifier();
|
|
@@ -1865,12 +3502,127 @@ export function TSRXPlugin(config) {
|
|
|
1865
3502
|
default:
|
|
1866
3503
|
this.raise(this.start, 'value should be either an expression or a quoted text');
|
|
1867
3504
|
}
|
|
1868
|
-
}
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
/**
|
|
3508
|
+
* `@try`/`@pending`/`@catch` blocks lower their direct `return`
|
|
3509
|
+
* values into reactive boundary fallbacks, so unlike `@if`/`@for`/`@switch`
|
|
3510
|
+
* blocks they legitimately allow `return <markup>` statements. Set the flag
|
|
3511
|
+
* immediately before parsing each such block so its body sees it.
|
|
3512
|
+
* @returns {AST.BlockStatement}
|
|
3513
|
+
*/
|
|
3514
|
+
#parseTemplateControlFlowReturnBlock(createNewLexicalScope = true) {
|
|
3515
|
+
this.#controlFlowBlockAllowsNativeReturn = true;
|
|
3516
|
+
return this.#parseTemplateControlFlowBlock(createNewLexicalScope);
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
/**
|
|
3520
|
+
* @type {Parse.Parser['parseTryStatement']}
|
|
3521
|
+
*/
|
|
3522
|
+
parseTryStatement(node) {
|
|
3523
|
+
if (this.#templateControlFlowTryDepth > 0) {
|
|
3524
|
+
this.#templateControlFlowTryDepth--;
|
|
3525
|
+
try {
|
|
3526
|
+
this.next();
|
|
3527
|
+
node.block = this.#parseTemplateControlFlowReturnBlock();
|
|
3528
|
+
node.handler = null;
|
|
3529
|
+
|
|
3530
|
+
if (this.#eatJSXDirectiveClauseKeyword('pending')) {
|
|
3531
|
+
node.pending = this.#parseTemplateControlFlowReturnBlock();
|
|
3532
|
+
} else if (this.#isUnprefixedDirectiveClauseContinuation('pending', ['{'])) {
|
|
3533
|
+
this.raise(this.start, 'Expected `@pending` after `@try` block.');
|
|
3534
|
+
} else {
|
|
3535
|
+
node.pending = null;
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
const clauseStart = this.start;
|
|
3539
|
+
const clauseStartLoc = this.startLoc;
|
|
3540
|
+
if (this.#eatJSXDirectiveClauseKeyword('catch')) {
|
|
3541
|
+
if (this.type === tt._catch || this.value === 'catch') {
|
|
3542
|
+
this.next();
|
|
3543
|
+
}
|
|
3544
|
+
const paramStart = skip_whitespace_from(this.input, this.start);
|
|
3545
|
+
if (this.input.charCodeAt(paramStart) === CharCode.openParen) {
|
|
3546
|
+
this.pos = paramStart;
|
|
3547
|
+
this.start = paramStart;
|
|
3548
|
+
this.startLoc = acorn.getLineInfo(this.input, paramStart);
|
|
3549
|
+
this.curLine = this.startLoc.line;
|
|
3550
|
+
this.lineStart = paramStart - this.startLoc.column;
|
|
3551
|
+
this.#filterTemplateScriptContexts();
|
|
3552
|
+
if (this.curContext() !== b_stat) {
|
|
3553
|
+
this.context.push(b_stat);
|
|
3554
|
+
}
|
|
3555
|
+
this.exprAllowed = true;
|
|
3556
|
+
this.#suppressTemplateRawTextToken = true;
|
|
3557
|
+
try {
|
|
3558
|
+
this.nextToken();
|
|
3559
|
+
} finally {
|
|
3560
|
+
this.#suppressTemplateRawTextToken = false;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
const clause = /** @type {AST.CatchClause} */ (
|
|
3564
|
+
this.startNodeAt(clauseStart, clauseStartLoc)
|
|
3565
|
+
);
|
|
3566
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
3567
|
+
this.#readingJSXControlFlowHeader = true;
|
|
3568
|
+
try {
|
|
3569
|
+
if (this.eat(tt.parenL)) {
|
|
3570
|
+
const param = this.parseBindingAtom();
|
|
3571
|
+
const simple = param.type === 'Identifier';
|
|
3572
|
+
this.enterScope(simple ? BINDING_TYPES.BIND_SIMPLE_CATCH : 0);
|
|
3573
|
+
this.checkLValPattern(
|
|
3574
|
+
param,
|
|
3575
|
+
simple ? BINDING_TYPES.BIND_SIMPLE_CATCH : BINDING_TYPES.BIND_LEXICAL,
|
|
3576
|
+
);
|
|
3577
|
+
const type = this.tsTryParseTypeAnnotation();
|
|
3578
|
+
if (type) {
|
|
3579
|
+
param.typeAnnotation = type;
|
|
3580
|
+
this.resetEndLocation(param);
|
|
3581
|
+
}
|
|
3582
|
+
clause.param = param;
|
|
3583
|
+
|
|
3584
|
+
if (this.eat(tt.comma)) {
|
|
3585
|
+
const reset_param = this.parseBindingAtom();
|
|
3586
|
+
this.checkLValSimple(reset_param, BINDING_TYPES.BIND_LEXICAL);
|
|
3587
|
+
const reset_type = this.tsTryParseTypeAnnotation();
|
|
3588
|
+
if (reset_type) {
|
|
3589
|
+
reset_param.typeAnnotation = reset_type;
|
|
3590
|
+
this.resetEndLocation(reset_param);
|
|
3591
|
+
}
|
|
3592
|
+
clause.resetParam = reset_param;
|
|
3593
|
+
} else {
|
|
3594
|
+
clause.resetParam = null;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
this.expect(tt.parenR);
|
|
3598
|
+
} else {
|
|
3599
|
+
clause.param = null;
|
|
3600
|
+
clause.resetParam = null;
|
|
3601
|
+
this.enterScope(0);
|
|
3602
|
+
}
|
|
3603
|
+
} finally {
|
|
3604
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
3605
|
+
}
|
|
3606
|
+
clause.body = this.#parseTemplateControlFlowReturnBlock(false);
|
|
3607
|
+
this.exitScope();
|
|
3608
|
+
node.handler = this.finishNode(clause, 'CatchClause');
|
|
3609
|
+
} else if (this.#isUnprefixedDirectiveClauseContinuation('catch', ['{', '('])) {
|
|
3610
|
+
this.raise(this.start, 'Expected `@catch` after `@try` block.');
|
|
3611
|
+
}
|
|
3612
|
+
node.finalizer = null;
|
|
3613
|
+
|
|
3614
|
+
if (!node.handler && !node.pending) {
|
|
3615
|
+
this.raise(
|
|
3616
|
+
/** @type {AST.NodeWithLocation} */ (node).start,
|
|
3617
|
+
'Missing `@catch` or `@pending` after `@try` block.',
|
|
3618
|
+
);
|
|
3619
|
+
}
|
|
3620
|
+
return this.finishNode(node, 'TryStatement');
|
|
3621
|
+
} finally {
|
|
3622
|
+
this.#templateControlFlowTryDepth++;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
1869
3625
|
|
|
1870
|
-
/**
|
|
1871
|
-
* @type {Parse.Parser['parseTryStatement']}
|
|
1872
|
-
*/
|
|
1873
|
-
parseTryStatement(node) {
|
|
1874
3626
|
this.next();
|
|
1875
3627
|
node.block = this.parseBlock();
|
|
1876
3628
|
node.handler = null;
|
|
@@ -1939,20 +3691,53 @@ export function TSRXPlugin(config) {
|
|
|
1939
3691
|
|
|
1940
3692
|
/** @type {Parse.Parser['jsx_readToken']} */
|
|
1941
3693
|
jsx_readToken() {
|
|
1942
|
-
|
|
1943
|
-
(
|
|
1944
|
-
|
|
1945
|
-
|
|
3694
|
+
if (this.#scriptJSXElementDepth > 0 || this.#path.length === 0) {
|
|
3695
|
+
if (
|
|
3696
|
+
this.input.charCodeAt(this.pos) === CharCode.closeBrace &&
|
|
3697
|
+
this.context.includes(tstc.tc_expr)
|
|
3698
|
+
) {
|
|
3699
|
+
this.#resetTokenStartToCurrentPosition();
|
|
3700
|
+
return original.readToken.call(this, CharCode.closeBrace);
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
let index = this.pos;
|
|
3704
|
+
while (
|
|
3705
|
+
this.input.charCodeAt(index) === CharCode.space ||
|
|
3706
|
+
this.input.charCodeAt(index) === CharCode.tab ||
|
|
3707
|
+
this.input.charCodeAt(index) === CharCode.lineFeed ||
|
|
3708
|
+
this.input.charCodeAt(index) === CharCode.carriageReturn
|
|
3709
|
+
) {
|
|
3710
|
+
index++;
|
|
3711
|
+
}
|
|
3712
|
+
if (
|
|
3713
|
+
index !== this.pos &&
|
|
3714
|
+
this.input.charCodeAt(index) === CharCode.slash &&
|
|
3715
|
+
this.input.charCodeAt(index + 1) === CharCode.greaterThan &&
|
|
3716
|
+
this.context.includes(tstc.tc_expr)
|
|
3717
|
+
) {
|
|
3718
|
+
const loc = acorn.getLineInfo(this.input, index);
|
|
3719
|
+
this.pos = index;
|
|
3720
|
+
this.start = index;
|
|
3721
|
+
this.startLoc = loc;
|
|
3722
|
+
this.curLine = loc.line;
|
|
3723
|
+
this.lineStart = index - loc.column;
|
|
3724
|
+
this.exprAllowed = false;
|
|
3725
|
+
if (this.curContext() !== tstc.tc_oTag) {
|
|
3726
|
+
this.context.push(tstc.tc_oTag);
|
|
3727
|
+
}
|
|
3728
|
+
return original.readToken.call(this, CharCode.slash);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
if (this.#scriptJSXElementDepth > 0 || this.#path.length === 0) {
|
|
1946
3732
|
return super.jsx_readToken();
|
|
1947
3733
|
}
|
|
3734
|
+
|
|
1948
3735
|
let out = '',
|
|
1949
3736
|
chunkStart = this.pos;
|
|
1950
3737
|
|
|
1951
3738
|
while (true) {
|
|
1952
3739
|
if (this.pos >= this.input.length) {
|
|
1953
|
-
const inside_open_template = this.#path.findLast(
|
|
1954
|
-
(n) => n.type === 'Element' || n.type === 'TsrxFragment' || n.type === 'TsxCompat',
|
|
1955
|
-
);
|
|
3740
|
+
const inside_open_template = this.#path.findLast((n) => this.#isNativeTemplateNode(n));
|
|
1956
3741
|
if (!inside_open_template) {
|
|
1957
3742
|
while (this.curContext() === tstc.tc_expr) {
|
|
1958
3743
|
this.context.pop();
|
|
@@ -1964,8 +3749,29 @@ export function TSRXPlugin(config) {
|
|
|
1964
3749
|
let ch = this.input.charCodeAt(this.pos);
|
|
1965
3750
|
|
|
1966
3751
|
switch (ch) {
|
|
3752
|
+
case CharCode.equals:
|
|
3753
|
+
if (
|
|
3754
|
+
!this.#shouldReadTemplateRawTextToken() &&
|
|
3755
|
+
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
|
|
3756
|
+
) {
|
|
3757
|
+
this.#resetTokenStartToCurrentPosition();
|
|
3758
|
+
this.pos += 2;
|
|
3759
|
+
return this.finishToken(tt.arrow);
|
|
3760
|
+
}
|
|
3761
|
+
if (this.#shouldReadTemplateRawTextToken()) {
|
|
3762
|
+
++this.pos;
|
|
3763
|
+
break;
|
|
3764
|
+
}
|
|
3765
|
+
this.#resetTokenStartToCurrentPosition();
|
|
3766
|
+
this.context.push(b_stat);
|
|
3767
|
+
this.exprAllowed = true;
|
|
3768
|
+
return original.readToken.call(this, ch);
|
|
3769
|
+
|
|
1967
3770
|
case CharCode.lessThan:
|
|
1968
3771
|
case CharCode.openBrace:
|
|
3772
|
+
if (out || this.pos > chunkStart) {
|
|
3773
|
+
return this.finishToken(tstt.jsxText, out + this.input.slice(chunkStart, this.pos));
|
|
3774
|
+
}
|
|
1969
3775
|
// In JSX text mode, '<' and '{' always start a tag/expression container.
|
|
1970
3776
|
// `exprAllowed` can be false here due to surrounding parser state, but
|
|
1971
3777
|
// throwing breaks valid templates (e.g. sibling tags after a close).
|
|
@@ -2012,6 +3818,7 @@ export function TSRXPlugin(config) {
|
|
|
2012
3818
|
}
|
|
2013
3819
|
|
|
2014
3820
|
// Continue processing from current position
|
|
3821
|
+
chunkStart = this.pos;
|
|
2015
3822
|
break;
|
|
2016
3823
|
} else if (this.input.charCodeAt(this.pos + 1) === CharCode.asterisk) {
|
|
2017
3824
|
// '/*'
|
|
@@ -2051,9 +3858,13 @@ export function TSRXPlugin(config) {
|
|
|
2051
3858
|
}
|
|
2052
3859
|
|
|
2053
3860
|
// Continue processing from current position
|
|
3861
|
+
chunkStart = this.pos;
|
|
3862
|
+
break;
|
|
3863
|
+
}
|
|
3864
|
+
if (this.#shouldReadTemplateRawTextToken()) {
|
|
3865
|
+
++this.pos;
|
|
2054
3866
|
break;
|
|
2055
3867
|
}
|
|
2056
|
-
// If not a comment, fall through to default case
|
|
2057
3868
|
this.#resetTokenStartToCurrentPosition();
|
|
2058
3869
|
this.context.push(b_stat);
|
|
2059
3870
|
this.exprAllowed = true;
|
|
@@ -2068,10 +3879,21 @@ export function TSRXPlugin(config) {
|
|
|
2068
3879
|
case CharCode.greaterThan:
|
|
2069
3880
|
case CharCode.closeBrace: {
|
|
2070
3881
|
if (
|
|
2071
|
-
ch === CharCode.
|
|
2072
|
-
(this
|
|
2073
|
-
|
|
2074
|
-
|
|
3882
|
+
ch === CharCode.greaterThan &&
|
|
3883
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.equals &&
|
|
3884
|
+
!this.#shouldReadTemplateRawTextToken()
|
|
3885
|
+
) {
|
|
3886
|
+
const start = this.pos - 1;
|
|
3887
|
+
const loc = acorn.getLineInfo(this.input, start);
|
|
3888
|
+
this.start = start;
|
|
3889
|
+
this.startLoc = loc;
|
|
3890
|
+
this.pos++;
|
|
3891
|
+
return this.finishToken(tt.arrow);
|
|
3892
|
+
}
|
|
3893
|
+
if (
|
|
3894
|
+
this.#isInsideNativeTemplateScriptSection() ||
|
|
3895
|
+
(ch === CharCode.closeBrace &&
|
|
3896
|
+
(this.#path.length === 0 || this.#isNativeTemplateNode(this.#path.at(-1))))
|
|
2075
3897
|
) {
|
|
2076
3898
|
this.#resetTokenStartToCurrentPosition();
|
|
2077
3899
|
return original.readToken.call(this, ch);
|
|
@@ -2098,6 +3920,21 @@ export function TSRXPlugin(config) {
|
|
|
2098
3920
|
} else if (ch === CharCode.space || ch === CharCode.tab) {
|
|
2099
3921
|
++this.pos;
|
|
2100
3922
|
} else {
|
|
3923
|
+
// A JSX element nested inside a `{ … }` expression container is
|
|
3924
|
+
// still a template-mode element whose text children are raw JSX
|
|
3925
|
+
// text (e.g. `{<div> a</div>}`). The default raw-text check bails
|
|
3926
|
+
// for everything inside an expression container, so without the
|
|
3927
|
+
// `allow_inside_expression_container` form the first non-space char
|
|
3928
|
+
// would re-anchor the token start and drop the leading whitespace
|
|
3929
|
+
// this loop already skipped. Keep scanning so the full run —
|
|
3930
|
+
// leading indentation included — is captured, matching the
|
|
3931
|
+
// bare-template path. Directive bodies (`@if`/`@for`/…) inside the
|
|
3932
|
+
// element still fall through to JS tokenization via the other
|
|
3933
|
+
// checks in `#shouldReadTemplateRawTextToken`.
|
|
3934
|
+
if (this.#shouldReadTemplateRawTextToken(true)) {
|
|
3935
|
+
++this.pos;
|
|
3936
|
+
break;
|
|
3937
|
+
}
|
|
2101
3938
|
this.#resetTokenStartToCurrentPosition();
|
|
2102
3939
|
this.context.push(b_stat);
|
|
2103
3940
|
this.exprAllowed = true;
|
|
@@ -2108,465 +3945,294 @@ export function TSRXPlugin(config) {
|
|
|
2108
3945
|
}
|
|
2109
3946
|
|
|
2110
3947
|
/**
|
|
2111
|
-
* Override jsx_parseElement to
|
|
2112
|
-
*
|
|
2113
|
-
* their children.
|
|
3948
|
+
* Override jsx_parseElement to use TSRX template parsing only where the
|
|
3949
|
+
* fragment/element body can contain TSRX-only syntax.
|
|
2114
3950
|
* @type {Parse.Parser['jsx_parseElement']}
|
|
2115
3951
|
*/
|
|
2116
3952
|
jsx_parseElement() {
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
const inside_tsx_island = current_template_node?.type === 'TsxCompat';
|
|
2123
|
-
if (inside_tsx_island) {
|
|
2124
|
-
if (this.input.charCodeAt(tag_name_start) === CharCode.at) {
|
|
2125
|
-
this.#report_recoverable_error_range(
|
|
2126
|
-
this.start,
|
|
2127
|
-
tag_name_start + 1,
|
|
2128
|
-
DYNAMIC_ELEMENT_IN_TSX_ERROR,
|
|
3953
|
+
if (this.#forceScriptJSXElementDepth > 0 || this.#isInsideNativeTemplateScriptSection()) {
|
|
3954
|
+
if (this.#isStyleOpeningTagStart()) {
|
|
3955
|
+
this.next();
|
|
3956
|
+
return /** @type {ESTreeJSX.JSXElement | AST.JSXStyleElement} */ (
|
|
3957
|
+
/** @type {unknown} */ (this.parseElement())
|
|
2129
3958
|
);
|
|
2130
3959
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
3960
|
+
|
|
3961
|
+
this.#scriptJSXElementDepth++;
|
|
3962
|
+
try {
|
|
3963
|
+
return super.jsx_parseElement();
|
|
3964
|
+
} finally {
|
|
3965
|
+
this.#scriptJSXElementDepth--;
|
|
3966
|
+
}
|
|
2133
3967
|
}
|
|
2134
3968
|
|
|
2135
3969
|
this.next();
|
|
2136
3970
|
const parsed = /** @type {import('estree-jsx').JSXElement} */ (
|
|
2137
3971
|
/** @type {unknown} */ (this.parseElement())
|
|
2138
3972
|
);
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
3973
|
+
this.#popTokenContextsAfterTemplateExpressionElement(parsed);
|
|
3974
|
+
return parsed;
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
/**
|
|
3978
|
+
* @type {Parse.Parser['jsx_parseOpeningElementAt']}
|
|
3979
|
+
*/
|
|
3980
|
+
jsx_parseOpeningElementAt(startPos, startLoc) {
|
|
3981
|
+
const node = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
|
|
3982
|
+
this.startNodeAt(/** @type {number} */ (startPos), /** @type {AST.Position} */ (startLoc))
|
|
3983
|
+
);
|
|
3984
|
+
node.attributes = [];
|
|
3985
|
+
const nodeName = this.jsx_parseElementName();
|
|
3986
|
+
if (nodeName) node.name = nodeName;
|
|
3987
|
+
if (this.match(tt.relational) || this.match(tt.bitShift)) {
|
|
3988
|
+
const typeArguments = /** @type {any} */ (this).tsTryParseAndCatch(() =>
|
|
3989
|
+
/** @type {any} */ (this).tsParseTypeArgumentsInExpression(),
|
|
2142
3990
|
);
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
3991
|
+
if (typeArguments) node.typeArguments = typeArguments;
|
|
3992
|
+
}
|
|
3993
|
+
while (this.type !== tt.slash && this.type !== tstt.jsxTagEnd) {
|
|
3994
|
+
node.attributes.push(this.jsx_parseAttribute());
|
|
3995
|
+
}
|
|
3996
|
+
node.selfClosing = this.eat(tt.slash);
|
|
3997
|
+
|
|
3998
|
+
const opening_template_node = this.#openingNativeTemplateNode;
|
|
3999
|
+
let pushed_opening_template_node = false;
|
|
4000
|
+
if (opening_template_node) {
|
|
4001
|
+
if (nodeName) {
|
|
4002
|
+
/** @type {any} */ (opening_template_node).type =
|
|
4003
|
+
this.getElementName(nodeName) === 'style' ? 'JSXStyleElement' : 'JSXElement';
|
|
4004
|
+
/** @type {any} */ (opening_template_node).openingElement = node;
|
|
4005
|
+
/** @type {any} */ (opening_template_node).closingElement = null;
|
|
4006
|
+
} else {
|
|
4007
|
+
/** @type {any} */ (opening_template_node).type = 'JSXFragment';
|
|
4008
|
+
/** @type {any} */ (opening_template_node).openingFragment =
|
|
4009
|
+
this.#toOpeningFragment(node);
|
|
4010
|
+
/** @type {any} */ (opening_template_node).closingFragment = null;
|
|
2149
4011
|
}
|
|
4012
|
+
this.#path.push(opening_template_node);
|
|
4013
|
+
pushed_opening_template_node = true;
|
|
2150
4014
|
}
|
|
2151
|
-
|
|
4015
|
+
|
|
4016
|
+
try {
|
|
4017
|
+
this.expect(tstt.jsxTagEnd);
|
|
4018
|
+
} finally {
|
|
4019
|
+
if (pushed_opening_template_node) {
|
|
4020
|
+
this.#path.pop();
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
if (nodeName) {
|
|
4024
|
+
return this.finishNode(node, 'JSXOpeningElement');
|
|
4025
|
+
}
|
|
4026
|
+
return /** @type {any} */ (
|
|
4027
|
+
/** @type {any} */ (this).finishNode(node, 'JSXOpeningFragment')
|
|
4028
|
+
);
|
|
2152
4029
|
}
|
|
2153
4030
|
|
|
2154
4031
|
/**
|
|
2155
4032
|
* @type {Parse.Parser['parseElement']}
|
|
2156
4033
|
*/
|
|
2157
4034
|
parseElement() {
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
)
|
|
4035
|
+
// Depth the tokenizer context must return to once this element closes:
|
|
4036
|
+
// the stack with the element's own opening `<` contexts (a trailing
|
|
4037
|
+
// tc_oTag/tc_expr) stripped off. A balanced element should leave the
|
|
4038
|
+
// stack here; the body (especially a control-flow block) can otherwise
|
|
4039
|
+
// leave residue that breaks tokenizing the following JS token when the
|
|
4040
|
+
// element is in expression position.
|
|
4041
|
+
let pre_element_context_depth = this.context.length;
|
|
4042
|
+
while (pre_element_context_depth > 0) {
|
|
4043
|
+
const ctx = this.context[pre_element_context_depth - 1];
|
|
4044
|
+
if (ctx === tstc.tc_expr || ctx === tstc.tc_oTag || ctx === tstc.tc_cTag) {
|
|
4045
|
+
pre_element_context_depth--;
|
|
4046
|
+
} else {
|
|
4047
|
+
break;
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
|
|
2161
4051
|
// Adjust the start so we capture the `<` as part of the element
|
|
2162
4052
|
const start = this.start - 1;
|
|
2163
4053
|
const position = new acorn.Position(this.curLine, start - this.lineStart);
|
|
2164
4054
|
|
|
2165
|
-
const
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
4055
|
+
const node =
|
|
4056
|
+
/** @type {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment | AST.JSXStyleElement} */ (
|
|
4057
|
+
/** @type {unknown} */ (this.startNode())
|
|
4058
|
+
);
|
|
4059
|
+
node.start = start;
|
|
4060
|
+
/** @type {AST.NodeWithLocation} */ (node).loc.start = position;
|
|
4061
|
+
node.metadata = {
|
|
4062
|
+
path: [],
|
|
4063
|
+
native_tsrx: true,
|
|
4064
|
+
templateMode: 'script',
|
|
4065
|
+
};
|
|
4066
|
+
node.children = [];
|
|
4067
|
+
|
|
4068
|
+
const previous_opening_native_template_node = this.#openingNativeTemplateNode;
|
|
4069
|
+
this.#openingNativeTemplateNode = node;
|
|
4070
|
+
let open;
|
|
4071
|
+
try {
|
|
4072
|
+
open = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
|
|
4073
|
+
this.jsx_parseOpeningElementAt(start, position)
|
|
4074
|
+
);
|
|
4075
|
+
} finally {
|
|
4076
|
+
this.#openingNativeTemplateNode = previous_opening_native_template_node;
|
|
4077
|
+
}
|
|
4078
|
+
const tag_name = open.name ? this.getElementName(open.name) : null;
|
|
4079
|
+
const is_style = tag_name === 'style';
|
|
4080
|
+
const inside_head = this.#path.findLast((n) => this.#isNativeElementNamed(n, 'head'));
|
|
2181
4081
|
|
|
2182
4082
|
// Fragments (<>) produce JSXOpeningFragment with no `name` property
|
|
2183
4083
|
const is_fragment = !open.name;
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
4084
|
+
const parent_template_node = this.#currentNativeTemplateNode();
|
|
4085
|
+
const parent_is_template_output =
|
|
4086
|
+
parent_template_node?.metadata?.templateMode === 'template';
|
|
4087
|
+
node.metadata.templateMode =
|
|
4088
|
+
is_fragment && parent_is_template_output ? 'template' : 'script';
|
|
4089
|
+
if (!is_fragment && open.name.type === 'JSXNamespacedName') {
|
|
2189
4090
|
const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
this.raise(
|
|
2196
|
-
open.start,
|
|
2197
|
-
`TSX compatibility elements cannot be self-closing. '<${tagName} />' must have a closing tag '</${tagName}>'.`,
|
|
2198
|
-
);
|
|
2199
|
-
}
|
|
2200
|
-
} else if (is_fragment) {
|
|
2201
|
-
/** @type {AST.TsrxFragment} */ (element).type = 'TsrxFragment';
|
|
2202
|
-
} else {
|
|
2203
|
-
element.type = 'Element';
|
|
4091
|
+
const tagName = namespace_node.namespace.name + ':' + namespace_node.name.name;
|
|
4092
|
+
this.raise(
|
|
4093
|
+
open.start,
|
|
4094
|
+
`Namespaced elements are not supported in TSRX templates: <${tagName}>.`,
|
|
4095
|
+
);
|
|
2204
4096
|
}
|
|
2205
4097
|
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
/** @type {ESTreeJSX.JSXAttribute} */ (attr).value =
|
|
2221
|
-
/** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (expression);
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
4098
|
+
if (is_fragment) {
|
|
4099
|
+
/** @type {ESTreeJSX.JSXFragment} */ (node).type = 'JSXFragment';
|
|
4100
|
+
/** @type {ESTreeJSX.JSXFragment} */ (node).openingFragment =
|
|
4101
|
+
this.#toOpeningFragment(open);
|
|
4102
|
+
/** @type {any} */ (node).closingFragment = null;
|
|
4103
|
+
} else {
|
|
4104
|
+
if (is_style) {
|
|
4105
|
+
/** @type {AST.JSXStyleElement} */ (node).type = 'JSXStyleElement';
|
|
4106
|
+
/** @type {AST.JSXStyleElement} */ (node).openingElement = open;
|
|
4107
|
+
/** @type {AST.JSXStyleElement} */ (node).closingElement = null;
|
|
4108
|
+
} else {
|
|
4109
|
+
/** @type {ESTreeJSX.JSXElement} */ (node).type = 'JSXElement';
|
|
4110
|
+
/** @type {ESTreeJSX.JSXElement} */ (node).openingElement = open;
|
|
4111
|
+
/** @type {ESTreeJSX.JSXElement} */ (node).closingElement = null;
|
|
2224
4112
|
}
|
|
2225
4113
|
}
|
|
2226
4114
|
|
|
2227
|
-
if (!is_tsx_compat && !is_fragment) {
|
|
2228
|
-
/** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
|
|
2229
|
-
convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
|
|
2230
|
-
);
|
|
2231
|
-
element.selfClosing = open.selfClosing;
|
|
2232
|
-
} else if (is_fragment) {
|
|
2233
|
-
element.selfClosing = false;
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
element.attributes = open.attributes;
|
|
2237
|
-
element.metadata ??= { path: [] };
|
|
2238
4115
|
// Opening-tag parsing can tokenize comments that appear before the first
|
|
2239
4116
|
// child. Preserve that early container id so the comment stays associated
|
|
2240
4117
|
// with this element during comment attachment/printing.
|
|
2241
|
-
if (
|
|
2242
|
-
|
|
4118
|
+
if (node.metadata.commentContainerId === undefined) {
|
|
4119
|
+
node.metadata.commentContainerId = ++this.#commentContextId;
|
|
2243
4120
|
}
|
|
2244
4121
|
|
|
2245
|
-
|
|
2246
|
-
this.#path.pop();
|
|
4122
|
+
this.#path.push(node);
|
|
2247
4123
|
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
|
|
4124
|
+
if (!is_fragment && open.selfClosing) {
|
|
4125
|
+
this.#path.pop();
|
|
4126
|
+
} else if (is_style) {
|
|
4127
|
+
this.#parseStyleElement(open, /** @type {AST.JSXStyleElement} */ (node), !!inside_head);
|
|
4128
|
+
this.#path.pop();
|
|
4129
|
+
} else {
|
|
4130
|
+
this.#parseNativeTemplateBody(node, /** @type {AST.Node[]} */ (node.children), {
|
|
2254
4131
|
enterScope: true,
|
|
2255
4132
|
resetFunctionBodyDepth: true,
|
|
2256
4133
|
});
|
|
2257
4134
|
|
|
2258
|
-
this.#path.
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
4135
|
+
if (this.#path[this.#path.length - 1] === node) {
|
|
4136
|
+
const displayTag = is_fragment
|
|
4137
|
+
? ''
|
|
4138
|
+
: this.getElementName(/** @type {ESTreeJSX.JSXElement} */ (node).openingElement.name);
|
|
4139
|
+
this.#report_broken_markup_error(
|
|
4140
|
+
this.start,
|
|
4141
|
+
`Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
|
|
4142
|
+
);
|
|
4143
|
+
/** @type {any} */ (node).unclosed = true;
|
|
4144
|
+
/** @type {AST.SourceLocation} */ (node.loc).end = {
|
|
4145
|
+
.../** @type {AST.SourceLocation} */ (
|
|
4146
|
+
is_fragment
|
|
4147
|
+
? /** @type {ESTreeJSX.JSXFragment} */ (node).openingFragment.loc
|
|
4148
|
+
: /** @type {ESTreeJSX.JSXElement} */ (node).openingElement.loc
|
|
4149
|
+
).end,
|
|
2263
4150
|
};
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
}
|
|
2269
|
-
this.next();
|
|
2270
|
-
if (this.type !== tstt.jsxTagEnd) {
|
|
2271
|
-
raise_error();
|
|
2272
|
-
}
|
|
2273
|
-
this.#popTsxTokenContextBeforeTemplateExpressionChild();
|
|
2274
|
-
this.next();
|
|
2275
|
-
}
|
|
2276
|
-
} else {
|
|
2277
|
-
if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'script') {
|
|
2278
|
-
let content = '';
|
|
2279
|
-
|
|
2280
|
-
// TODO implement this where we get a string for content of the content of the script tag
|
|
2281
|
-
// This is a temporary workaround to get the content of the script tag
|
|
2282
|
-
const start = open.end;
|
|
2283
|
-
const input = this.input.slice(start);
|
|
2284
|
-
const end = input.indexOf('</script>');
|
|
2285
|
-
content = end === -1 ? input : input.slice(0, end);
|
|
2286
|
-
|
|
2287
|
-
const newLines = content.match(regex_newline_characters)?.length;
|
|
2288
|
-
if (newLines) {
|
|
2289
|
-
this.curLine = open.loc.end.line + newLines;
|
|
2290
|
-
this.lineStart = start + content.lastIndexOf('\n') + 1;
|
|
2291
|
-
}
|
|
2292
|
-
if (end !== -1) {
|
|
2293
|
-
const closingStart = start + content.length;
|
|
2294
|
-
const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
|
|
2295
|
-
const closingStartLoc = new acorn.Position(
|
|
2296
|
-
closingLineInfo.line,
|
|
2297
|
-
closingLineInfo.column,
|
|
2298
|
-
);
|
|
2299
|
-
|
|
2300
|
-
// Ensure `</script>` can't be tokenized as `<` followed by a regexp
|
|
2301
|
-
// start when we manually advance to the `/`.
|
|
2302
|
-
this.exprAllowed = false;
|
|
2303
|
-
|
|
2304
|
-
// Position after '<' (so next() reads '/')
|
|
2305
|
-
this.pos = closingStart + 1;
|
|
2306
|
-
this.type = tstt.jsxTagStart;
|
|
2307
|
-
this.start = closingStart;
|
|
2308
|
-
this.startLoc = closingStartLoc;
|
|
2309
|
-
this.next();
|
|
2310
|
-
|
|
2311
|
-
// Consume '/'
|
|
2312
|
-
this.next();
|
|
2313
|
-
|
|
2314
|
-
const closingElement = this.jsx_parseClosingElementAt(closingStart, closingStartLoc);
|
|
2315
|
-
element.closingElement = closingElement;
|
|
2316
|
-
this.exprAllowed = false;
|
|
2317
|
-
|
|
2318
|
-
const contentStartLineInfo = acorn.getLineInfo(this.input, start);
|
|
2319
|
-
const contentStartLoc = new acorn.Position(
|
|
2320
|
-
contentStartLineInfo.line,
|
|
2321
|
-
contentStartLineInfo.column,
|
|
2322
|
-
);
|
|
2323
|
-
|
|
2324
|
-
const contentEndLineInfo = acorn.getLineInfo(this.input, closingStart);
|
|
2325
|
-
const contentEndLoc = new acorn.Position(
|
|
2326
|
-
contentEndLineInfo.line,
|
|
2327
|
-
contentEndLineInfo.column,
|
|
2328
|
-
);
|
|
2329
|
-
|
|
2330
|
-
element.children = [
|
|
2331
|
-
/** @type {AST.ScriptContent} */ (
|
|
2332
|
-
/** @type {unknown} */ ({
|
|
2333
|
-
type: 'ScriptContent',
|
|
2334
|
-
content,
|
|
2335
|
-
start,
|
|
2336
|
-
end: closingStart,
|
|
2337
|
-
loc: { start: contentStartLoc, end: contentEndLoc },
|
|
2338
|
-
})
|
|
2339
|
-
),
|
|
2340
|
-
];
|
|
2341
|
-
|
|
2342
|
-
this.#path.pop();
|
|
2343
|
-
} else {
|
|
2344
|
-
// No closing tag
|
|
2345
|
-
this.#report_broken_markup_error(
|
|
2346
|
-
open.end,
|
|
2347
|
-
"Unclosed tag '<script>'. Expected '</script>' before end of template.",
|
|
2348
|
-
);
|
|
2349
|
-
/** @type {AST.Element} */ (element).unclosed = true;
|
|
2350
|
-
this.#path.pop();
|
|
2351
|
-
}
|
|
2352
|
-
} else if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'style') {
|
|
2353
|
-
// jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
|
|
2354
|
-
// So backtrack to the end of the <style> tag to make sure everything is included
|
|
2355
|
-
const start = open.end;
|
|
2356
|
-
const input = this.input.slice(start);
|
|
2357
|
-
const end = input.indexOf('</style>');
|
|
2358
|
-
const content = end === -1 ? input : input.slice(0, end);
|
|
2359
|
-
|
|
2360
|
-
const parsed_css = parse_style(content, { loose: this.#loose });
|
|
2361
|
-
if (!inside_head) {
|
|
2362
|
-
/** @type {AST.Element} */ (element).metadata.styleScopeHash = parsed_css.hash;
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
const newLines = content.match(regex_newline_characters)?.length;
|
|
2366
|
-
if (newLines) {
|
|
2367
|
-
this.curLine = open.loc.end.line + newLines;
|
|
2368
|
-
this.lineStart = start + content.lastIndexOf('\n') + 1;
|
|
2369
|
-
}
|
|
2370
|
-
if (end !== -1) {
|
|
2371
|
-
const closingStart = start + content.length;
|
|
2372
|
-
const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
|
|
2373
|
-
const closingStartLoc = new acorn.Position(
|
|
2374
|
-
closingLineInfo.line,
|
|
2375
|
-
closingLineInfo.column,
|
|
2376
|
-
);
|
|
2377
|
-
|
|
2378
|
-
// Ensure `</style>` can't be tokenized as `<` followed by a regexp
|
|
2379
|
-
// start when we manually advance to the `/`.
|
|
2380
|
-
this.exprAllowed = false;
|
|
2381
|
-
|
|
2382
|
-
// Position after '<' (so next() reads '/')
|
|
2383
|
-
this.pos = closingStart + 1;
|
|
2384
|
-
this.type = tstt.jsxTagStart;
|
|
2385
|
-
this.start = closingStart;
|
|
2386
|
-
this.startLoc = closingStartLoc;
|
|
2387
|
-
this.next();
|
|
2388
|
-
|
|
2389
|
-
// Consume '/'
|
|
2390
|
-
this.next();
|
|
2391
|
-
|
|
2392
|
-
const closingElement = this.jsx_parseClosingElementAt(closingStart, closingStartLoc);
|
|
2393
|
-
element.closingElement = closingElement;
|
|
2394
|
-
this.exprAllowed = false;
|
|
2395
|
-
this.#path.pop();
|
|
2396
|
-
} else {
|
|
2397
|
-
this.#report_broken_markup_error(
|
|
2398
|
-
open.end,
|
|
2399
|
-
"Unclosed tag '<style>'. Expected '</style>' before end of template.",
|
|
2400
|
-
);
|
|
2401
|
-
/** @type {AST.Element} */ (element).unclosed = true;
|
|
2402
|
-
this.#path.pop();
|
|
2403
|
-
}
|
|
2404
|
-
// This node is used for Prettier - always add parsed CSS as children
|
|
2405
|
-
// for proper formatting, regardless of whether it's inside head or not
|
|
2406
|
-
/** @type {AST.Element} */ (element).children = [
|
|
2407
|
-
/** @type {AST.Node} */ (/** @type {unknown} */ (parsed_css)),
|
|
2408
|
-
];
|
|
2409
|
-
|
|
2410
|
-
// Ensure we escape JSX <tag></tag> context
|
|
2411
|
-
const curContext = this.curContext();
|
|
2412
|
-
const parent = this.#path.at(-1);
|
|
2413
|
-
const insideTemplate = this.#isNativeTemplateNode(parent);
|
|
2414
|
-
|
|
2415
|
-
if (curContext === tstc.tc_expr && !insideTemplate) {
|
|
2416
|
-
this.context.pop();
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
/** @type {AST.Element} */ (element).css = content;
|
|
2420
|
-
} else {
|
|
2421
|
-
this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
|
|
2422
|
-
enterScope: true,
|
|
2423
|
-
resetFunctionBodyDepth: true,
|
|
2424
|
-
});
|
|
2425
|
-
if (/** @type {AST.TsxCompat} */ (element).type === 'TsxCompat') {
|
|
2426
|
-
this.#reportDynamicJsxElementsInTsx(/** @type {AST.Element} */ (element).children);
|
|
2427
|
-
this.#path.pop();
|
|
2428
|
-
|
|
2429
|
-
if (!element.unclosed) {
|
|
2430
|
-
const raise_error = () => {
|
|
2431
|
-
this.raise(
|
|
2432
|
-
this.start,
|
|
2433
|
-
`Expected closing tag '</tsx:${/** @type {AST.TsxCompat} */ (element).kind}>'`,
|
|
2434
|
-
);
|
|
2435
|
-
};
|
|
2436
|
-
|
|
2437
|
-
this.next();
|
|
2438
|
-
// we should expect to see </tsx:kind>
|
|
2439
|
-
if (this.value !== '/') {
|
|
2440
|
-
raise_error();
|
|
2441
|
-
}
|
|
2442
|
-
this.next();
|
|
2443
|
-
if (this.value !== 'tsx') {
|
|
2444
|
-
raise_error();
|
|
2445
|
-
}
|
|
2446
|
-
this.next();
|
|
2447
|
-
if (this.type.label !== ':') {
|
|
2448
|
-
raise_error();
|
|
2449
|
-
}
|
|
2450
|
-
this.next();
|
|
2451
|
-
if (this.value !== /** @type {AST.TsxCompat} */ (element).kind) {
|
|
2452
|
-
raise_error();
|
|
2453
|
-
}
|
|
2454
|
-
this.next();
|
|
2455
|
-
if (this.type !== tstt.jsxTagEnd) {
|
|
2456
|
-
raise_error();
|
|
2457
|
-
}
|
|
2458
|
-
this.#popTsxTokenContextBeforeTemplateExpressionChild();
|
|
2459
|
-
this.next();
|
|
2460
|
-
}
|
|
2461
|
-
} else if (
|
|
2462
|
-
/** @type {AST.TsrxFragment} */ (element).type === 'TsrxFragment' &&
|
|
2463
|
-
this.#path[this.#path.length - 1] === element
|
|
2464
|
-
) {
|
|
2465
|
-
const displayTag = element.openingElement.name ? 'tsrx' : '';
|
|
2466
|
-
this.#report_broken_markup_error(
|
|
2467
|
-
this.start,
|
|
2468
|
-
`Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
|
|
2469
|
-
);
|
|
2470
|
-
element.unclosed = true;
|
|
2471
|
-
/** @type {AST.SourceLocation} */ (element.loc).end = {
|
|
2472
|
-
.../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
|
|
2473
|
-
};
|
|
2474
|
-
element.end = element.openingElement.end;
|
|
2475
|
-
this.#path.pop();
|
|
2476
|
-
} else if (
|
|
2477
|
-
element.type === 'Element' &&
|
|
2478
|
-
this.#path[this.#path.length - 1] === element
|
|
2479
|
-
) {
|
|
2480
|
-
// Check if this element was properly closed
|
|
2481
|
-
const tagName = this.getElementName(element.id);
|
|
2482
|
-
this.#report_broken_markup_error(
|
|
2483
|
-
this.start,
|
|
2484
|
-
`Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of template.`,
|
|
2485
|
-
);
|
|
2486
|
-
element.unclosed = true;
|
|
2487
|
-
/** @type {AST.SourceLocation} */ (element.loc).end = {
|
|
2488
|
-
.../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
|
|
2489
|
-
};
|
|
2490
|
-
element.end = element.openingElement.end;
|
|
2491
|
-
this.#path.pop();
|
|
2492
|
-
}
|
|
4151
|
+
node.end = is_fragment
|
|
4152
|
+
? /** @type {ESTreeJSX.JSXFragment} */ (node).openingFragment.end
|
|
4153
|
+
: /** @type {ESTreeJSX.JSXElement} */ (node).openingElement.end;
|
|
4154
|
+
this.#path.pop();
|
|
2493
4155
|
}
|
|
2494
4156
|
|
|
2495
|
-
//
|
|
2496
|
-
|
|
4157
|
+
// A balanced element must leave the tokenizer context exactly where it
|
|
4158
|
+
// began. The body (especially a control-flow block) can leave residue
|
|
4159
|
+
// above the children context — the children tc_expr plus a spurious
|
|
4160
|
+
// b_stat from an @if/@for block save-restore — which the old single
|
|
4161
|
+
// tc_expr pop missed when the b_stat sat on top. In expression position,
|
|
4162
|
+
// unwind back to the pre-element depth so the following JS token (e.g. a
|
|
4163
|
+
// comma/brace closing an enclosing object) tokenizes as code, not text.
|
|
2497
4164
|
const parent = this.#path.at(-1);
|
|
2498
4165
|
const insideTemplate = this.#isNativeTemplateNode(parent);
|
|
2499
4166
|
|
|
2500
|
-
if (
|
|
2501
|
-
this.context.
|
|
4167
|
+
if (!insideTemplate && this.context.length > pre_element_context_depth) {
|
|
4168
|
+
this.context.length = pre_element_context_depth;
|
|
2502
4169
|
}
|
|
2503
4170
|
}
|
|
2504
4171
|
|
|
2505
|
-
if (
|
|
2506
|
-
/** @type {
|
|
2507
|
-
|
|
4172
|
+
if (is_style && /** @type {AST.JSXStyleElement} */ (node).closingElement) {
|
|
4173
|
+
const closing = /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
|
|
4174
|
+
/** @type {AST.JSXStyleElement} */ (node).closingElement
|
|
2508
4175
|
);
|
|
4176
|
+
return this.finishNodeAt(node, node.type, closing.end, closing.loc.end);
|
|
2509
4177
|
}
|
|
2510
4178
|
|
|
2511
|
-
this.finishNode(
|
|
2512
|
-
return element;
|
|
4179
|
+
return this.finishNode(node, node.type);
|
|
2513
4180
|
}
|
|
2514
4181
|
|
|
2515
4182
|
/**
|
|
2516
4183
|
* @type {Parse.Parser['parseTemplateBody']}
|
|
2517
4184
|
*/
|
|
2518
4185
|
parseTemplateBody(body) {
|
|
2519
|
-
const
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
4186
|
+
const current_template_node = this.#currentNativeTemplateNode();
|
|
4187
|
+
if (!current_template_node) return;
|
|
4188
|
+
// Outside a `@{ … }` block every element/fragment body is plain JSX (§2,
|
|
4189
|
+
// §5). There is no script section and no `---` fence to infer — text is
|
|
4190
|
+
// text, and setup code lives only inside a code block.
|
|
4191
|
+
current_template_node.metadata ??= { path: [] };
|
|
4192
|
+
current_template_node.metadata.templateMode = 'template';
|
|
4193
|
+
|
|
4194
|
+
// `@{ … }` code block as element/fragment content (§2 rule 1). Sibling
|
|
4195
|
+
// code blocks are allowed, so this is not gated on an empty body;
|
|
4196
|
+
// reposition onto the `@` if leading whitespace was tokenized ahead of it.
|
|
4197
|
+
if (this.#atCodeBlockStart()) {
|
|
4198
|
+
const at_index = skip_whitespace_from(this.input, this.start);
|
|
4199
|
+
if (this.start !== at_index) {
|
|
4200
|
+
const loc = acorn.getLineInfo(this.input, at_index);
|
|
4201
|
+
this.pos = at_index;
|
|
4202
|
+
this.start = at_index;
|
|
4203
|
+
this.startLoc = new acorn.Position(loc.line, loc.column);
|
|
4204
|
+
this.curLine = loc.line;
|
|
4205
|
+
this.lineStart = at_index - loc.column;
|
|
2530
4206
|
}
|
|
2531
|
-
|
|
2532
|
-
this.next();
|
|
4207
|
+
body.push(/** @type {any} */ (this.#parseCodeBlock()));
|
|
2533
4208
|
this.parseTemplateBody(body);
|
|
2534
4209
|
return;
|
|
2535
4210
|
}
|
|
2536
4211
|
|
|
2537
|
-
if (!inside_func) {
|
|
2538
|
-
if (this.type.label === 'continue') {
|
|
2539
|
-
throw new Error('`continue` statements are not allowed in native templates');
|
|
2540
|
-
}
|
|
2541
|
-
if (this.type.label === 'break') {
|
|
2542
|
-
throw new Error('`break` statements are not allowed in native templates');
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
if (inside_tsx_island) {
|
|
2547
|
-
this.#parseTsxIslandBody(
|
|
2548
|
-
/** @type {AST.TsxCompat} */ (inside_tsx_island),
|
|
2549
|
-
/** @type {AST.Node[]} */ (/** @type {unknown} */ (body)),
|
|
2550
|
-
);
|
|
2551
|
-
return;
|
|
2552
|
-
}
|
|
2553
|
-
if (
|
|
2554
|
-
current_template_node?.type === 'TsrxFragment' &&
|
|
2555
|
-
!current_template_node.openingElement.name &&
|
|
2556
|
-
((this.type === tstt.jsxTagStart && this.input.slice(this.pos, this.pos + 2) === '/>') ||
|
|
2557
|
-
(this.input.charCodeAt(this.start) === CharCode.lessThan &&
|
|
2558
|
-
this.input.slice(this.start + 1, this.start + 3) === '/>'))
|
|
2559
|
-
) {
|
|
2560
|
-
this.exprAllowed = false;
|
|
2561
|
-
return;
|
|
2562
|
-
}
|
|
2563
4212
|
if (this.type === tt.braceL) {
|
|
2564
4213
|
body.push(this.#parseNativeTemplateExpressionContainer());
|
|
2565
|
-
} else if (
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
4214
|
+
} else if (this.type === tstt.jsxText) {
|
|
4215
|
+
// A nested element with its own body can leak a JSX expression context,
|
|
4216
|
+
// so the whitespace after its closing tag is mis-tokenized as a stale
|
|
4217
|
+
// text token whose start was advanced onto the following `<`. Real JSX
|
|
4218
|
+
// text never starts at `<`, so drop the leaked context and re-read the
|
|
4219
|
+
// tag instead of emitting an empty node.
|
|
4220
|
+
if (this.input.charCodeAt(this.start) === CharCode.lessThan) {
|
|
4221
|
+
while (this.curContext() === tstc.tc_expr) {
|
|
4222
|
+
this.context.pop();
|
|
4223
|
+
}
|
|
4224
|
+
this.pos = this.start;
|
|
4225
|
+
this.exprAllowed = true;
|
|
4226
|
+
this.next();
|
|
4227
|
+
this.parseTemplateBody(body);
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
const text = this.#parseTemplateRawText();
|
|
4231
|
+
if (this.#shouldKeepTemplateTextNode(text)) {
|
|
4232
|
+
body.push(text);
|
|
4233
|
+
}
|
|
4234
|
+
} else if (this.#isJSXControlFlowDirectiveStart()) {
|
|
4235
|
+
body.push(this.#parseJSXControlFlowExpression());
|
|
2570
4236
|
} else if (this.type === tt.braceR) {
|
|
2571
4237
|
// Leaving a native template body. We may still be in TSX/JSX tokenization
|
|
2572
4238
|
// context (e.g. after parsing markup), but the closing `}` is a JS token.
|
|
@@ -2578,8 +4244,7 @@ export function TSRXPlugin(config) {
|
|
|
2578
4244
|
return;
|
|
2579
4245
|
} else if (
|
|
2580
4246
|
this.type === tstt.jsxTagStart ||
|
|
2581
|
-
|
|
2582
|
-
this.input.charCodeAt(this.start + 1) === CharCode.slash)
|
|
4247
|
+
this.input.charCodeAt(this.start) === CharCode.lessThan
|
|
2583
4248
|
) {
|
|
2584
4249
|
const startPos = this.start;
|
|
2585
4250
|
const startLoc = this.startLoc;
|
|
@@ -2600,20 +4265,20 @@ export function TSRXPlugin(config) {
|
|
|
2600
4265
|
// Consume '/'
|
|
2601
4266
|
this.next();
|
|
2602
4267
|
|
|
2603
|
-
|
|
2604
|
-
|
|
4268
|
+
let closingElement;
|
|
4269
|
+
this.#closingNativeTemplateNode = true;
|
|
4270
|
+
try {
|
|
4271
|
+
closingElement = /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
|
|
2605
4272
|
this.jsx_parseClosingElementAt(startPos, startLoc)
|
|
2606
4273
|
);
|
|
4274
|
+
} finally {
|
|
4275
|
+
this.#closingNativeTemplateNode = false;
|
|
4276
|
+
}
|
|
2607
4277
|
this.exprAllowed = false;
|
|
2608
4278
|
|
|
2609
4279
|
// Validate that the closing tag matches the opening tag
|
|
2610
|
-
const currentElement = this.#path[this.#path.length - 1];
|
|
2611
|
-
if (
|
|
2612
|
-
!currentElement ||
|
|
2613
|
-
(currentElement.type !== 'Element' &&
|
|
2614
|
-
currentElement.type !== 'TsrxFragment' &&
|
|
2615
|
-
currentElement.type !== 'TsxCompat')
|
|
2616
|
-
) {
|
|
4280
|
+
const currentElement = /** @type {any} */ (this.#path[this.#path.length - 1]);
|
|
4281
|
+
if (!this.#isNativeTemplateNode(currentElement)) {
|
|
2617
4282
|
this.raise(this.start, 'Unexpected closing tag');
|
|
2618
4283
|
}
|
|
2619
4284
|
|
|
@@ -2622,21 +4287,17 @@ export function TSRXPlugin(config) {
|
|
|
2622
4287
|
/** @type {string | null} */
|
|
2623
4288
|
let closingTagName;
|
|
2624
4289
|
|
|
2625
|
-
if (currentElement.type === '
|
|
2626
|
-
openingTagName = 'tsx:' + currentElement.kind;
|
|
2627
|
-
closingTagName =
|
|
2628
|
-
closingElement.name?.type === 'JSXNamespacedName'
|
|
2629
|
-
? closingElement.name.namespace.name + ':' + closingElement.name.name.name
|
|
2630
|
-
: this.getElementName(closingElement.name);
|
|
2631
|
-
} else if (currentElement.type === 'TsrxFragment') {
|
|
4290
|
+
if (currentElement.type === 'JSXFragment') {
|
|
2632
4291
|
openingTagName = '';
|
|
2633
|
-
closingTagName =
|
|
2634
|
-
|
|
4292
|
+
closingTagName = !closingElement.name
|
|
4293
|
+
? ''
|
|
4294
|
+
: closingElement.name.type === 'JSXNamespacedName'
|
|
2635
4295
|
? closingElement.name.namespace.name + ':' + closingElement.name.name.name
|
|
2636
4296
|
: this.getElementName(closingElement.name);
|
|
2637
4297
|
} else {
|
|
2638
|
-
|
|
2639
|
-
|
|
4298
|
+
openingTagName = currentElement.openingElement?.name
|
|
4299
|
+
? this.getElementName(currentElement.openingElement.name)
|
|
4300
|
+
: null;
|
|
2640
4301
|
closingTagName = closingElement.name
|
|
2641
4302
|
? closingElement.name?.type === 'JSXNamespacedName'
|
|
2642
4303
|
? closingElement.name.namespace.name + ':' + closingElement.name.name.name
|
|
@@ -2645,6 +4306,24 @@ export function TSRXPlugin(config) {
|
|
|
2645
4306
|
}
|
|
2646
4307
|
|
|
2647
4308
|
if (openingTagName !== closingTagName) {
|
|
4309
|
+
// A closing tag that matches no open element on the path is not a
|
|
4310
|
+
// mismatch we can recover from by marking ancestors unclosed — it is
|
|
4311
|
+
// simply an unexpected closing tag (e.g. `<div></span>`).
|
|
4312
|
+
const normalized_closing_name = closingTagName ?? '';
|
|
4313
|
+
const matches_open_element = this.#path.some((node) => {
|
|
4314
|
+
const elem = /** @type {any} */ (node);
|
|
4315
|
+
if (!this.#isNativeTemplateNode(elem)) return false;
|
|
4316
|
+
const elemName =
|
|
4317
|
+
elem.type === 'JSXFragment'
|
|
4318
|
+
? ''
|
|
4319
|
+
: elem.openingElement?.name
|
|
4320
|
+
? this.getElementName(elem.openingElement.name)
|
|
4321
|
+
: null;
|
|
4322
|
+
return elemName === normalized_closing_name;
|
|
4323
|
+
});
|
|
4324
|
+
if (!matches_open_element && this.#collect) {
|
|
4325
|
+
this.raise(closingElement.start, 'Unexpected closing tag');
|
|
4326
|
+
}
|
|
2648
4327
|
// this will throw if not collecting errors
|
|
2649
4328
|
this.#report_broken_markup_error(
|
|
2650
4329
|
closingElement.start,
|
|
@@ -2653,25 +4332,19 @@ export function TSRXPlugin(config) {
|
|
|
2653
4332
|
);
|
|
2654
4333
|
// Loop through all unclosed elements on the stack
|
|
2655
4334
|
while (this.#path.length > 0) {
|
|
2656
|
-
const elem = this.#path[this.#path.length - 1];
|
|
4335
|
+
const elem = /** @type {any} */ (this.#path[this.#path.length - 1]);
|
|
2657
4336
|
|
|
2658
4337
|
// Stop at non-template boundaries.
|
|
2659
|
-
if (
|
|
2660
|
-
elem.type !== 'Element' &&
|
|
2661
|
-
elem.type !== 'TsrxFragment' &&
|
|
2662
|
-
elem.type !== 'TsxCompat'
|
|
2663
|
-
) {
|
|
4338
|
+
if (!this.#isNativeTemplateNode(elem)) {
|
|
2664
4339
|
break;
|
|
2665
4340
|
}
|
|
2666
4341
|
|
|
2667
4342
|
const elemName =
|
|
2668
|
-
elem.type === '
|
|
2669
|
-
? '
|
|
2670
|
-
: elem.
|
|
2671
|
-
?
|
|
2672
|
-
:
|
|
2673
|
-
? this.getElementName(elem.id)
|
|
2674
|
-
: null;
|
|
4343
|
+
elem.type === 'JSXFragment'
|
|
4344
|
+
? ''
|
|
4345
|
+
: elem.openingElement?.name
|
|
4346
|
+
? this.getElementName(elem.openingElement.name)
|
|
4347
|
+
: null;
|
|
2675
4348
|
|
|
2676
4349
|
// Found matching opening tag
|
|
2677
4350
|
if (elemName === closingTagName) {
|
|
@@ -2681,28 +4354,31 @@ export function TSRXPlugin(config) {
|
|
|
2681
4354
|
// Mark as unclosed and adjust location
|
|
2682
4355
|
elem.unclosed = true;
|
|
2683
4356
|
/** @type {AST.NodeWithLocation} */ (elem).loc.end = {
|
|
2684
|
-
.../** @type {AST.SourceLocation} */ (
|
|
4357
|
+
.../** @type {AST.SourceLocation} */ (
|
|
4358
|
+
elem.type === 'JSXFragment' ? elem.openingFragment.loc : elem.openingElement.loc
|
|
4359
|
+
).end,
|
|
2685
4360
|
};
|
|
2686
|
-
elem.end =
|
|
4361
|
+
elem.end =
|
|
4362
|
+
elem.type === 'JSXFragment' ? elem.openingFragment.end : elem.openingElement.end;
|
|
2687
4363
|
|
|
2688
4364
|
this.#path.pop(); // Remove from stack
|
|
2689
4365
|
}
|
|
2690
4366
|
}
|
|
2691
4367
|
|
|
2692
|
-
const elementToClose = this.#path[this.#path.length - 1];
|
|
2693
|
-
if (
|
|
2694
|
-
elementToClose &&
|
|
2695
|
-
(elementToClose.type === 'Element' || elementToClose.type === 'TsrxFragment')
|
|
2696
|
-
) {
|
|
4368
|
+
const elementToClose = /** @type {any} */ (this.#path[this.#path.length - 1]);
|
|
4369
|
+
if (this.#isNativeTemplateNode(elementToClose)) {
|
|
2697
4370
|
const elementToCloseName =
|
|
2698
|
-
elementToClose.type === '
|
|
4371
|
+
elementToClose.type === 'JSXFragment'
|
|
2699
4372
|
? ''
|
|
2700
|
-
:
|
|
2701
|
-
? this.getElementName(
|
|
4373
|
+
: elementToClose.openingElement?.name
|
|
4374
|
+
? this.getElementName(elementToClose.openingElement.name)
|
|
2702
4375
|
: null;
|
|
2703
4376
|
if (elementToCloseName === closingTagName) {
|
|
2704
|
-
|
|
2705
|
-
closingElement;
|
|
4377
|
+
if (elementToClose.type === 'JSXFragment') {
|
|
4378
|
+
elementToClose.closingFragment = this.#toClosingFragment(closingElement);
|
|
4379
|
+
} else {
|
|
4380
|
+
elementToClose.closingElement = closingElement;
|
|
4381
|
+
}
|
|
2706
4382
|
}
|
|
2707
4383
|
}
|
|
2708
4384
|
|
|
@@ -2714,16 +4390,12 @@ export function TSRXPlugin(config) {
|
|
|
2714
4390
|
if (node !== null) {
|
|
2715
4391
|
body.push(node);
|
|
2716
4392
|
}
|
|
4393
|
+
} else if (this.type === tt.eof) {
|
|
4394
|
+
return;
|
|
2717
4395
|
} else {
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
body.push(node);
|
|
2722
|
-
|
|
2723
|
-
// Ensure we're not in JSX context before recursing
|
|
2724
|
-
// This is important when elements are parsed at statement level
|
|
2725
|
-
if (this.curContext() === tstc.tc_expr) {
|
|
2726
|
-
this.context.pop();
|
|
4396
|
+
const text = this.#parseTemplateRawText();
|
|
4397
|
+
if (this.#shouldKeepTemplateTextNode(text)) {
|
|
4398
|
+
body.push(text);
|
|
2727
4399
|
}
|
|
2728
4400
|
}
|
|
2729
4401
|
|
|
@@ -2802,16 +4474,20 @@ export function TSRXPlugin(config) {
|
|
|
2802
4474
|
this.type === tt.braceL &&
|
|
2803
4475
|
this.context.some((c) => c === tstc.tc_expr)
|
|
2804
4476
|
) {
|
|
2805
|
-
return /** @type {ESTreeJSX.
|
|
2806
|
-
|
|
4477
|
+
return /** @type {ESTreeJSX.JSXExpressionContainer} */ (
|
|
4478
|
+
this.#parseNativeTemplateExpressionContainer()
|
|
2807
4479
|
);
|
|
2808
4480
|
}
|
|
2809
4481
|
|
|
2810
4482
|
if (this.type === tstt.jsxTagStart) {
|
|
2811
|
-
this
|
|
2812
|
-
|
|
2813
|
-
|
|
4483
|
+
if (this.#forceScriptJSXElementDepth > 0) {
|
|
4484
|
+
return /** @type {AST.Statement} */ (
|
|
4485
|
+
/** @type {unknown} */ (super.parseStatement(context, topLevel, exports))
|
|
4486
|
+
);
|
|
2814
4487
|
}
|
|
4488
|
+
|
|
4489
|
+
this.next();
|
|
4490
|
+
if (this.value === '/') this.unexpected();
|
|
2815
4491
|
const node = this.parseElement();
|
|
2816
4492
|
|
|
2817
4493
|
if (!node) {
|
|
@@ -2819,7 +4495,7 @@ export function TSRXPlugin(config) {
|
|
|
2819
4495
|
}
|
|
2820
4496
|
if (
|
|
2821
4497
|
this.#functionBodyDepth > 0 &&
|
|
2822
|
-
node.type === '
|
|
4498
|
+
node.type === 'JSXFragment' &&
|
|
2823
4499
|
this.curContext() === b_stat
|
|
2824
4500
|
) {
|
|
2825
4501
|
this.context.pop();
|
|
@@ -2834,16 +4510,15 @@ export function TSRXPlugin(config) {
|
|
|
2834
4510
|
}
|
|
2835
4511
|
|
|
2836
4512
|
if (
|
|
2837
|
-
this
|
|
2838
|
-
this.
|
|
2839
|
-
this.input.charCodeAt(this.start) === CharCode.doubleQuote &&
|
|
2840
|
-
(this.#path.at(-1)?.type === 'Element' || this.#path.at(-1)?.type === 'TsrxFragment')
|
|
4513
|
+
this.input.charCodeAt(this.start) === CharCode.at &&
|
|
4514
|
+
(this.#isCodeBlockStart(this.start) || this.#isJSXControlFlowDirectiveStart())
|
|
2841
4515
|
) {
|
|
2842
|
-
|
|
2843
|
-
this
|
|
2844
|
-
const node = this.parseDoubleQuotedTextChild();
|
|
4516
|
+
const node = /** @type {AST.ExpressionStatement} */ (this.startNode());
|
|
4517
|
+
node.expression = /** @type {AST.Expression} */ (this.parseExpression());
|
|
2845
4518
|
this.semicolon();
|
|
2846
|
-
return
|
|
4519
|
+
return /** @type {AST.ExpressionStatement} */ (
|
|
4520
|
+
this.finishNode(node, 'ExpressionStatement')
|
|
4521
|
+
);
|
|
2847
4522
|
}
|
|
2848
4523
|
|
|
2849
4524
|
// &[ or &{ at statement level — lazy destructuring assignment
|
|
@@ -2886,30 +4561,28 @@ export function TSRXPlugin(config) {
|
|
|
2886
4561
|
parseBlock(createNewLexicalScope, node, exitStrict) {
|
|
2887
4562
|
const parent = this.#path.at(-1);
|
|
2888
4563
|
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
) {
|
|
2896
|
-
if (createNewLexicalScope === void 0) createNewLexicalScope = true;
|
|
2897
|
-
if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());
|
|
2898
|
-
|
|
2899
|
-
node.body = [];
|
|
2900
|
-
this.#allowDoubleQuotedTextChildAfterBrace = true;
|
|
2901
|
-
this.expect(tt.braceL);
|
|
2902
|
-
this.#parseNativeTemplateBody(node, node.body, {
|
|
2903
|
-
enterScope: createNewLexicalScope,
|
|
2904
|
-
});
|
|
2905
|
-
|
|
2906
|
-
if (exitStrict) {
|
|
2907
|
-
this.strict = false;
|
|
4564
|
+
if (this.#isNativeTemplateNode(parent) && this.#templateControlFlowBlockDepth > 0) {
|
|
4565
|
+
this.#templateControlFlowBlockDepth--;
|
|
4566
|
+
try {
|
|
4567
|
+
return this.#parseTemplateControlFlowBlock(createNewLexicalScope, node, exitStrict);
|
|
4568
|
+
} finally {
|
|
4569
|
+
this.#templateControlFlowBlockDepth++;
|
|
2908
4570
|
}
|
|
2909
|
-
|
|
4571
|
+
}
|
|
2910
4572
|
|
|
2911
|
-
|
|
2912
|
-
|
|
4573
|
+
if (this.#functionBodyDepth > 0 && this.#isNativeTemplateNode(parent)) {
|
|
4574
|
+
let pushed_statement_context = false;
|
|
4575
|
+
if (this.curContext() !== b_stat) {
|
|
4576
|
+
this.context.push(b_stat);
|
|
4577
|
+
pushed_statement_context = true;
|
|
4578
|
+
}
|
|
4579
|
+
try {
|
|
4580
|
+
return super.parseBlock(createNewLexicalScope, node, exitStrict);
|
|
4581
|
+
} finally {
|
|
4582
|
+
if (pushed_statement_context && this.curContext() === b_stat) {
|
|
4583
|
+
this.context.pop();
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
2913
4586
|
}
|
|
2914
4587
|
|
|
2915
4588
|
return super.parseBlock(createNewLexicalScope, node, exitStrict);
|