@tsrx/core 0.1.20 → 0.1.22
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 +119 -61
- package/src/analyze/validation.js +122 -5
- package/src/index.js +11 -3
- package/src/parse/index.js +157 -99
- package/src/plugin.js +2417 -844
- package/src/runtime/iterable.js +15 -13
- 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 +44 -59
- package/src/transform/jsx/helpers.js +8 -5
- package/src/transform/jsx/index.js +755 -344
- package/src/transform/jsx-interleave.js +1 -2
- package/src/transform/scoping.js +26 -63
- package/src/transform/segments.js +53 -5
- package/src/transform/style-ref.js +3 -11
- package/src/utils/builders.js +2 -3
- package/types/index.d.ts +179 -110
- package/types/jsx-platform.d.ts +6 -11
- package/types/parse.d.ts +36 -10
- package/types/runtime/ref.d.ts +1 -0
package/src/plugin.js
CHANGED
|
@@ -5,20 +5,17 @@
|
|
|
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 DYNAMIC_ELEMENT_IN_TSX_ERROR =
|
|
21
|
-
'Dynamic element syntax (`<@...>`) is only supported in native TSRX templates.';
|
|
22
19
|
const DYNAMIC_ATTRIBUTE_NAME_ERROR =
|
|
23
20
|
'Dynamic component / element syntax (`@`) is only supported on native TSRX element names, not attribute names.';
|
|
24
21
|
|
|
@@ -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,24 @@ 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
|
+
#consumeContainerBraceAfterScope = false;
|
|
260
|
+
#scriptJSXElementDepth = 0;
|
|
261
|
+
#forceScriptJSXElementDepth = 0;
|
|
262
|
+
#suppressTemplateRawTextToken = false;
|
|
263
|
+
#templateScriptParsingDepth = 0;
|
|
264
|
+
#controlFlowBlockAllowsNativeReturn = false;
|
|
265
|
+
#parsingJSXSwitchCaseScriptStatementDepth = 0;
|
|
266
|
+
#templateControlFlowBlockDepth = 0;
|
|
267
|
+
#templateControlFlowTryDepth = 0;
|
|
268
|
+
/** @type {Parse.Parser['context']} */
|
|
269
|
+
context = [b_stat];
|
|
270
|
+
/** @type {AST.Node | null} */
|
|
271
|
+
#openingNativeTemplateNode = null;
|
|
272
|
+
#closingNativeTemplateNode = false;
|
|
273
|
+
#readingJSXControlFlowDirectiveKeyword = false;
|
|
274
|
+
#readingJSXControlFlowHeader = false;
|
|
240
275
|
|
|
241
276
|
/**
|
|
242
277
|
* @type {Parse.Parser['finishNode']}
|
|
@@ -260,6 +295,7 @@ export function TSRXPlugin(config) {
|
|
|
260
295
|
*/
|
|
261
296
|
constructor(options, input) {
|
|
262
297
|
super(options, input);
|
|
298
|
+
this.context ??= [b_stat];
|
|
263
299
|
const tsrx_options = options?.tsrxOptions ?? options?.rippleOptions;
|
|
264
300
|
this.#collect = tsrx_options?.collect === true || tsrx_options?.loose === true;
|
|
265
301
|
this.#loose = tsrx_options?.loose === true;
|
|
@@ -267,6 +303,7 @@ export function TSRXPlugin(config) {
|
|
|
267
303
|
this.#filename = tsrx_options?.filename || null;
|
|
268
304
|
}
|
|
269
305
|
|
|
306
|
+
/** @this {Parse.Parser} */
|
|
270
307
|
#resetTokenStartToCurrentPosition() {
|
|
271
308
|
if (this.start !== this.pos) {
|
|
272
309
|
this.start = this.pos;
|
|
@@ -274,23 +311,6 @@ export function TSRXPlugin(config) {
|
|
|
274
311
|
}
|
|
275
312
|
}
|
|
276
313
|
|
|
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
314
|
/**
|
|
295
315
|
* Native TSRX template bodies share one grammar across elements and fragments.
|
|
296
316
|
* This helper keeps the parser-state setup in one place while callers keep
|
|
@@ -336,205 +356,1592 @@ export function TSRXPlugin(config) {
|
|
|
336
356
|
}
|
|
337
357
|
}
|
|
338
358
|
|
|
359
|
+
/**
|
|
360
|
+
* @param {boolean} [createNewLexicalScope]
|
|
361
|
+
* @param {AST.BlockStatement} [node]
|
|
362
|
+
* @param {boolean} [exitStrict]
|
|
363
|
+
* @returns {AST.BlockStatement}
|
|
364
|
+
*/
|
|
365
|
+
#parseTemplateControlFlowBlock(createNewLexicalScope = true, node, exitStrict) {
|
|
366
|
+
node ??= /** @type {AST.BlockStatement} */ (this.startNode());
|
|
367
|
+
// Consume the flag for this block only; nested control-flow blocks
|
|
368
|
+
// parsed inside the body must not inherit it.
|
|
369
|
+
const allows_native_return = this.#controlFlowBlockAllowsNativeReturn;
|
|
370
|
+
this.#controlFlowBlockAllowsNativeReturn = false;
|
|
371
|
+
node.body = [];
|
|
372
|
+
node.metadata = {
|
|
373
|
+
...node.metadata,
|
|
374
|
+
path: [],
|
|
375
|
+
native_tsrx_template_block: true,
|
|
376
|
+
templateMode: 'script',
|
|
377
|
+
allows_native_return,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// A directive's `{ }` IS a code block (§2 rule 8): setup statements then
|
|
381
|
+
// at most one render node. Code-only blocks are allowed (§2 rule 6). Hide
|
|
382
|
+
// the enclosing template from `#path` so the body tokenizes as code (not
|
|
383
|
+
// JSX raw text); render nodes re-establish their own path via `parseElement`.
|
|
384
|
+
const enclosing_context = this.context;
|
|
385
|
+
const enclosing_path = this.#path;
|
|
386
|
+
this.context = enclosing_context.filter(
|
|
387
|
+
(context) =>
|
|
388
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
389
|
+
);
|
|
390
|
+
if (this.curContext() !== b_stat) {
|
|
391
|
+
this.context.push(b_stat);
|
|
392
|
+
}
|
|
393
|
+
this.#path = [];
|
|
394
|
+
if (createNewLexicalScope) {
|
|
395
|
+
this.enterScope(0);
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
this.expect(tt.braceL);
|
|
399
|
+
this.#parseCodeBlockBody(node.body);
|
|
400
|
+
} finally {
|
|
401
|
+
if (createNewLexicalScope) {
|
|
402
|
+
this.exitScope();
|
|
403
|
+
}
|
|
404
|
+
this.#path = enclosing_path;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (exitStrict) {
|
|
408
|
+
this.strict = false;
|
|
409
|
+
}
|
|
410
|
+
this.exprAllowed = true;
|
|
411
|
+
this.context = enclosing_context;
|
|
412
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
413
|
+
this.#readingJSXControlFlowHeader = true;
|
|
414
|
+
try {
|
|
415
|
+
this.next();
|
|
416
|
+
} finally {
|
|
417
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
418
|
+
}
|
|
419
|
+
return this.finishNode(node, 'BlockStatement');
|
|
420
|
+
}
|
|
421
|
+
|
|
339
422
|
/**
|
|
340
423
|
* @param {AST.Node | undefined} node
|
|
341
424
|
*/
|
|
342
425
|
#isNativeTemplateNode(node) {
|
|
343
426
|
return (
|
|
344
|
-
node?.
|
|
427
|
+
node?.metadata?.native_tsrx_template_block ||
|
|
428
|
+
(node?.type === 'JSXElement' && node.metadata?.native_tsrx) ||
|
|
429
|
+
(node?.type === 'JSXFragment' && node.metadata?.native_tsrx) ||
|
|
430
|
+
(node?.type === 'JSXStyleElement' && node.metadata?.native_tsrx)
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
#currentNativeTemplateNode() {
|
|
435
|
+
return (
|
|
436
|
+
this.#openingNativeTemplateNode ??
|
|
437
|
+
this.#path.findLast((node) => this.#isNativeTemplateNode(node))
|
|
345
438
|
);
|
|
346
439
|
}
|
|
347
440
|
|
|
348
441
|
/**
|
|
349
|
-
* @param {AST.Node
|
|
442
|
+
* @param {AST.Node | undefined} node
|
|
443
|
+
* @param {string} name
|
|
350
444
|
*/
|
|
351
|
-
#
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
445
|
+
#isNativeElementNamed(node, name) {
|
|
446
|
+
return (
|
|
447
|
+
(node?.type === 'JSXElement' || node?.type === 'JSXStyleElement') &&
|
|
448
|
+
node.metadata?.native_tsrx &&
|
|
449
|
+
this.getElementName(node.openingElement?.name) === name
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* @param {any} name
|
|
455
|
+
* @returns {boolean}
|
|
456
|
+
*/
|
|
457
|
+
#isDynamicJSXElementName(name) {
|
|
458
|
+
if (!name || typeof name !== 'object') return false;
|
|
459
|
+
if (name.dynamic === true) return true;
|
|
460
|
+
return name.type === 'JSXMemberExpression' && this.#isDynamicJSXElementName(name.object);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#isInsideNativeTemplateScriptSection() {
|
|
464
|
+
const node = this.#currentNativeTemplateNode();
|
|
465
|
+
return !!node && node.metadata?.templateMode !== 'template';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#isStyleOpeningTagStart() {
|
|
469
|
+
let index = this.start + 1;
|
|
470
|
+
if (this.input.charCodeAt(index) === CharCode.slash) return false;
|
|
471
|
+
if (this.input.slice(index, index + 'style'.length) !== 'style') return false;
|
|
472
|
+
|
|
473
|
+
const after = this.input.charCodeAt(index + 'style'.length);
|
|
474
|
+
return (
|
|
475
|
+
after === CharCode.greaterThan ||
|
|
476
|
+
after === CharCode.slash ||
|
|
477
|
+
after === CharCode.space ||
|
|
478
|
+
after === CharCode.tab ||
|
|
479
|
+
after === CharCode.lineFeed ||
|
|
480
|
+
after === CharCode.carriageReturn
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* @param {number} index
|
|
486
|
+
*/
|
|
487
|
+
#isLineStartPosition(index) {
|
|
488
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
489
|
+
const ch = this.input.charCodeAt(i);
|
|
490
|
+
if (ch === CharCode.lineFeed || ch === CharCode.carriageReturn) return true;
|
|
491
|
+
if (ch !== CharCode.space && ch !== CharCode.tab) return false;
|
|
492
|
+
}
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* @param {number} index
|
|
498
|
+
*/
|
|
499
|
+
#previousNonSpaceTabIndex(index) {
|
|
500
|
+
let cursor = index - 1;
|
|
501
|
+
while (
|
|
502
|
+
cursor >= 0 &&
|
|
503
|
+
(this.input.charCodeAt(cursor) === CharCode.space ||
|
|
504
|
+
this.input.charCodeAt(cursor) === CharCode.tab)
|
|
505
|
+
) {
|
|
506
|
+
cursor--;
|
|
507
|
+
}
|
|
508
|
+
return cursor;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @param {number} end_index Inclusive index of the keyword's last character.
|
|
513
|
+
* @param {string} keyword
|
|
514
|
+
*/
|
|
515
|
+
#keywordEndsAt(end_index, keyword) {
|
|
516
|
+
const start = end_index - keyword.length + 1;
|
|
517
|
+
if (start < 0) return false;
|
|
518
|
+
if (this.input.slice(start, end_index + 1) !== keyword) return false;
|
|
519
|
+
return !this.#isIdentifierChar(this.input.charCodeAt(start - 1));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Returns true when a `<` at `index` can start TypeScript type
|
|
524
|
+
* parameters/arguments in expression-like code rather than a JSX tag.
|
|
525
|
+
* Most type argument lists are adjacent to the previous token (`foo<T>`,
|
|
526
|
+
* `build<T>()`, `Map<K, V>`). The whitespace-separated form is valid for
|
|
527
|
+
* anonymous generic function expressions (`function <T>() {}`); generic
|
|
528
|
+
* arrows are handled separately by `looks_like_generic_arrow`.
|
|
529
|
+
*
|
|
530
|
+
* @param {number} index
|
|
531
|
+
*/
|
|
532
|
+
#canStartTypeParameterOrArgumentList(index) {
|
|
533
|
+
const previous = this.#previousNonSpaceTabIndex(index);
|
|
534
|
+
if (previous < 0) return false;
|
|
535
|
+
if (previous === index - 1) {
|
|
536
|
+
return this.#canPrecedeTypeArgumentList(this.input.charCodeAt(previous));
|
|
537
|
+
}
|
|
538
|
+
return this.#keywordEndsAt(previous, 'function');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
#parseTemplateRawText() {
|
|
542
|
+
const start = this.start;
|
|
543
|
+
// The current jsxText token spans `[start, token_end]`. Comments inside
|
|
544
|
+
// that span were already consumed and recorded by the tokenizer
|
|
545
|
+
// (`jsx_readToken`); only comments at/after `token_end` (e.g. a body that
|
|
546
|
+
// opens with a comment, where the raw-text token stops before it) still
|
|
547
|
+
// need recording here. Either way we drop `//` lines from the JSXText value
|
|
548
|
+
// and always advance past them so the scan can't re-tokenize the same spot.
|
|
549
|
+
const token_end = this.end;
|
|
550
|
+
let index = start;
|
|
551
|
+
let value = '';
|
|
552
|
+
while (index < this.input.length) {
|
|
553
|
+
if (this.#isTemplateLineCommentStart(index)) {
|
|
554
|
+
const comment_start = index;
|
|
555
|
+
const comment_start_loc = acorn.getLineInfo(this.input, comment_start);
|
|
556
|
+
index += 2;
|
|
557
|
+
while (
|
|
558
|
+
index < this.input.length &&
|
|
559
|
+
this.input.charCodeAt(index) !== CharCode.lineFeed &&
|
|
560
|
+
this.input.charCodeAt(index) !== CharCode.carriageReturn
|
|
561
|
+
) {
|
|
562
|
+
index++;
|
|
563
|
+
}
|
|
564
|
+
if (this.options.onComment && comment_start >= token_end) {
|
|
565
|
+
const comment_end_loc = acorn.getLineInfo(this.input, index);
|
|
566
|
+
// Pass null metadata so position-based attachment places the comment
|
|
567
|
+
// as a leading comment on the following child (which the JSX printers
|
|
568
|
+
// emit), rather than on the container's `elementLeadingComments`.
|
|
569
|
+
this.options.onComment(
|
|
570
|
+
false,
|
|
571
|
+
this.input.slice(comment_start + 2, index),
|
|
572
|
+
comment_start,
|
|
573
|
+
index,
|
|
574
|
+
new acorn.Position(comment_start_loc.line, comment_start_loc.column),
|
|
575
|
+
new acorn.Position(comment_end_loc.line, comment_end_loc.column),
|
|
576
|
+
/** @type {any} */ (null),
|
|
365
577
|
);
|
|
366
578
|
}
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const ch = this.input.charCodeAt(index);
|
|
582
|
+
if (
|
|
583
|
+
ch === CharCode.lessThan ||
|
|
584
|
+
ch === CharCode.openBrace ||
|
|
585
|
+
ch === CharCode.closeBrace ||
|
|
586
|
+
this.#isJSXControlFlowDirectiveAt(index)
|
|
587
|
+
) {
|
|
588
|
+
break;
|
|
370
589
|
}
|
|
590
|
+
value += this.input[index];
|
|
591
|
+
index++;
|
|
371
592
|
}
|
|
372
|
-
}
|
|
373
593
|
|
|
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';
|
|
594
|
+
const endLoc = acorn.getLineInfo(this.input, index);
|
|
595
|
+
const node = /** @type {ESTreeJSX.JSXText} */ (this.startNodeAt(start, this.startLoc));
|
|
596
|
+
node.value = value;
|
|
597
|
+
node.raw = this.input.slice(start, index);
|
|
598
|
+
|
|
599
|
+
if (node.raw.match(regex_newline_characters)) {
|
|
600
|
+
this.curLine = endLoc.line;
|
|
601
|
+
this.lineStart = index - endLoc.column;
|
|
388
602
|
}
|
|
603
|
+
this.pos = index;
|
|
604
|
+
this.#popTemplateLiteralTokenContext();
|
|
605
|
+
this.next();
|
|
389
606
|
|
|
390
|
-
return
|
|
391
|
-
/** @type {unknown} */ (node)
|
|
392
|
-
);
|
|
607
|
+
return this.finishNodeAt(node, 'JSXText', index, endLoc);
|
|
393
608
|
}
|
|
394
609
|
|
|
395
610
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
611
|
+
* JSX significant-whitespace rule for a template text child. Non-whitespace
|
|
612
|
+
* text is always kept; whitespace-only text is kept only when it is an
|
|
613
|
+
* intentional inline space (no newline) separating two siblings, and dropped
|
|
614
|
+
* when it is layout indentation (contains a newline).
|
|
615
|
+
*
|
|
616
|
+
* @param {ESTreeJSX.JSXText} node
|
|
398
617
|
*/
|
|
399
|
-
#
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
618
|
+
#shouldKeepTemplateTextNode(node) {
|
|
619
|
+
if (!isWhitespaceTextNode(node)) {
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
return node.value !== '' && !regex_newline_characters.test(node.value);
|
|
623
|
+
}
|
|
403
624
|
|
|
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
|
-
}
|
|
625
|
+
#isSwitchCaseScriptStatementStart() {
|
|
626
|
+
let index = skip_whitespace_from(this.input, this.start);
|
|
418
627
|
|
|
419
|
-
|
|
420
|
-
this.exprAllowed = false;
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
628
|
+
const first = this.input.charCodeAt(index);
|
|
423
629
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
630
|
+
if (first === CharCode.openBracket || first === CharCode.openBrace) {
|
|
631
|
+
let depth = 0;
|
|
632
|
+
let i = index;
|
|
633
|
+
for (; i < this.input.length; i++) {
|
|
634
|
+
const ch = this.input.charCodeAt(i);
|
|
635
|
+
if (
|
|
636
|
+
ch === CharCode.openBracket ||
|
|
637
|
+
ch === CharCode.openBrace ||
|
|
638
|
+
ch === CharCode.openParen
|
|
639
|
+
) {
|
|
640
|
+
depth++;
|
|
641
|
+
} else if (
|
|
642
|
+
ch === CharCode.closeBracket ||
|
|
643
|
+
ch === CharCode.closeBrace ||
|
|
644
|
+
ch === CharCode.closeParen
|
|
645
|
+
) {
|
|
646
|
+
depth--;
|
|
647
|
+
if (depth === 0) {
|
|
648
|
+
i++;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
432
651
|
}
|
|
433
|
-
this.#popTemplateLiteralTokenContext();
|
|
434
|
-
this.next();
|
|
435
652
|
}
|
|
653
|
+
if (depth !== 0) return false;
|
|
654
|
+
i = skip_whitespace_from(this.input, i);
|
|
655
|
+
if (this.input.charCodeAt(i) !== CharCode.equals) return false;
|
|
656
|
+
const next = this.input.charCodeAt(i + 1);
|
|
657
|
+
return next !== CharCode.equals && next !== CharCode.greaterThan;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (
|
|
661
|
+
!this.#isIdentifierChar(first) ||
|
|
662
|
+
(first >= CharCode.digit0 && first <= CharCode.digit9)
|
|
663
|
+
) {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const word_start = index;
|
|
668
|
+
index++;
|
|
669
|
+
while (this.#isIdentifierChar(this.input.charCodeAt(index))) {
|
|
670
|
+
index++;
|
|
671
|
+
}
|
|
672
|
+
const word = this.input.slice(word_start, index);
|
|
673
|
+
if (
|
|
674
|
+
word === 'const' ||
|
|
675
|
+
word === 'let' ||
|
|
676
|
+
word === 'var' ||
|
|
677
|
+
word === 'function' ||
|
|
678
|
+
word === 'class' ||
|
|
679
|
+
word === 'if' ||
|
|
680
|
+
word === 'for' ||
|
|
681
|
+
word === 'switch' ||
|
|
682
|
+
word === 'try' ||
|
|
683
|
+
word === 'throw'
|
|
684
|
+
) {
|
|
685
|
+
return true;
|
|
436
686
|
}
|
|
687
|
+
|
|
688
|
+
index = skip_whitespace_from(this.input, index);
|
|
689
|
+
if (this.input.charCodeAt(index) !== CharCode.equals) return false;
|
|
690
|
+
const next = this.input.charCodeAt(index + 1);
|
|
691
|
+
return next !== CharCode.equals && next !== CharCode.greaterThan;
|
|
437
692
|
}
|
|
438
693
|
|
|
439
|
-
#
|
|
440
|
-
this
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
|
|
694
|
+
#switchCaseLabelStart(index = this.start) {
|
|
695
|
+
while (index < this.input.length) {
|
|
696
|
+
const ch = this.input.charCodeAt(index);
|
|
697
|
+
if (
|
|
698
|
+
ch !== CharCode.space &&
|
|
699
|
+
ch !== CharCode.tab &&
|
|
700
|
+
ch !== CharCode.lineFeed &&
|
|
701
|
+
ch !== CharCode.carriageReturn
|
|
702
|
+
) {
|
|
703
|
+
break;
|
|
444
704
|
}
|
|
705
|
+
index++;
|
|
706
|
+
}
|
|
707
|
+
if (!this.#isLineStartPosition(index)) return -1;
|
|
708
|
+
if (this.input.charCodeAt(index) !== CharCode.at) return -1;
|
|
709
|
+
index++;
|
|
710
|
+
if (
|
|
711
|
+
this.input.slice(index, index + 4) === 'case' &&
|
|
712
|
+
!this.#isIdentifierChar(this.input.charCodeAt(index + 4))
|
|
713
|
+
) {
|
|
714
|
+
return index;
|
|
715
|
+
}
|
|
716
|
+
if (
|
|
717
|
+
this.input.slice(index, index + 7) === 'default' &&
|
|
718
|
+
!this.#isIdentifierChar(this.input.charCodeAt(index + 7))
|
|
719
|
+
) {
|
|
720
|
+
return index;
|
|
721
|
+
}
|
|
722
|
+
return -1;
|
|
723
|
+
}
|
|
445
724
|
|
|
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--;
|
|
725
|
+
#rewindToSwitchCaseLabel() {
|
|
726
|
+
const start = this.#switchCaseLabelStart();
|
|
727
|
+
if (start === -1) return false;
|
|
728
|
+
while (this.curContext() === tstc.tc_expr) {
|
|
729
|
+
this.context.pop();
|
|
460
730
|
}
|
|
731
|
+
this.pos = start;
|
|
732
|
+
this.start = start;
|
|
733
|
+
this.startLoc = acorn.getLineInfo(this.input, start);
|
|
734
|
+
this.exprAllowed = true;
|
|
735
|
+
this.#suppressTemplateRawTextToken = true;
|
|
736
|
+
this.next();
|
|
737
|
+
return true;
|
|
461
738
|
}
|
|
462
739
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
740
|
+
/**
|
|
741
|
+
* @param {number} index
|
|
742
|
+
*/
|
|
743
|
+
#switchCaseBoundaryStart(index) {
|
|
744
|
+
if (!this.#isLineStartPosition(index)) return -1;
|
|
745
|
+
let wordStart = index;
|
|
746
|
+
while (wordStart < this.input.length) {
|
|
747
|
+
const ch = this.input.charCodeAt(wordStart);
|
|
748
|
+
if (ch !== CharCode.space && ch !== CharCode.tab) break;
|
|
749
|
+
wordStart++;
|
|
466
750
|
}
|
|
467
751
|
|
|
468
|
-
|
|
752
|
+
const ch = this.input.charCodeAt(wordStart);
|
|
753
|
+
if (ch === CharCode.closeBrace) return index;
|
|
754
|
+
if (ch === CharCode.at) {
|
|
755
|
+
const keywordStart = wordStart + 1;
|
|
756
|
+
if (
|
|
757
|
+
this.input.slice(keywordStart, keywordStart + 4) === 'case' &&
|
|
758
|
+
!this.#isIdentifierChar(this.input.charCodeAt(keywordStart + 4))
|
|
759
|
+
) {
|
|
760
|
+
return index;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (
|
|
764
|
+
this.input.slice(keywordStart, keywordStart + 7) === 'default' &&
|
|
765
|
+
!this.#isIdentifierChar(this.input.charCodeAt(keywordStart + 7))
|
|
766
|
+
) {
|
|
767
|
+
return index;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
for (const keyword of ['break', 'continue', 'return', 'throw']) {
|
|
772
|
+
if (
|
|
773
|
+
this.input.slice(wordStart, wordStart + keyword.length) === keyword &&
|
|
774
|
+
!this.#isIdentifierChar(this.input.charCodeAt(wordStart + keyword.length))
|
|
775
|
+
) {
|
|
776
|
+
return index;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return -1;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* @param {number} ch
|
|
785
|
+
*/
|
|
786
|
+
#isIdentifierChar(ch) {
|
|
787
|
+
return (
|
|
788
|
+
(ch >= CharCode.uppercaseA && ch <= CharCode.uppercaseZ) ||
|
|
789
|
+
(ch >= CharCode.lowercaseA && ch <= CharCode.lowercaseZ) ||
|
|
790
|
+
(ch >= CharCode.digit0 && ch <= CharCode.digit9) ||
|
|
791
|
+
ch === CharCode.underscore ||
|
|
792
|
+
ch === CharCode.dollar
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* @param {number} ch
|
|
798
|
+
*/
|
|
799
|
+
#canPrecedeTypeArgumentList(ch) {
|
|
800
|
+
return this.#isIdentifierChar(ch) || ch === CharCode.closeParen;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** @this {TSRXParser & Parse.Parser} */
|
|
804
|
+
#parseJSXSwitchCaseRawText() {
|
|
805
|
+
const start = this.start;
|
|
806
|
+
let index = start;
|
|
807
|
+
let found_boundary = false;
|
|
469
808
|
while (index < this.input.length) {
|
|
809
|
+
const boundary = this.#switchCaseBoundaryStart(index);
|
|
810
|
+
if (boundary !== -1) {
|
|
811
|
+
index = boundary;
|
|
812
|
+
found_boundary = true;
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
|
|
470
816
|
const ch = this.input.charCodeAt(index);
|
|
471
817
|
if (
|
|
472
|
-
ch === CharCode.
|
|
473
|
-
ch === CharCode.
|
|
474
|
-
ch === CharCode.
|
|
475
|
-
ch === CharCode.
|
|
818
|
+
ch === CharCode.lessThan ||
|
|
819
|
+
ch === CharCode.openBrace ||
|
|
820
|
+
ch === CharCode.closeBrace ||
|
|
821
|
+
ch === CharCode.at
|
|
476
822
|
) {
|
|
477
|
-
index++;
|
|
478
|
-
} else {
|
|
479
823
|
break;
|
|
480
824
|
}
|
|
825
|
+
index++;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const endLoc = acorn.getLineInfo(this.input, index);
|
|
829
|
+
const node = /** @type {ESTreeJSX.JSXText} */ (this.startNodeAt(start, this.startLoc));
|
|
830
|
+
node.value = this.input.slice(start, index);
|
|
831
|
+
node.raw = node.value;
|
|
832
|
+
|
|
833
|
+
if (node.value.match(regex_newline_characters)) {
|
|
834
|
+
this.curLine = endLoc.line;
|
|
835
|
+
this.lineStart = index - endLoc.column;
|
|
836
|
+
}
|
|
837
|
+
this.pos = index;
|
|
838
|
+
if (found_boundary) {
|
|
839
|
+
this.context = this.context.filter(
|
|
840
|
+
(context) =>
|
|
841
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
842
|
+
);
|
|
843
|
+
if (this.curContext() !== b_stat) {
|
|
844
|
+
this.context.push(b_stat);
|
|
845
|
+
}
|
|
846
|
+
this.exprAllowed = true;
|
|
847
|
+
this.#suppressTemplateRawTextToken = true;
|
|
848
|
+
}
|
|
849
|
+
this.next();
|
|
850
|
+
|
|
851
|
+
return this.finishNodeAt(node, 'JSXText', index, endLoc);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
#shouldReadTemplateRawTextToken() {
|
|
855
|
+
if (
|
|
856
|
+
this.#closingNativeTemplateNode ||
|
|
857
|
+
this.#readingJSXControlFlowDirectiveKeyword ||
|
|
858
|
+
this.#readingJSXControlFlowHeader ||
|
|
859
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth > 0 ||
|
|
860
|
+
this.#templateScriptParsingDepth > 0 ||
|
|
861
|
+
this.#jsxExpressionContainerDepth > 0
|
|
862
|
+
) {
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
const current_context_token = this.curContext()?.token;
|
|
866
|
+
if (current_context_token === '<tag' || current_context_token === '</tag') {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
if (this.labels.some((label) => label.kind === 'switch')) {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
const current_template_node = this.#currentNativeTemplateNode();
|
|
873
|
+
if (!current_template_node || this.#isJSXControlFlowDirectiveAt(this.pos)) {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
if (this.#isTemplateLineCommentStart(this.pos)) {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
if (this.#switchCaseLabelStart(this.pos) !== -1) {
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
if (this.input.charCodeAt(this.pos - 1) === CharCode.lessThan) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
if (
|
|
886
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.slash &&
|
|
887
|
+
this.input.charCodeAt(this.pos - 2) === CharCode.lessThan
|
|
888
|
+
) {
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
if (
|
|
892
|
+
this.input.charCodeAt(this.pos) === CharCode.slash &&
|
|
893
|
+
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
|
|
894
|
+
) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
if (
|
|
898
|
+
this.input.charCodeAt(this.pos) === CharCode.greaterThan &&
|
|
899
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.slash &&
|
|
900
|
+
this.input.charCodeAt(this.pos - 2) === CharCode.lessThan
|
|
901
|
+
) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
// Just past a self-closing tag's `/>`: that element has no body, so any
|
|
905
|
+
// following raw text belongs to an enclosing template, not to it. With no
|
|
906
|
+
// enclosing template (e.g. a top-level `return <div />`), the trailing
|
|
907
|
+
// text is plain JS and must not be read as template raw text.
|
|
908
|
+
const opening = this.#openingNativeTemplateNode;
|
|
909
|
+
if (
|
|
910
|
+
opening &&
|
|
911
|
+
current_template_node === opening &&
|
|
912
|
+
/** @type {any} */ (opening).openingElement?.selfClosing &&
|
|
913
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.greaterThan &&
|
|
914
|
+
this.input.charCodeAt(this.pos - 2) === CharCode.slash
|
|
915
|
+
) {
|
|
916
|
+
const enclosing = this.#path.findLast(
|
|
917
|
+
(node) => node !== opening && this.#isNativeTemplateNode(node),
|
|
918
|
+
);
|
|
919
|
+
if (!enclosing) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
#readTemplateRawTextToken() {
|
|
928
|
+
const start = this.pos;
|
|
929
|
+
const index = this.#templateRawTextEnd(start);
|
|
930
|
+
|
|
931
|
+
const endLoc = acorn.getLineInfo(this.input, index);
|
|
932
|
+
const value = this.input.slice(start, index);
|
|
933
|
+
if (value.match(regex_newline_characters)) {
|
|
934
|
+
this.curLine = endLoc.line;
|
|
935
|
+
this.lineStart = index - endLoc.column;
|
|
936
|
+
}
|
|
937
|
+
this.pos = index;
|
|
938
|
+
return this.finishToken(tstt.jsxText, value);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* @param {number} index
|
|
943
|
+
*/
|
|
944
|
+
#isTemplateLineCommentStart(index) {
|
|
945
|
+
return (
|
|
946
|
+
this.input.charCodeAt(index) === CharCode.slash &&
|
|
947
|
+
this.input.charCodeAt(index + 1) === CharCode.slash &&
|
|
948
|
+
this.#isLineStartPosition(index)
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* @param {number} start
|
|
954
|
+
*/
|
|
955
|
+
#templateRawTextEnd(start) {
|
|
956
|
+
let index = start;
|
|
957
|
+
while (index < this.input.length) {
|
|
958
|
+
const ch = this.input.charCodeAt(index);
|
|
959
|
+
if (
|
|
960
|
+
ch === CharCode.lessThan ||
|
|
961
|
+
ch === CharCode.openBrace ||
|
|
962
|
+
ch === CharCode.closeBrace ||
|
|
963
|
+
this.#isJSXControlFlowDirectiveAt(index) ||
|
|
964
|
+
this.#isTemplateLineCommentStart(index)
|
|
965
|
+
) {
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
index++;
|
|
969
|
+
}
|
|
970
|
+
return index;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* @param {number} index
|
|
975
|
+
*/
|
|
976
|
+
#isJSXControlFlowDirectiveAt(index) {
|
|
977
|
+
if (this.input.charCodeAt(index) !== CharCode.at) return false;
|
|
978
|
+
|
|
979
|
+
let cursor = index + 1;
|
|
980
|
+
if (!this.#isIdentifierChar(this.input.charCodeAt(cursor))) return false;
|
|
981
|
+
|
|
982
|
+
const word_start = cursor;
|
|
983
|
+
cursor++;
|
|
984
|
+
while (this.#isIdentifierChar(this.input.charCodeAt(cursor))) {
|
|
985
|
+
cursor++;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const word = this.input.slice(word_start, cursor);
|
|
989
|
+
const next_non_whitespace = skip_whitespace_from(this.input, cursor);
|
|
990
|
+
const next = this.input.charCodeAt(next_non_whitespace);
|
|
991
|
+
if (this.#isIdentifierChar(this.input.charCodeAt(cursor))) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
if (word === 'try') {
|
|
995
|
+
return next === CharCode.openBrace;
|
|
996
|
+
}
|
|
997
|
+
if (word === 'for') {
|
|
998
|
+
if (next === CharCode.openParen) return true;
|
|
999
|
+
if (
|
|
1000
|
+
this.input.slice(next_non_whitespace, next_non_whitespace + 5) === 'await' &&
|
|
1001
|
+
!this.#isIdentifierChar(this.input.charCodeAt(next_non_whitespace + 5))
|
|
1002
|
+
) {
|
|
1003
|
+
const after_await = skip_whitespace_from(this.input, next_non_whitespace + 5);
|
|
1004
|
+
return this.input.charCodeAt(after_await) === CharCode.openParen;
|
|
1005
|
+
}
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
return (word === 'if' || word === 'switch') && next === CharCode.openParen;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
#isJSXControlFlowDirectiveStart() {
|
|
1012
|
+
return this.#isJSXControlFlowDirectiveAt(this.start);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* `@{ … }` code block: an `@` immediately followed by `{` at child/body
|
|
1017
|
+
* position. This is the marker that switches a body from plain JSX to a JS
|
|
1018
|
+
* code block (§2). Whitespace between `@` and `{` is not allowed — they must
|
|
1019
|
+
* be adjacent so it can never be confused with an `@directive` or a literal
|
|
1020
|
+
* `@` followed by an expression container.
|
|
1021
|
+
* @param {number} index
|
|
1022
|
+
*/
|
|
1023
|
+
#isCodeBlockStart(index) {
|
|
1024
|
+
return (
|
|
1025
|
+
this.input.charCodeAt(index) === CharCode.at &&
|
|
1026
|
+
this.input.charCodeAt(index + 1) === CharCode.openBrace
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* True when the body position starting at `this.start` opens a `@{ … }`
|
|
1032
|
+
* code block, skipping leading whitespace.
|
|
1033
|
+
*/
|
|
1034
|
+
#atCodeBlockStart() {
|
|
1035
|
+
const index = skip_whitespace_from(this.input, this.start);
|
|
1036
|
+
return this.#isCodeBlockStart(index);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* @param {AST.Node | null | undefined} node
|
|
1041
|
+
*/
|
|
1042
|
+
#isRenderOutputNode(node) {
|
|
1043
|
+
if (!node) return false;
|
|
1044
|
+
switch (node.type) {
|
|
1045
|
+
case 'JSXElement':
|
|
1046
|
+
case 'JSXFragment':
|
|
1047
|
+
case 'JSXStyleElement':
|
|
1048
|
+
case 'JSXCodeBlock':
|
|
1049
|
+
case 'JSXIfExpression':
|
|
1050
|
+
case 'JSXForExpression':
|
|
1051
|
+
case 'JSXSwitchExpression':
|
|
1052
|
+
case 'JSXTryExpression':
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Inside a code block (`@{ … }` or a directive's `{ }`), decides whether the
|
|
1060
|
+
* next thing is the single bare render node (`<tag …>`, `<>…</>`, or an
|
|
1061
|
+
* `@if`/`@for`/`@switch`/`@try` directive) rather than a setup statement.
|
|
1062
|
+
*
|
|
1063
|
+
* Render output that begins with `<` is recognized by the tokenizer
|
|
1064
|
+
* (`getTokenFromCode`): it emits `jsxTagStart` for a `<` that opens a tag — at
|
|
1065
|
+
* the start of a line, or in an expression position such as after `;`/`{`/`=>` —
|
|
1066
|
+
* which the `jsxTagStart` fast path below covers. The char-based fallback for a
|
|
1067
|
+
* raw `<` therefore only treats it as render output when the tag starts its own
|
|
1068
|
+
* line or follows a `;` on the same line (so one-liners such as
|
|
1069
|
+
* `@{ const foo = 1; <>{foo}</> }` work). A `<` the tokenizer left as a
|
|
1070
|
+
* relational operator while trailing a value on the same line is the comparison
|
|
1071
|
+
* it looks like (`aaa <b` is `aaa < b`, never a `<b>` tag), so it stays setup
|
|
1072
|
+
* code rather than being mistaken for render output.
|
|
1073
|
+
*/
|
|
1074
|
+
#atRenderNodeStart() {
|
|
1075
|
+
if (this.type === tstt.jsxTagStart) return true;
|
|
1076
|
+
const index = skip_whitespace_from(this.input, this.start);
|
|
1077
|
+
const ch = this.input.charCodeAt(index);
|
|
1078
|
+
if (ch === CharCode.lessThan) {
|
|
1079
|
+
const next = this.input.charCodeAt(index + 1);
|
|
1080
|
+
if (next === CharCode.slash) return false;
|
|
1081
|
+
const tagLike =
|
|
1082
|
+
next === CharCode.greaterThan ||
|
|
1083
|
+
next === CharCode.at ||
|
|
1084
|
+
next === CharCode.dollar ||
|
|
1085
|
+
next === CharCode.underscore ||
|
|
1086
|
+
(next >= CharCode.uppercaseA && next <= CharCode.uppercaseZ) ||
|
|
1087
|
+
(next >= CharCode.lowercaseA && next <= CharCode.lowercaseZ);
|
|
1088
|
+
const previous = this.#previousNonSpaceTabIndex(index);
|
|
1089
|
+
const afterSemicolon =
|
|
1090
|
+
previous >= 0 && this.input.charCodeAt(previous) === CharCode.semicolon;
|
|
1091
|
+
return tagLike && (this.#isLineStartPosition(index) || afterSemicolon);
|
|
1092
|
+
}
|
|
1093
|
+
return this.#isCodeBlockStart(index) || this.#isJSXControlFlowDirectiveAt(index);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Parse one setup statement inside a code block as ordinary TS, with the
|
|
1098
|
+
* native-template path hidden so `<` reads as a relational/type operator
|
|
1099
|
+
* (`value < limit`, `foo<T>()`) rather than a JSX tag, and any JSX value
|
|
1100
|
+
* (`const x = <div/>`) parses as a plain JSX expression.
|
|
1101
|
+
*/
|
|
1102
|
+
#parseCodeBlockSetupStatement() {
|
|
1103
|
+
const previous_context = this.context;
|
|
1104
|
+
this.context = previous_context.filter(
|
|
1105
|
+
(context) =>
|
|
1106
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
1107
|
+
);
|
|
1108
|
+
let pushed_statement_context = false;
|
|
1109
|
+
if (this.curContext() !== b_stat) {
|
|
1110
|
+
this.context.push(b_stat);
|
|
1111
|
+
pushed_statement_context = true;
|
|
1112
|
+
}
|
|
1113
|
+
this.exprAllowed = true;
|
|
1114
|
+
const previous_path = this.#path;
|
|
1115
|
+
this.#path = [];
|
|
1116
|
+
this.#templateScriptParsingDepth++;
|
|
1117
|
+
let node;
|
|
1118
|
+
try {
|
|
1119
|
+
// A code-block/directive body is statements plus at most one render node —
|
|
1120
|
+
// never bare text or markup tokens. If the tokenizer mis-read trailing
|
|
1121
|
+
// code as JSX (raw text or a tag-name token — both can happen for a
|
|
1122
|
+
// statement following the render node, depending on the leftover context),
|
|
1123
|
+
// reposition to the token start and re-read it as code now that the
|
|
1124
|
+
// template path is hidden. It then parses as a statement so the
|
|
1125
|
+
// one-render-node rule reports a clear "statements cannot follow" error
|
|
1126
|
+
// instead of a generic parse fault.
|
|
1127
|
+
if (this.type === tstt.jsxText || this.type === tstt.jsxName) {
|
|
1128
|
+
// Rewinding `pos` to the mis-read token's start must also rewind the
|
|
1129
|
+
// line counter: a `jsxText` token can span newlines (e.g. the blank
|
|
1130
|
+
// line before a following render node), and reading it already
|
|
1131
|
+
// advanced `curLine`/`lineStart` to its end. Resetting only `pos`
|
|
1132
|
+
// would leave the line counter ahead of `pos`, inflating the `loc`
|
|
1133
|
+
// of this statement and every node after it (which crashes source-map
|
|
1134
|
+
// mapping when the inflated end line runs past the file).
|
|
1135
|
+
const loc = acorn.getLineInfo(this.input, this.start);
|
|
1136
|
+
this.pos = this.start;
|
|
1137
|
+
this.curLine = loc.line;
|
|
1138
|
+
this.lineStart = this.start - loc.column;
|
|
1139
|
+
this.nextToken();
|
|
1140
|
+
}
|
|
1141
|
+
node = this.parseStatement(null);
|
|
1142
|
+
} finally {
|
|
1143
|
+
this.#templateScriptParsingDepth--;
|
|
1144
|
+
this.#path = previous_path;
|
|
1145
|
+
if (pushed_statement_context && this.curContext() === b_stat) {
|
|
1146
|
+
this.context.pop();
|
|
1147
|
+
}
|
|
1148
|
+
this.context = previous_context;
|
|
1149
|
+
}
|
|
1150
|
+
if (this.curContext() === tstc.tc_expr) {
|
|
1151
|
+
this.context.pop();
|
|
1152
|
+
}
|
|
1153
|
+
return node;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Parse the single bare render node of a code block — a JSX element/fragment
|
|
1158
|
+
* (parsed as a native TSRX element so its own body may again be plain JSX or
|
|
1159
|
+
* a nested `@{ … }`) or an `@if`/`@for`/`@switch`/`@try` directive.
|
|
1160
|
+
*/
|
|
1161
|
+
#parseCodeBlockRenderNode() {
|
|
1162
|
+
const at_index = skip_whitespace_from(this.input, this.start);
|
|
1163
|
+
// Reposition onto the render token so it re-tokenizes in a clean context
|
|
1164
|
+
// (a preceding setup statement's context restore can strip the JSX tag
|
|
1165
|
+
// contexts the trailing `<`/`@` token first pushed).
|
|
1166
|
+
if (this.start !== at_index) {
|
|
1167
|
+
const loc = acorn.getLineInfo(this.input, at_index);
|
|
1168
|
+
this.pos = at_index;
|
|
1169
|
+
this.start = at_index;
|
|
1170
|
+
this.startLoc = new acorn.Position(loc.line, loc.column);
|
|
1171
|
+
this.curLine = loc.line;
|
|
1172
|
+
this.lineStart = at_index - loc.column;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (this.#isCodeBlockStart(at_index)) {
|
|
1176
|
+
return /** @type {AST.Node} */ (/** @type {unknown} */ (this.#parseCodeBlock()));
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (this.#isJSXControlFlowDirectiveAt(at_index)) {
|
|
1180
|
+
return /** @type {AST.Node} */ (
|
|
1181
|
+
/** @type {unknown} */ (this.#parseJSXControlFlowExpression())
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Re-read the `<` so its `jsxTagStart` pushes the opening-tag contexts.
|
|
1186
|
+
this.pos = at_index;
|
|
1187
|
+
this.exprAllowed = true;
|
|
1188
|
+
this.next();
|
|
1189
|
+
if (this.type !== tstt.jsxTagStart) {
|
|
1190
|
+
this.unexpected();
|
|
1191
|
+
}
|
|
1192
|
+
this.next();
|
|
1193
|
+
if (this.value === '/' || this.type === tt.slash) {
|
|
1194
|
+
this.unexpected();
|
|
1195
|
+
}
|
|
1196
|
+
const node = this.parseElement();
|
|
1197
|
+
if (!node) {
|
|
1198
|
+
this.unexpected();
|
|
1199
|
+
}
|
|
1200
|
+
if (this.curContext() === tstc.tc_expr) {
|
|
1201
|
+
this.context.pop();
|
|
1202
|
+
}
|
|
1203
|
+
return /** @type {AST.Node} */ (/** @type {unknown} */ (node));
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Shared `Statement* RenderOutput?` grammar for the body of a `@{ … }` code
|
|
1208
|
+
* block and the `{ }` of an `@if`/`@for`/`@switch`/`@try` directive (§2
|
|
1209
|
+
* rules 4–8). Fills `flat` with the setup statements followed by at most one
|
|
1210
|
+
* trailing render node. Leaves the tokenizer positioned at the closing `}`.
|
|
1211
|
+
* @param {AST.Node[]} flat
|
|
1212
|
+
*/
|
|
1213
|
+
#parseCodeBlockBody(flat) {
|
|
1214
|
+
let render_seen = false;
|
|
1215
|
+
while (this.type !== tt.braceR && this.type !== tt.eof) {
|
|
1216
|
+
// A bare `;` is an empty statement carrying no meaning. JSX render
|
|
1217
|
+
// output does not consume a trailing `;`, so one written after the
|
|
1218
|
+
// render node (`<>…</>;`) would otherwise parse as a statement and
|
|
1219
|
+
// trip the "statements cannot follow the rendered output" rule. Skip
|
|
1220
|
+
// stray semicolons silently here; prettier strips them on format.
|
|
1221
|
+
if (this.type === tt.semi) {
|
|
1222
|
+
this.next();
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
if (this.#atRenderNodeStart()) {
|
|
1226
|
+
const render_node = this.#parseCodeBlockRenderNode();
|
|
1227
|
+
if (render_seen) {
|
|
1228
|
+
this.#report_recoverable_error_range(
|
|
1229
|
+
/** @type {number} */ (render_node.start),
|
|
1230
|
+
/** @type {number} */ (render_node.end),
|
|
1231
|
+
"A code block renders a single node; wrap multiple nodes or text in a fragment '<>…</>'.",
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
flat.push(render_node);
|
|
1235
|
+
render_seen = true;
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
const statement = this.#parseCodeBlockSetupStatement();
|
|
1239
|
+
if (statement) {
|
|
1240
|
+
if (render_seen) {
|
|
1241
|
+
// A statement after the rendered output: code must come first.
|
|
1242
|
+
this.#report_recoverable_error_range(
|
|
1243
|
+
/** @type {number} */ (statement.start),
|
|
1244
|
+
/** @type {number} */ (statement.end),
|
|
1245
|
+
"Code must be at the top of '@{ }'; statements cannot follow the rendered output.",
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
flat.push(statement);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Parse an explicit `@{ … }` code block (`this.start` at `@`). Returns a
|
|
1255
|
+
* `JSXCodeBlock` whose `body` holds the setup statements and `render` the
|
|
1256
|
+
* single optional render output (§9).
|
|
1257
|
+
*/
|
|
1258
|
+
#parseCodeBlock() {
|
|
1259
|
+
const start = this.start;
|
|
1260
|
+
const startLoc = this.startLoc;
|
|
1261
|
+
const node = /** @type {AST.JSXCodeBlock} */ (this.startNodeAt(start, startLoc));
|
|
1262
|
+
node.body = [];
|
|
1263
|
+
node.render = null;
|
|
1264
|
+
node.metadata = { path: [] };
|
|
1265
|
+
|
|
1266
|
+
// The body parses as JS, so swap the surrounding JSX/template token
|
|
1267
|
+
// contexts for a clean statement context and hide the enclosing template
|
|
1268
|
+
// from `#path` so the body tokenizes as code (not JSX raw text). Both are
|
|
1269
|
+
// restored before the closing `}` is consumed so the following `</tag>`
|
|
1270
|
+
// tokenizes against the same template context the body opened in.
|
|
1271
|
+
const enclosing_context = this.context;
|
|
1272
|
+
const enclosing_path = this.#path;
|
|
1273
|
+
const braceStart = start + 1;
|
|
1274
|
+
this.context = enclosing_context.filter(
|
|
1275
|
+
(context) =>
|
|
1276
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
1277
|
+
);
|
|
1278
|
+
if (this.curContext() !== b_stat) {
|
|
1279
|
+
this.context.push(b_stat);
|
|
1280
|
+
}
|
|
1281
|
+
const braceLoc = acorn.getLineInfo(this.input, braceStart);
|
|
1282
|
+
this.pos = braceStart;
|
|
1283
|
+
this.start = braceStart;
|
|
1284
|
+
this.startLoc = new acorn.Position(braceLoc.line, braceLoc.column);
|
|
1285
|
+
this.curLine = braceLoc.line;
|
|
1286
|
+
this.lineStart = braceStart - braceLoc.column;
|
|
1287
|
+
this.exprAllowed = true;
|
|
1288
|
+
this.#path = [];
|
|
1289
|
+
this.next();
|
|
1290
|
+
this.expect(tt.braceL);
|
|
1291
|
+
|
|
1292
|
+
/** @type {AST.Node[]} */
|
|
1293
|
+
const flat = [];
|
|
1294
|
+
this.enterScope(0);
|
|
1295
|
+
try {
|
|
1296
|
+
this.#parseCodeBlockBody(flat);
|
|
1297
|
+
} finally {
|
|
1298
|
+
this.exitScope();
|
|
1299
|
+
this.#path = enclosing_path;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const last = flat[flat.length - 1];
|
|
1303
|
+
if (this.#isRenderOutputNode(last)) {
|
|
1304
|
+
node.render = last;
|
|
1305
|
+
node.body = /** @type {AST.Statement[]} */ (flat.slice(0, -1));
|
|
1306
|
+
} else {
|
|
1307
|
+
node.body = /** @type {AST.Statement[]} */ (flat);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (this.type !== tt.braceR) {
|
|
1311
|
+
this.unexpected();
|
|
1312
|
+
}
|
|
1313
|
+
// Restore the enclosing template context, then consume `}` and read the
|
|
1314
|
+
// following token (typically the parent's `</tag>`) against it. Finish the
|
|
1315
|
+
// node after the `}` so its range spans the whole `@{ … }` (this is what
|
|
1316
|
+
// lets trailing comments before `}` attach to the block, not the parent's
|
|
1317
|
+
// closing tag).
|
|
1318
|
+
const brace_close_end = this.end;
|
|
1319
|
+
const brace_close_end_loc = this.endLoc;
|
|
1320
|
+
this.context = enclosing_context;
|
|
1321
|
+
this.next();
|
|
1322
|
+
this.finishNodeAt(node, 'JSXCodeBlock', brace_close_end, brace_close_end_loc);
|
|
1323
|
+
return node;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* At-sign constructs are expressions (§6a, §2 rule 9): code blocks and the
|
|
1328
|
+
* if/for/switch/try directive forms may be returned, assigned, or passed
|
|
1329
|
+
* anywhere an expression is expected. Only code blocks and the four reserved
|
|
1330
|
+
* control-flow keywords are intercepted; any other at-sign form, such as a
|
|
1331
|
+
* decorated class expression, falls through so decorators keep working.
|
|
1332
|
+
* @type {Parse.Parser['parseExprAtom']}
|
|
1333
|
+
*/
|
|
1334
|
+
parseExprAtom(refDestructuringErrors, forInit, forNew) {
|
|
1335
|
+
if (this.input.charCodeAt(this.start) === CharCode.at) {
|
|
1336
|
+
if (this.#isCodeBlockStart(this.start)) {
|
|
1337
|
+
return /** @type {any} */ (this.#parseCodeBlock());
|
|
1338
|
+
}
|
|
1339
|
+
if (this.#isJSXControlFlowDirectiveAt(this.start)) {
|
|
1340
|
+
return /** @type {any} */ (this.#parseJSXControlFlowExpression());
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return super.parseExprAtom(refDestructuringErrors, forInit, forNew);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* @param {AST.Node} node
|
|
1348
|
+
* @param {string} type
|
|
1349
|
+
* @param {number} start
|
|
1350
|
+
* @param {AST.Position} startLoc
|
|
1351
|
+
*/
|
|
1352
|
+
#finishJSXControlFlowExpression(node, type, start, startLoc) {
|
|
1353
|
+
node.start = start;
|
|
1354
|
+
/** @type {AST.NodeWithLocation} */ (node).loc.start = startLoc;
|
|
1355
|
+
node.metadata ??= { path: [] };
|
|
1356
|
+
/** @type {any} */ (node).statementType = node.type;
|
|
1357
|
+
/** @type {any} */ (node).type = type;
|
|
1358
|
+
return node;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
#parseJSXControlFlowExpression() {
|
|
1362
|
+
const start = this.start;
|
|
1363
|
+
const startLoc = this.startLoc;
|
|
1364
|
+
const keywordStart = start + 1;
|
|
1365
|
+
this.pos = keywordStart;
|
|
1366
|
+
this.start = keywordStart;
|
|
1367
|
+
this.startLoc = acorn.getLineInfo(this.input, keywordStart);
|
|
1368
|
+
this.#readingJSXControlFlowDirectiveKeyword = true;
|
|
1369
|
+
try {
|
|
1370
|
+
this.nextToken();
|
|
1371
|
+
} finally {
|
|
1372
|
+
this.#readingJSXControlFlowDirectiveKeyword = false;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const label = this.type.keyword || this.type.label || this.value;
|
|
1376
|
+
if (label === 'if') {
|
|
1377
|
+
return this.#finishJSXControlFlowExpression(
|
|
1378
|
+
this.#parseTemplateIfStatement(),
|
|
1379
|
+
'JSXIfExpression',
|
|
1380
|
+
start,
|
|
1381
|
+
startLoc,
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (label === 'for') {
|
|
1386
|
+
this.#templateControlFlowBlockDepth++;
|
|
1387
|
+
let node;
|
|
1388
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
1389
|
+
this.#readingJSXControlFlowHeader = true;
|
|
1390
|
+
try {
|
|
1391
|
+
node = this.#finishJSXControlFlowExpression(
|
|
1392
|
+
this.parseStatement(null),
|
|
1393
|
+
'JSXForExpression',
|
|
1394
|
+
start,
|
|
1395
|
+
startLoc,
|
|
1396
|
+
);
|
|
1397
|
+
} finally {
|
|
1398
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
1399
|
+
this.#templateControlFlowBlockDepth--;
|
|
1400
|
+
}
|
|
1401
|
+
if (
|
|
1402
|
+
/** @type {any} */ (node).statementType !== 'ForOfStatement' &&
|
|
1403
|
+
/** @type {any} */ (node).statementType !== 'ForInStatement' &&
|
|
1404
|
+
/** @type {any} */ (node).statementType !== 'ForStatement'
|
|
1405
|
+
) {
|
|
1406
|
+
this.raise(start, 'Expected `for` after `@`.');
|
|
1407
|
+
}
|
|
1408
|
+
if (/** @type {any} */ (node).body?.type !== 'BlockStatement') {
|
|
1409
|
+
this.raise(
|
|
1410
|
+
/** @type {any} */ (node).body?.start ?? start,
|
|
1411
|
+
'Expected `{` after JSX control-flow directive.',
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
if (this.#eatJSXForEmptyKeyword()) {
|
|
1415
|
+
if (this.type !== tt.braceL) {
|
|
1416
|
+
this.raise(this.start, 'Expected `{` after JSX control-flow directive.');
|
|
1417
|
+
}
|
|
1418
|
+
this.#templateControlFlowBlockDepth++;
|
|
1419
|
+
try {
|
|
1420
|
+
/** @type {any} */ (node).empty = this.parseBlock();
|
|
1421
|
+
} finally {
|
|
1422
|
+
this.#templateControlFlowBlockDepth--;
|
|
1423
|
+
}
|
|
1424
|
+
} else if (this.#isUnprefixedDirectiveClauseKeyword('empty')) {
|
|
1425
|
+
this.raise(this.start, 'Expected `@empty` after `@for` block.');
|
|
1426
|
+
} else {
|
|
1427
|
+
/** @type {any} */ (node).empty = null;
|
|
1428
|
+
}
|
|
1429
|
+
return node;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (label === 'switch') {
|
|
1433
|
+
return this.#parseJSXSwitchExpression(start, startLoc);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
if (label === 'try') {
|
|
1437
|
+
this.#templateControlFlowTryDepth++;
|
|
1438
|
+
try {
|
|
1439
|
+
return this.#finishJSXControlFlowExpression(
|
|
1440
|
+
this.parseStatement(null),
|
|
1441
|
+
'JSXTryExpression',
|
|
1442
|
+
start,
|
|
1443
|
+
startLoc,
|
|
1444
|
+
);
|
|
1445
|
+
} finally {
|
|
1446
|
+
this.#templateControlFlowTryDepth--;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
this.raise(start, 'Expected `@if`, `@for`, `@switch`, or `@try`.');
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* @param {string} keyword
|
|
1455
|
+
*/
|
|
1456
|
+
#eatJSXDirectiveClauseKeyword(keyword) {
|
|
1457
|
+
const keywordStart = skip_whitespace_from(this.input, this.start);
|
|
1458
|
+
if (this.input.charCodeAt(keywordStart) !== CharCode.at) {
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
const wordStart = keywordStart + 1;
|
|
1462
|
+
if (
|
|
1463
|
+
this.input.slice(wordStart, wordStart + keyword.length) !== keyword ||
|
|
1464
|
+
this.#isIdentifierChar(this.input.charCodeAt(wordStart + keyword.length))
|
|
1465
|
+
) {
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
this.pos = wordStart;
|
|
1470
|
+
this.start = wordStart;
|
|
1471
|
+
this.startLoc = acorn.getLineInfo(this.input, wordStart);
|
|
1472
|
+
this.curLine = this.startLoc.line;
|
|
1473
|
+
this.lineStart = wordStart - this.startLoc.column;
|
|
1474
|
+
this.context = this.context.filter(
|
|
1475
|
+
(context) =>
|
|
1476
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
1477
|
+
);
|
|
1478
|
+
if (this.curContext() !== b_stat) {
|
|
1479
|
+
this.context.push(b_stat);
|
|
1480
|
+
}
|
|
1481
|
+
this.exprAllowed = true;
|
|
1482
|
+
this.#readingJSXControlFlowDirectiveKeyword = true;
|
|
1483
|
+
try {
|
|
1484
|
+
this.nextToken();
|
|
1485
|
+
} finally {
|
|
1486
|
+
this.#readingJSXControlFlowDirectiveKeyword = false;
|
|
1487
|
+
}
|
|
1488
|
+
this.next();
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
#eatJSXForEmptyKeyword() {
|
|
1493
|
+
return this.#eatJSXDirectiveClauseKeyword('empty');
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* @param {string} keyword
|
|
1498
|
+
*/
|
|
1499
|
+
#eatJSXDirectiveBareClauseKeyword(keyword) {
|
|
1500
|
+
const wordStart = skip_whitespace_from(this.input, this.start);
|
|
1501
|
+
if (
|
|
1502
|
+
this.input.slice(wordStart, wordStart + keyword.length) !== keyword ||
|
|
1503
|
+
this.#isIdentifierChar(this.input.charCodeAt(wordStart + keyword.length))
|
|
1504
|
+
) {
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
this.pos = wordStart;
|
|
1509
|
+
this.start = wordStart;
|
|
1510
|
+
this.startLoc = acorn.getLineInfo(this.input, wordStart);
|
|
1511
|
+
this.curLine = this.startLoc.line;
|
|
1512
|
+
this.lineStart = wordStart - this.startLoc.column;
|
|
1513
|
+
this.context = this.context.filter(
|
|
1514
|
+
(context) =>
|
|
1515
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
1516
|
+
);
|
|
1517
|
+
if (this.curContext() !== b_stat) {
|
|
1518
|
+
this.context.push(b_stat);
|
|
1519
|
+
}
|
|
1520
|
+
this.exprAllowed = true;
|
|
1521
|
+
this.#readingJSXControlFlowDirectiveKeyword = true;
|
|
1522
|
+
try {
|
|
1523
|
+
this.nextToken();
|
|
1524
|
+
} finally {
|
|
1525
|
+
this.#readingJSXControlFlowDirectiveKeyword = false;
|
|
1526
|
+
}
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* @param {string} keyword
|
|
1532
|
+
*/
|
|
1533
|
+
#isUnprefixedDirectiveClauseKeyword(keyword) {
|
|
1534
|
+
const keywordStart = skip_whitespace_from(this.input, this.start);
|
|
1535
|
+
return (
|
|
1536
|
+
this.input.slice(keywordStart, keywordStart + keyword.length) === keyword &&
|
|
1537
|
+
!this.#isIdentifierChar(this.input.charCodeAt(keywordStart + keyword.length))
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* @returns {'case' | 'default' | null}
|
|
1543
|
+
*/
|
|
1544
|
+
#eatJSXSwitchCaseClauseKeyword() {
|
|
1545
|
+
if (this.#eatJSXDirectiveClauseKeyword('case')) {
|
|
1546
|
+
return 'case';
|
|
1547
|
+
}
|
|
1548
|
+
if (this.#eatJSXDirectiveClauseKeyword('default')) {
|
|
1549
|
+
return 'default';
|
|
1550
|
+
}
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
#parseTemplateControlFlowStatement() {
|
|
1555
|
+
if (this.type !== tt.braceL) {
|
|
1556
|
+
this.raise(this.start, 'Expected `{` after JSX control-flow directive.');
|
|
1557
|
+
}
|
|
1558
|
+
return this.#parseTemplateControlFlowBlock();
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
#parseTemplateIfStatement() {
|
|
1562
|
+
const node = /** @type {AST.IfStatement} */ (this.startNode());
|
|
1563
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
1564
|
+
this.#readingJSXControlFlowHeader = true;
|
|
1565
|
+
try {
|
|
1566
|
+
this.next();
|
|
1567
|
+
node.test = this.parseParenExpression();
|
|
1568
|
+
} finally {
|
|
1569
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
1570
|
+
}
|
|
1571
|
+
node.consequent = /** @type {AST.Statement} */ (this.#parseTemplateControlFlowStatement());
|
|
1572
|
+
node.alternate = null;
|
|
1573
|
+
|
|
1574
|
+
if (this.#eatJSXDirectiveClauseKeyword('else')) {
|
|
1575
|
+
node.alternate = this.#eatJSXDirectiveBareClauseKeyword('if')
|
|
1576
|
+
? this.#parseTemplateIfStatement()
|
|
1577
|
+
: /** @type {AST.Statement} */ (this.#parseTemplateControlFlowStatement());
|
|
1578
|
+
} else if (this.#isUnprefixedDirectiveClauseKeyword('else')) {
|
|
1579
|
+
this.raise(this.start, 'Expected `@else` after `@if` block.');
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
return this.finishNode(node, 'IfStatement');
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* @param {number} start
|
|
1587
|
+
* @param {AST.Position} startLoc
|
|
1588
|
+
*/
|
|
1589
|
+
#parseJSXSwitchExpression(start, startLoc) {
|
|
1590
|
+
const node = /** @type {AST.SwitchStatement} */ (this.startNodeAt(start, startLoc));
|
|
1591
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
1592
|
+
this.#readingJSXControlFlowHeader = true;
|
|
1593
|
+
try {
|
|
1594
|
+
this.next();
|
|
1595
|
+
node.discriminant = this.parseParenExpression();
|
|
1596
|
+
} finally {
|
|
1597
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
1598
|
+
}
|
|
1599
|
+
node.cases = [];
|
|
1600
|
+
this.expect(tt.braceL);
|
|
1601
|
+
this.labels.push({ kind: 'switch' });
|
|
1602
|
+
this.enterScope(0);
|
|
1603
|
+
|
|
1604
|
+
let sawDefault = false;
|
|
1605
|
+
while (this.type !== tt.braceR) {
|
|
1606
|
+
if (this.type === tstt.jsxText && this.#rewindToSwitchCaseLabel()) {
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const clauseStart = this.start;
|
|
1611
|
+
const clauseStartLoc = this.startLoc;
|
|
1612
|
+
const clause = this.#eatJSXSwitchCaseClauseKeyword();
|
|
1613
|
+
if (clause) {
|
|
1614
|
+
const isCase = clause === 'case';
|
|
1615
|
+
const current = /** @type {AST.SwitchCase} */ (
|
|
1616
|
+
this.startNodeAt(clauseStart, clauseStartLoc)
|
|
1617
|
+
);
|
|
1618
|
+
current.consequent = [];
|
|
1619
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
1620
|
+
this.#readingJSXControlFlowHeader = true;
|
|
1621
|
+
try {
|
|
1622
|
+
if (isCase) {
|
|
1623
|
+
current.test = this.parseExpression();
|
|
1624
|
+
} else {
|
|
1625
|
+
if (sawDefault) {
|
|
1626
|
+
this.raiseRecoverable(this.lastTokStart, 'Multiple default clauses');
|
|
1627
|
+
}
|
|
1628
|
+
sawDefault = true;
|
|
1629
|
+
current.test = null;
|
|
1630
|
+
}
|
|
1631
|
+
this.expect(tt.colon);
|
|
1632
|
+
} finally {
|
|
1633
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
1634
|
+
}
|
|
1635
|
+
this.expect(tt.braceL);
|
|
1636
|
+
while (this.type !== tt.braceR) {
|
|
1637
|
+
this.#parseJSXSwitchCaseConsequent(current.consequent);
|
|
1638
|
+
}
|
|
1639
|
+
this.expect(tt.braceR);
|
|
1640
|
+
node.cases.push(this.finishNode(current, 'SwitchCase'));
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
this.unexpected();
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
this.exitScope();
|
|
1648
|
+
this.next();
|
|
1649
|
+
this.labels.pop();
|
|
1650
|
+
return this.#finishJSXControlFlowExpression(
|
|
1651
|
+
this.finishNode(node, 'SwitchStatement'),
|
|
1652
|
+
'JSXSwitchExpression',
|
|
1653
|
+
start,
|
|
1654
|
+
startLoc,
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* @param {AST.Node[]} consequent
|
|
1660
|
+
* @this {TSRXParser & Parse.Parser}
|
|
1661
|
+
*/
|
|
1662
|
+
#parseJSXSwitchCaseConsequent(consequent) {
|
|
1663
|
+
if (this.type === tt.braceL) {
|
|
1664
|
+
consequent.push(this.#parseNativeTemplateExpressionContainer());
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// A non-whitespace, non-directive case consequent that the tokenizer read
|
|
1669
|
+
// as raw text is a setup statement (in the new design bare text must be
|
|
1670
|
+
// wrapped in `<>`, so anything left here is code, e.g.
|
|
1671
|
+
// `props.status satisfies never`, `doThing()`, `x = 1`). Re-tokenize it as
|
|
1672
|
+
// JS and parse it as a statement instead of treating it as text.
|
|
1673
|
+
if (
|
|
1674
|
+
this.type === tstt.jsxText &&
|
|
1675
|
+
String(this.value ?? '').trim() !== '' &&
|
|
1676
|
+
!this.#isJSXControlFlowDirectiveStart() &&
|
|
1677
|
+
this.#switchCaseLabelStart(this.start) === -1
|
|
1678
|
+
) {
|
|
1679
|
+
const raw = String(this.value ?? '').trimStart();
|
|
1680
|
+
if (/^break\b/.test(raw)) {
|
|
1681
|
+
this.raise(this.start, '`break` is invalid inside `@switch` cases.');
|
|
1682
|
+
}
|
|
1683
|
+
if (/^return\b/.test(raw)) {
|
|
1684
|
+
this.raise(this.start, '`return` is invalid inside `@switch` cases.');
|
|
1685
|
+
}
|
|
1686
|
+
this.context = this.context.filter(
|
|
1687
|
+
(context) =>
|
|
1688
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
1689
|
+
);
|
|
1690
|
+
this.pos = this.start;
|
|
1691
|
+
this.startLoc = this.curPosition();
|
|
1692
|
+
if (this.curContext() !== b_stat) {
|
|
1693
|
+
this.context.push(b_stat);
|
|
1694
|
+
}
|
|
1695
|
+
this.exprAllowed = true;
|
|
1696
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth++;
|
|
1697
|
+
try {
|
|
1698
|
+
this.#suppressTemplateRawTextToken = true;
|
|
1699
|
+
this.next();
|
|
1700
|
+
consequent.push(this.parseStatement(null));
|
|
1701
|
+
} finally {
|
|
1702
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth--;
|
|
1703
|
+
}
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (this.type === tstt.jsxText) {
|
|
1708
|
+
const text = this.#parseJSXSwitchCaseRawText();
|
|
1709
|
+
if (!isWhitespaceTextNode(text)) {
|
|
1710
|
+
consequent.push(/** @type {any} */ (text));
|
|
1711
|
+
}
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
if (
|
|
1716
|
+
this.type === tstt.jsxTagStart ||
|
|
1717
|
+
this.input.charCodeAt(this.start) === CharCode.lessThan
|
|
1718
|
+
) {
|
|
1719
|
+
const startPos = this.start;
|
|
1720
|
+
const startLoc = this.startLoc;
|
|
1721
|
+
if (this.type === tstt.jsxTagStart) {
|
|
1722
|
+
this.next();
|
|
1723
|
+
} else {
|
|
1724
|
+
this.pos = startPos + 1;
|
|
1725
|
+
this.type = tstt.jsxTagStart;
|
|
1726
|
+
this.start = startPos;
|
|
1727
|
+
this.startLoc = startLoc;
|
|
1728
|
+
this.exprAllowed = false;
|
|
1729
|
+
this.next();
|
|
1730
|
+
}
|
|
1731
|
+
if (this.value === '/' || this.type === tt.slash) {
|
|
1732
|
+
this.unexpected();
|
|
1733
|
+
}
|
|
1734
|
+
const node = this.parseElement();
|
|
1735
|
+
if (!node) {
|
|
1736
|
+
this.unexpected();
|
|
1737
|
+
}
|
|
1738
|
+
consequent.push(/** @type {any} */ (node));
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (this.#isJSXControlFlowDirectiveStart()) {
|
|
1743
|
+
consequent.push(/** @type {any} */ (this.#parseJSXControlFlowExpression()));
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
if (this.#isSwitchCaseScriptStatementStart()) {
|
|
1748
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth++;
|
|
1749
|
+
try {
|
|
1750
|
+
consequent.push(this.parseStatement(null));
|
|
1751
|
+
} finally {
|
|
1752
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth--;
|
|
1753
|
+
}
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const label = this.type.keyword || this.type.label;
|
|
1758
|
+
if (label === 'break') {
|
|
1759
|
+
this.raise(this.start, '`break` is invalid inside `@switch` cases.');
|
|
1760
|
+
}
|
|
1761
|
+
if (label === 'return') {
|
|
1762
|
+
this.raise(this.start, '`return` is invalid inside `@switch` cases.');
|
|
1763
|
+
}
|
|
1764
|
+
if (label === 'continue' || label === 'throw') {
|
|
1765
|
+
consequent.push(this.parseStatement(null));
|
|
1766
|
+
return;
|
|
481
1767
|
}
|
|
482
1768
|
|
|
483
|
-
|
|
484
|
-
|
|
1769
|
+
// Anything else here is JS read as ordinary tokens (e.g.
|
|
1770
|
+
// `props.status satisfies never`, `doThing()`): a setup statement, not text
|
|
1771
|
+
// (bare text in a case must be wrapped in `<>`). Clear the JSX/template
|
|
1772
|
+
// token contexts so the statement and the following `}`/`case` tokenize as
|
|
1773
|
+
// code.
|
|
1774
|
+
if (this.type !== tstt.jsxText && this.type !== tt.eof) {
|
|
1775
|
+
this.context = this.context.filter(
|
|
1776
|
+
(context) =>
|
|
1777
|
+
context !== tstc.tc_expr && context !== tstc.tc_oTag && context !== tstc.tc_cTag,
|
|
1778
|
+
);
|
|
1779
|
+
if (this.curContext() !== b_stat) {
|
|
1780
|
+
this.context.push(b_stat);
|
|
1781
|
+
}
|
|
1782
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth++;
|
|
1783
|
+
try {
|
|
1784
|
+
consequent.push(this.parseStatement(null));
|
|
1785
|
+
} finally {
|
|
1786
|
+
this.#parsingJSXSwitchCaseScriptStatementDepth--;
|
|
1787
|
+
}
|
|
1788
|
+
return;
|
|
485
1789
|
}
|
|
486
1790
|
|
|
487
|
-
|
|
1791
|
+
const text = this.#parseJSXSwitchCaseRawText();
|
|
1792
|
+
if (!isWhitespaceTextNode(text)) {
|
|
1793
|
+
consequent.push(text);
|
|
1794
|
+
}
|
|
488
1795
|
}
|
|
489
1796
|
|
|
490
1797
|
/**
|
|
491
|
-
* @param {
|
|
1798
|
+
* @param {ESTreeJSX.JSXOpeningElement} openingElement
|
|
1799
|
+
* @returns {ESTreeJSX.JSXOpeningFragment}
|
|
492
1800
|
*/
|
|
493
|
-
#
|
|
494
|
-
|
|
1801
|
+
#toOpeningFragment(openingElement) {
|
|
1802
|
+
const openingFragment = /** @type {ESTreeJSX.JSXOpeningFragment} */ (
|
|
1803
|
+
/** @type {unknown} */ (openingElement)
|
|
1804
|
+
);
|
|
1805
|
+
openingFragment.type = 'JSXOpeningFragment';
|
|
1806
|
+
delete (/** @type {any} */ (openingFragment).name);
|
|
1807
|
+
delete (/** @type {any} */ (openingFragment).attributes);
|
|
1808
|
+
delete (/** @type {any} */ (openingFragment).selfClosing);
|
|
1809
|
+
return openingFragment;
|
|
495
1810
|
}
|
|
496
1811
|
|
|
497
1812
|
/**
|
|
1813
|
+
* @param {ESTreeJSX.JSXClosingElement} closingElement
|
|
1814
|
+
* @returns {ESTreeJSX.JSXClosingFragment}
|
|
498
1815
|
*/
|
|
499
|
-
#
|
|
500
|
-
|
|
1816
|
+
#toClosingFragment(closingElement) {
|
|
1817
|
+
const closingFragment = /** @type {ESTreeJSX.JSXClosingFragment} */ (
|
|
1818
|
+
/** @type {unknown} */ (closingElement)
|
|
1819
|
+
);
|
|
1820
|
+
closingFragment.type = 'JSXClosingFragment';
|
|
1821
|
+
delete (/** @type {any} */ (closingFragment).name);
|
|
1822
|
+
return closingFragment;
|
|
501
1823
|
}
|
|
502
1824
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1825
|
+
/**
|
|
1826
|
+
* @param {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} open
|
|
1827
|
+
* @param {AST.JSXStyleElement} node
|
|
1828
|
+
* @param {boolean} insideHead
|
|
1829
|
+
*/
|
|
1830
|
+
#parseStyleElement(open, node, insideHead) {
|
|
1831
|
+
const contentStart = open.end;
|
|
1832
|
+
const input = this.input.slice(contentStart);
|
|
1833
|
+
const relativeCloseStart = input.indexOf('</style>');
|
|
1834
|
+
const content = relativeCloseStart === -1 ? input : input.slice(0, relativeCloseStart);
|
|
1835
|
+
const parsedCss = parse_style(content, { loose: this.#loose });
|
|
1836
|
+
|
|
1837
|
+
if (!insideHead) {
|
|
1838
|
+
node.metadata.styleScopeHash = parsedCss.hash;
|
|
1839
|
+
}
|
|
507
1840
|
|
|
508
|
-
|
|
509
|
-
|
|
1841
|
+
const newLines = content.match(regex_newline_characters)?.length;
|
|
1842
|
+
if (newLines) {
|
|
1843
|
+
this.curLine = open.loc.end.line + newLines;
|
|
1844
|
+
this.lineStart = contentStart + content.lastIndexOf('\n') + 1;
|
|
1845
|
+
}
|
|
510
1846
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
1847
|
+
if (relativeCloseStart !== -1) {
|
|
1848
|
+
const closingStart = contentStart + content.length;
|
|
1849
|
+
const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
|
|
1850
|
+
const closingStartLoc = new acorn.Position(closingLineInfo.line, closingLineInfo.column);
|
|
1851
|
+
const nameStart = closingStart + 2;
|
|
1852
|
+
const nameEnd = nameStart + 'style'.length;
|
|
1853
|
+
const nameStartInfo = acorn.getLineInfo(this.input, nameStart);
|
|
1854
|
+
const nameEndInfo = acorn.getLineInfo(this.input, nameEnd);
|
|
1855
|
+
const name = /** @type {ESTreeJSX.JSXIdentifier} */ (
|
|
1856
|
+
this.startNodeAt(
|
|
1857
|
+
nameStart,
|
|
1858
|
+
new acorn.Position(nameStartInfo.line, nameStartInfo.column),
|
|
1859
|
+
)
|
|
1860
|
+
);
|
|
1861
|
+
name.name = 'style';
|
|
1862
|
+
name.tracked = false;
|
|
1863
|
+
this.finishNodeAt(
|
|
1864
|
+
name,
|
|
1865
|
+
'JSXIdentifier',
|
|
1866
|
+
nameEnd,
|
|
1867
|
+
new acorn.Position(nameEndInfo.line, nameEndInfo.column),
|
|
1868
|
+
);
|
|
1869
|
+
const closingEnd = closingStart + '</style>'.length;
|
|
1870
|
+
const closingEndInfo = acorn.getLineInfo(this.input, closingEnd);
|
|
1871
|
+
const closingElement = /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
|
|
1872
|
+
this.startNodeAt(closingStart, closingStartLoc)
|
|
1873
|
+
);
|
|
1874
|
+
closingElement.name = name;
|
|
1875
|
+
this.finishNodeAt(
|
|
1876
|
+
closingElement,
|
|
1877
|
+
'JSXClosingElement',
|
|
1878
|
+
closingEnd,
|
|
1879
|
+
new acorn.Position(closingEndInfo.line, closingEndInfo.column),
|
|
1880
|
+
);
|
|
1881
|
+
node.closingElement = closingElement;
|
|
1882
|
+
const parent = this.#path.at(-2);
|
|
1883
|
+
const insideTemplate = this.#isNativeTemplateNode(parent);
|
|
1884
|
+
if (this.curContext() === tstc.tc_expr && !insideTemplate) {
|
|
1885
|
+
this.context.pop();
|
|
514
1886
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
this.
|
|
1887
|
+
this.exprAllowed = false;
|
|
1888
|
+
this.pos = closingEnd;
|
|
1889
|
+
this.curLine = closingEndInfo.line;
|
|
1890
|
+
this.lineStart = closingEnd - closingEndInfo.column;
|
|
1891
|
+
if (insideTemplate && relativeCloseStart === 0) {
|
|
1892
|
+
// Acorn has already tokenized the adjacent </style>; TSRX synthesizes
|
|
1893
|
+
// that close manually, so drop the stale style tag context.
|
|
1894
|
+
if (this.curContext() === tstc.tc_oTag) {
|
|
1895
|
+
this.context.pop();
|
|
1896
|
+
}
|
|
1897
|
+
if (this.curContext() === tstc.tc_expr) {
|
|
1898
|
+
this.context.pop();
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (!insideTemplate && this.#path.at(-1) === node) {
|
|
1902
|
+
this.#path.pop();
|
|
1903
|
+
try {
|
|
1904
|
+
this.next();
|
|
1905
|
+
} finally {
|
|
1906
|
+
this.#path.push(node);
|
|
1907
|
+
}
|
|
1908
|
+
} else {
|
|
1909
|
+
this.next();
|
|
1910
|
+
}
|
|
1911
|
+
} else {
|
|
1912
|
+
this.#report_broken_markup_error(
|
|
1913
|
+
open.end,
|
|
1914
|
+
"Unclosed tag '<style>'. Expected '</style>' before end of template.",
|
|
1915
|
+
);
|
|
1916
|
+
node.unclosed = true;
|
|
518
1917
|
}
|
|
519
1918
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
1919
|
+
node.css = content;
|
|
1920
|
+
node.children = [parsedCss];
|
|
1921
|
+
}
|
|
523
1922
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1923
|
+
#parseNativeTemplateExpressionContainer() {
|
|
1924
|
+
const allow_trailing_semicolon = this.#allowExpressionContainerTrailingSemicolon;
|
|
1925
|
+
this.#allowExpressionContainerTrailingSemicolon = true;
|
|
1926
|
+
// One-shot: marks this as a template *child* container (not an attribute
|
|
1927
|
+
// value or script-mode JSX child), so `jsx_parseExpressionContainer`
|
|
1928
|
+
// consumes the closing `}` after leaving container scope.
|
|
1929
|
+
this.#consumeContainerBraceAfterScope = true;
|
|
1930
|
+
let node;
|
|
1931
|
+
try {
|
|
1932
|
+
node = this.jsx_parseExpressionContainer();
|
|
1933
|
+
} finally {
|
|
1934
|
+
this.#allowExpressionContainerTrailingSemicolon = allow_trailing_semicolon;
|
|
1935
|
+
this.#consumeContainerBraceAfterScope = false;
|
|
1936
|
+
}
|
|
1937
|
+
return /** @type {ESTreeJSX.JSXExpressionContainer} */ (/** @type {unknown} */ (node));
|
|
531
1938
|
}
|
|
532
1939
|
|
|
533
|
-
#
|
|
1940
|
+
#popTemplateTokenContextBeforeExpressionChild() {
|
|
534
1941
|
let index = this.pos;
|
|
535
1942
|
let has_newline = false;
|
|
536
1943
|
|
|
537
|
-
//
|
|
1944
|
+
// JSXText-only template fragments can leave the tokenizer in JSX text mode.
|
|
538
1945
|
// Only unwind it for ASI before a following TSRX `{expr}` child;
|
|
539
1946
|
// fragment props like `content={<></>}` still need the JSX context.
|
|
540
1947
|
while (index < this.input.length) {
|
|
@@ -631,19 +2038,27 @@ export function TSRXPlugin(config) {
|
|
|
631
2038
|
}
|
|
632
2039
|
|
|
633
2040
|
/**
|
|
634
|
-
* @param {
|
|
2041
|
+
* @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
|
|
635
2042
|
* @returns {boolean}
|
|
636
2043
|
*/
|
|
637
2044
|
#hasDirectStatementChild(node) {
|
|
638
|
-
|
|
2045
|
+
const children = /** @type {AST.Node[]} */ (/** @type {unknown} */ (node.children ?? []));
|
|
2046
|
+
return children.some(
|
|
639
2047
|
(child) => child.type.endsWith('Statement') || child.type === 'VariableDeclaration',
|
|
640
2048
|
);
|
|
641
2049
|
}
|
|
642
2050
|
|
|
643
2051
|
/**
|
|
644
|
-
* @param {
|
|
2052
|
+
* @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
|
|
645
2053
|
*/
|
|
646
2054
|
#popTokenContextsAfterTemplateExpressionElement(node) {
|
|
2055
|
+
// A fragment in expression position (`() => <>…</>`) leaves the tokenizer
|
|
2056
|
+
// at `exprAllowed === false`, unlike a self-closing element. When the next
|
|
2057
|
+
// token is a `;`, the following statement may legitimately open with a JSX
|
|
2058
|
+
// tag (`<List/>`), so restore expression position to match the element path.
|
|
2059
|
+
if (this.type === tt.semi && node.type === 'JSXFragment') {
|
|
2060
|
+
this.exprAllowed = true;
|
|
2061
|
+
}
|
|
647
2062
|
const ctx = this.context;
|
|
648
2063
|
const ci = ctx.length - 1;
|
|
649
2064
|
const top = ctx[ci];
|
|
@@ -734,70 +2149,6 @@ export function TSRXPlugin(config) {
|
|
|
734
2149
|
}
|
|
735
2150
|
}
|
|
736
2151
|
|
|
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
2152
|
/**
|
|
802
2153
|
* @param {number} position
|
|
803
2154
|
* @param {number} end
|
|
@@ -888,8 +2239,9 @@ export function TSRXPlugin(config) {
|
|
|
888
2239
|
...node.metadata,
|
|
889
2240
|
invalid_tsrx_template_return: true,
|
|
890
2241
|
};
|
|
891
|
-
this.#
|
|
2242
|
+
this.#report_recoverable_error_range(
|
|
892
2243
|
/** @type {AST.NodeWithLocation} */ (node).start ?? this.start,
|
|
2244
|
+
/** @type {AST.NodeWithLocation} */ (node).end ?? this.start + 1,
|
|
893
2245
|
TSRX_RETURN_STATEMENT_ERROR,
|
|
894
2246
|
DIAGNOSTIC_CODES.TEMPLATE_RETURN_STATEMENT,
|
|
895
2247
|
);
|
|
@@ -1064,13 +2416,15 @@ export function TSRXPlugin(config) {
|
|
|
1064
2416
|
}
|
|
1065
2417
|
|
|
1066
2418
|
const container = this.#path[this.#path.length - 1];
|
|
1067
|
-
if (!container
|
|
2419
|
+
if (!this.#isNativeTemplateNode(container)) {
|
|
1068
2420
|
return null;
|
|
1069
2421
|
}
|
|
1070
2422
|
|
|
1071
|
-
const children = Array.isArray(
|
|
2423
|
+
const children = Array.isArray(/** @type {any} */ (container).children)
|
|
2424
|
+
? /** @type {any} */ (container).children
|
|
2425
|
+
: [];
|
|
1072
2426
|
const hasMeaningfulChildren = children.some(
|
|
1073
|
-
(child) => child && !isWhitespaceTextNode(child),
|
|
2427
|
+
(/** @type {any} */ child) => child && !isWhitespaceTextNode(child),
|
|
1074
2428
|
);
|
|
1075
2429
|
|
|
1076
2430
|
if (hasMeaningfulChildren) {
|
|
@@ -1115,9 +2469,58 @@ export function TSRXPlugin(config) {
|
|
|
1115
2469
|
* @type {Parse.Parser['readToken']}
|
|
1116
2470
|
*/
|
|
1117
2471
|
readToken(code) {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
2472
|
+
const suppressTemplateRawTextToken = this.#suppressTemplateRawTextToken;
|
|
2473
|
+
this.#suppressTemplateRawTextToken = false;
|
|
2474
|
+
const context = this.curContext();
|
|
2475
|
+
if (
|
|
2476
|
+
code !== CharCode.lessThan &&
|
|
2477
|
+
code !== CharCode.greaterThan &&
|
|
2478
|
+
code !== CharCode.openBrace &&
|
|
2479
|
+
code !== CharCode.closeBrace &&
|
|
2480
|
+
!suppressTemplateRawTextToken &&
|
|
2481
|
+
this.#shouldReadTemplateRawTextToken()
|
|
2482
|
+
) {
|
|
2483
|
+
return this.#readTemplateRawTextToken();
|
|
2484
|
+
}
|
|
2485
|
+
if (
|
|
2486
|
+
code === CharCode.greaterThan &&
|
|
2487
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.equals
|
|
2488
|
+
) {
|
|
2489
|
+
const start = this.pos - 1;
|
|
2490
|
+
const loc = acorn.getLineInfo(this.input, start);
|
|
2491
|
+
this.start = start;
|
|
2492
|
+
this.startLoc = loc;
|
|
2493
|
+
this.pos++;
|
|
2494
|
+
return this.finishToken(tt.arrow);
|
|
2495
|
+
}
|
|
2496
|
+
if (code === CharCode.lessThan) {
|
|
2497
|
+
const next = this.input.charCodeAt(this.pos + 1);
|
|
2498
|
+
if (
|
|
2499
|
+
next !== CharCode.slash &&
|
|
2500
|
+
(looks_like_generic_arrow(this.input, this.pos) ||
|
|
2501
|
+
this.#canStartTypeParameterOrArgumentList(this.pos))
|
|
2502
|
+
) {
|
|
2503
|
+
++this.pos;
|
|
2504
|
+
return this.finishToken(tt.relational, '<');
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
if (context === tstc.tc_expr || context === tstc.tc_oTag || context === tstc.tc_cTag) {
|
|
2508
|
+
return super.readToken(code);
|
|
2509
|
+
}
|
|
2510
|
+
if (code === CharCode.lessThan) {
|
|
2511
|
+
const next = this.input.charCodeAt(this.pos + 1);
|
|
2512
|
+
const isTagLikeAfterLt =
|
|
2513
|
+
next === CharCode.slash ||
|
|
2514
|
+
next === CharCode.greaterThan ||
|
|
2515
|
+
next === CharCode.at ||
|
|
2516
|
+
next === CharCode.dollar ||
|
|
2517
|
+
next === CharCode.underscore ||
|
|
2518
|
+
(next >= CharCode.uppercaseA && next <= CharCode.uppercaseZ) ||
|
|
2519
|
+
(next >= CharCode.lowercaseA && next <= CharCode.lowercaseZ);
|
|
2520
|
+
if (this.exprAllowed && isTagLikeAfterLt) {
|
|
2521
|
+
++this.pos;
|
|
2522
|
+
return this.finishToken(tstt.jsxTagStart);
|
|
2523
|
+
}
|
|
1121
2524
|
}
|
|
1122
2525
|
return super.readToken(code);
|
|
1123
2526
|
}
|
|
@@ -1127,74 +2530,91 @@ export function TSRXPlugin(config) {
|
|
|
1127
2530
|
* @type {Parse.Parser['getTokenFromCode']}
|
|
1128
2531
|
*/
|
|
1129
2532
|
getTokenFromCode(code) {
|
|
2533
|
+
// acorn-typescript only recognizes `@` as the at-token when it is not
|
|
2534
|
+
// reading a type. A return-type annotation (`function f(): T @{ … }`)
|
|
2535
|
+
// finishes while still `inType`, so its trailing `@` reaches the base
|
|
2536
|
+
// tokenizer, which throws "Unexpected character '@'". Emit the at-token
|
|
2537
|
+
// here so the `@{ … }` code block that follows the type can be parsed.
|
|
2538
|
+
if (code === CharCode.at && this.inType) {
|
|
2539
|
+
++this.pos;
|
|
2540
|
+
return this.finishToken(tstt.at);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
if (
|
|
2544
|
+
code === CharCode.greaterThan &&
|
|
2545
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.equals
|
|
2546
|
+
) {
|
|
2547
|
+
const start = this.pos - 1;
|
|
2548
|
+
const loc = acorn.getLineInfo(this.input, start);
|
|
2549
|
+
this.start = start;
|
|
2550
|
+
this.startLoc = loc;
|
|
2551
|
+
this.pos++;
|
|
2552
|
+
return this.finishToken(tt.arrow);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
1130
2555
|
// Callback props that return native templates without a semicolon can
|
|
1131
2556
|
// leave the attribute expression context above the still-open tag. Drop
|
|
1132
2557
|
// it before tokenizing `/>`, otherwise Acorn treats `/` as a regexp.
|
|
1133
2558
|
if (
|
|
1134
2559
|
code === CharCode.slash &&
|
|
1135
|
-
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
|
|
1136
|
-
this.context.includes(tstc.tc_oTag)
|
|
2560
|
+
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
|
|
1137
2561
|
) {
|
|
1138
|
-
while (
|
|
2562
|
+
while (
|
|
2563
|
+
this.context.length > 0 &&
|
|
2564
|
+
this.curContext() !== tstc.tc_oTag &&
|
|
2565
|
+
this.curContext() !== tstc.tc_expr
|
|
2566
|
+
) {
|
|
1139
2567
|
this.context.pop();
|
|
1140
2568
|
}
|
|
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();
|
|
2569
|
+
if (this.curContext() !== tstc.tc_oTag) {
|
|
2570
|
+
this.context.push(tstc.tc_oTag);
|
|
1148
2571
|
}
|
|
1149
|
-
|
|
1150
|
-
this.#allowDoubleQuotedTextChildAfterBrace = false;
|
|
2572
|
+
this.exprAllowed = false;
|
|
1151
2573
|
}
|
|
1152
2574
|
|
|
1153
|
-
if (
|
|
1154
|
-
|
|
2575
|
+
if (
|
|
2576
|
+
(code === CharCode.numberSign || code === CharCode.slash) &&
|
|
2577
|
+
this.#functionBodyDepth === 0 &&
|
|
2578
|
+
this.#isNativeTemplateNode(this.#path.at(-1)) &&
|
|
2579
|
+
!(
|
|
2580
|
+
code === CharCode.slash &&
|
|
2581
|
+
(this.input.charCodeAt(this.pos - 1) === CharCode.lessThan ||
|
|
2582
|
+
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan)
|
|
2583
|
+
)
|
|
2584
|
+
) {
|
|
2585
|
+
++this.pos;
|
|
2586
|
+
return this.finishToken(tt.name, this.input.slice(this.start, this.pos));
|
|
1155
2587
|
}
|
|
1156
2588
|
|
|
1157
2589
|
if (code === CharCode.lessThan) {
|
|
1158
2590
|
// < character
|
|
1159
2591
|
const parent = this.#path.at(-1);
|
|
1160
2592
|
const inNativeTemplate =
|
|
1161
|
-
this.#functionBodyDepth === 0 &&
|
|
1162
|
-
(parent?.type === 'Element' || parent?.type === 'TsrxFragment');
|
|
2593
|
+
this.#functionBodyDepth === 0 && this.#isNativeTemplateNode(parent);
|
|
1163
2594
|
/** @type {number | null} */
|
|
1164
2595
|
let prevNonWhitespaceChar = null;
|
|
2596
|
+
const nextChar =
|
|
2597
|
+
this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
|
|
1165
2598
|
|
|
1166
2599
|
// Check if this could be TypeScript generics instead of JSX
|
|
1167
|
-
// TypeScript generics appear
|
|
1168
|
-
//
|
|
2600
|
+
// TypeScript generics usually appear adjacent to an expression token,
|
|
2601
|
+
// for example: Array<T>, func<T>(), new Map<K,V>(), method<T>().
|
|
1169
2602
|
// This check applies everywhere, not just inside components
|
|
1170
2603
|
|
|
1171
2604
|
// 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
|
-
}
|
|
2605
|
+
const lookback = this.#previousNonSpaceTabIndex(this.pos);
|
|
1180
2606
|
|
|
1181
2607
|
// Check what character/token precedes the <
|
|
1182
2608
|
if (lookback >= 0) {
|
|
1183
2609
|
const prevChar = this.input.charCodeAt(lookback);
|
|
1184
2610
|
prevNonWhitespaceChar = prevChar;
|
|
1185
2611
|
|
|
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);
|
|
2612
|
+
if (
|
|
2613
|
+
nextChar !== CharCode.slash &&
|
|
2614
|
+
this.#canStartTypeParameterOrArgumentList(this.pos)
|
|
2615
|
+
) {
|
|
2616
|
+
++this.pos;
|
|
2617
|
+
return this.finishToken(tt.relational, '<');
|
|
1198
2618
|
}
|
|
1199
2619
|
}
|
|
1200
2620
|
|
|
@@ -1203,8 +2623,6 @@ export function TSRXPlugin(config) {
|
|
|
1203
2623
|
// <Something>...</Something>\n\n<Child />
|
|
1204
2624
|
// <head><style>...</style></head>
|
|
1205
2625
|
// 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
2626
|
const isWhitespaceAfterLt =
|
|
1209
2627
|
nextChar === CharCode.space ||
|
|
1210
2628
|
nextChar === CharCode.tab ||
|
|
@@ -1227,6 +2645,11 @@ export function TSRXPlugin(config) {
|
|
|
1227
2645
|
prevNonWhitespaceChar === CharCode.closeBrace ||
|
|
1228
2646
|
prevNonWhitespaceChar === CharCode.greaterThan;
|
|
1229
2647
|
|
|
2648
|
+
if (!inNativeTemplate && this.exprAllowed && isTagLikeAfterLt) {
|
|
2649
|
+
++this.pos;
|
|
2650
|
+
return this.finishToken(tstt.jsxTagStart);
|
|
2651
|
+
}
|
|
2652
|
+
|
|
1230
2653
|
if (!inNativeTemplate && prevAllowsTagStart && isTagLikeAfterLt) {
|
|
1231
2654
|
++this.pos;
|
|
1232
2655
|
return this.finishToken(tstt.jsxTagStart);
|
|
@@ -1237,13 +2660,10 @@ export function TSRXPlugin(config) {
|
|
|
1237
2660
|
// a newline/indentation before the next '<'. This is important for inputs
|
|
1238
2661
|
// like `<div />` and `</div><style>...</style>` which Prettier formats.
|
|
1239
2662
|
if (
|
|
1240
|
-
(prevNonWhitespaceChar === CharCode.doubleQuote &&
|
|
1241
|
-
this.#allowTagStartAfterDoubleQuotedText) ||
|
|
1242
2663
|
prevNonWhitespaceChar === CharCode.openBrace ||
|
|
1243
2664
|
prevNonWhitespaceChar === CharCode.greaterThan
|
|
1244
2665
|
) {
|
|
1245
2666
|
if (!isWhitespaceAfterLt) {
|
|
1246
|
-
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
1247
2667
|
++this.pos;
|
|
1248
2668
|
return this.finishToken(tstt.jsxTagStart);
|
|
1249
2669
|
}
|
|
@@ -1282,7 +2702,6 @@ export function TSRXPlugin(config) {
|
|
|
1282
2702
|
}
|
|
1283
2703
|
}
|
|
1284
2704
|
|
|
1285
|
-
this.#allowTagStartAfterDoubleQuotedText = false;
|
|
1286
2705
|
return super.getTokenFromCode(code);
|
|
1287
2706
|
}
|
|
1288
2707
|
|
|
@@ -1601,7 +3020,13 @@ export function TSRXPlugin(config) {
|
|
|
1601
3020
|
}
|
|
1602
3021
|
|
|
1603
3022
|
this.expect(tt.parenR);
|
|
1604
|
-
|
|
3023
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
3024
|
+
this.#readingJSXControlFlowHeader = false;
|
|
3025
|
+
try {
|
|
3026
|
+
node.body = /** @type {AST.BlockStatement} */ (this.parseStatement('for'));
|
|
3027
|
+
} finally {
|
|
3028
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
3029
|
+
}
|
|
1605
3030
|
this.exitScope();
|
|
1606
3031
|
this.labels.pop();
|
|
1607
3032
|
return this.finishNode(node, isForIn ? 'ForInStatement' : 'ForOfStatement');
|
|
@@ -1613,6 +3038,25 @@ export function TSRXPlugin(config) {
|
|
|
1613
3038
|
parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
|
|
1614
3039
|
this.#functionBodyDepth++;
|
|
1615
3040
|
try {
|
|
3041
|
+
// Allow a `@{ … }` code block as the body of a (non-arrow) function or
|
|
3042
|
+
// method, so a component can be written `function Something() @{ … }` or
|
|
3043
|
+
// `{ Render() @{ … } }`. Arrow concise bodies (`() => @{ … }`) already
|
|
3044
|
+
// route through `parseExprAtom`.
|
|
3045
|
+
//
|
|
3046
|
+
// A return-type annotation sits between the params and the body
|
|
3047
|
+
// (`function f(): T @{ … }`). acorn-typescript parses it inside
|
|
3048
|
+
// `super.parseFunctionBody` and then demands a `{` block, so the `@{ … }`
|
|
3049
|
+
// would never be seen. Parse the return type here first (exactly as
|
|
3050
|
+
// acorn-typescript does) so `this.start` lands on the `@` that follows.
|
|
3051
|
+
if (!isArrowFunction && this.match(tt.colon)) {
|
|
3052
|
+
node.returnType = this.tsParseTypeOrTypePredicateAnnotation(tt.colon);
|
|
3053
|
+
}
|
|
3054
|
+
if (!isArrowFunction && this.#isCodeBlockStart(this.start)) {
|
|
3055
|
+
node.body = this.parseMaybeAssign(forInit);
|
|
3056
|
+
this.checkParams(node, false);
|
|
3057
|
+
this.exitScope();
|
|
3058
|
+
return node;
|
|
3059
|
+
}
|
|
1616
3060
|
return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
|
|
1617
3061
|
} finally {
|
|
1618
3062
|
this.#functionBodyDepth--;
|
|
@@ -1623,22 +3067,41 @@ export function TSRXPlugin(config) {
|
|
|
1623
3067
|
* @return {ESTreeJSX.JSXExpressionContainer}
|
|
1624
3068
|
*/
|
|
1625
3069
|
jsx_parseExpressionContainer() {
|
|
3070
|
+
// Template child containers consume `}` after leaving container scope, so
|
|
3071
|
+
// the following sibling — which may be raw template text — tokenizes
|
|
3072
|
+
// normally (acorn already preserves whitespace in the surrounding
|
|
3073
|
+
// `tc_expr` context). Attribute-value and script-mode JSX containers keep
|
|
3074
|
+
// consuming `}` in scope: their following token is part of the tag or JS,
|
|
3075
|
+
// never template text.
|
|
3076
|
+
const consumeBraceAfterScope = this.#consumeContainerBraceAfterScope;
|
|
3077
|
+
this.#consumeContainerBraceAfterScope = false;
|
|
1626
3078
|
let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
|
|
1627
|
-
this
|
|
3079
|
+
this.#jsxExpressionContainerDepth++;
|
|
3080
|
+
try {
|
|
3081
|
+
this.next();
|
|
1628
3082
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
3083
|
+
node.expression =
|
|
3084
|
+
this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
|
|
3085
|
+
if (this.#allowExpressionContainerTrailingSemicolon && this.type === tt.semi) {
|
|
3086
|
+
if (this.#collect) {
|
|
3087
|
+
this.#report_recoverable_error(
|
|
3088
|
+
this.start,
|
|
3089
|
+
'TSRX expression containers do not use semicolons. Remove this semicolon.',
|
|
3090
|
+
DIAGNOSTIC_CODES.TEMPLATE_EXPRESSION_TRAILING_SEMICOLON,
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
3093
|
+
this.next();
|
|
1638
3094
|
}
|
|
1639
|
-
|
|
3095
|
+
if (!consumeBraceAfterScope) {
|
|
3096
|
+
this.expect(tt.braceR);
|
|
3097
|
+
}
|
|
3098
|
+
} finally {
|
|
3099
|
+
this.#jsxExpressionContainerDepth--;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
if (consumeBraceAfterScope) {
|
|
3103
|
+
this.expect(tt.braceR);
|
|
1640
3104
|
}
|
|
1641
|
-
this.expect(tt.braceR);
|
|
1642
3105
|
|
|
1643
3106
|
return this.finishNode(node, 'JSXExpressionContainer');
|
|
1644
3107
|
}
|
|
@@ -1671,81 +3134,140 @@ export function TSRXPlugin(config) {
|
|
|
1671
3134
|
);
|
|
1672
3135
|
}
|
|
1673
3136
|
|
|
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
3137
|
/**
|
|
1699
3138
|
* @type {Parse.Parser['jsx_parseAttribute']}
|
|
1700
3139
|
*/
|
|
1701
3140
|
jsx_parseAttribute() {
|
|
1702
|
-
let node =
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
3141
|
+
let node = /** @type {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} */ (
|
|
3142
|
+
this.startNode()
|
|
3143
|
+
);
|
|
3144
|
+
|
|
3145
|
+
if (this.type === tt.braceL) {
|
|
3146
|
+
let name_start = skip_whitespace_from(this.input, this.start + 1);
|
|
3147
|
+
const first = this.input.charCodeAt(name_start);
|
|
3148
|
+
if (
|
|
3149
|
+
this.#isIdentifierChar(first) &&
|
|
3150
|
+
!(first >= CharCode.digit0 && first <= CharCode.digit9)
|
|
3151
|
+
) {
|
|
3152
|
+
let name_end = name_start + 1;
|
|
3153
|
+
while (this.#isIdentifierChar(this.input.charCodeAt(name_end))) {
|
|
3154
|
+
name_end++;
|
|
3155
|
+
}
|
|
3156
|
+
const brace_start = skip_whitespace_from(this.input, name_end);
|
|
3157
|
+
if (this.input.charCodeAt(brace_start) === CharCode.closeBrace) {
|
|
3158
|
+
const name_start_loc = acorn.getLineInfo(this.input, name_start);
|
|
3159
|
+
const name_end_loc = acorn.getLineInfo(this.input, name_end);
|
|
3160
|
+
const name_value = this.input.slice(name_start, name_end);
|
|
3161
|
+
const id = /** @type {ESTreeJSX.JSXIdentifier} */ (
|
|
3162
|
+
this.startNodeAt(name_start, name_start_loc)
|
|
3163
|
+
);
|
|
3164
|
+
id.name = name_value;
|
|
3165
|
+
id.tracked = false;
|
|
3166
|
+
this.finishNodeAt(id, 'JSXIdentifier', name_end, name_end_loc);
|
|
3167
|
+
const name = /** @type {AST.Identifier} */ (
|
|
3168
|
+
this.startNodeAt(name_start, name_start_loc)
|
|
3169
|
+
);
|
|
3170
|
+
name.name = name_value;
|
|
3171
|
+
this.finishNodeAt(name, 'Identifier', name_end, name_end_loc);
|
|
3172
|
+
const expression = /** @type {ESTreeJSX.JSXExpressionContainer} */ (
|
|
3173
|
+
this.startNodeAt(this.start, this.startLoc)
|
|
3174
|
+
);
|
|
3175
|
+
expression.expression = name;
|
|
3176
|
+
this.finishNodeAt(
|
|
3177
|
+
expression,
|
|
3178
|
+
'JSXExpressionContainer',
|
|
3179
|
+
brace_start + 1,
|
|
3180
|
+
acorn.getLineInfo(this.input, brace_start + 1),
|
|
3181
|
+
);
|
|
3182
|
+
/** @type {ESTreeJSX.JSXAttribute} */ (node).name = id;
|
|
3183
|
+
/** @type {any} */ (node).value = expression;
|
|
3184
|
+
/** @type {any} */ (node).shorthand = true;
|
|
3185
|
+
|
|
3186
|
+
const end = brace_start + 1;
|
|
3187
|
+
const endLoc = acorn.getLineInfo(this.input, end);
|
|
3188
|
+
this.pos = end;
|
|
3189
|
+
this.curLine = endLoc.line;
|
|
3190
|
+
this.lineStart = end - endLoc.column;
|
|
3191
|
+
if (this.curContext()?.token === '{') {
|
|
3192
|
+
this.context.pop();
|
|
3193
|
+
}
|
|
3194
|
+
this.exprAllowed = false;
|
|
3195
|
+
this.next();
|
|
3196
|
+
return this.finishNodeAt(node, 'JSXAttribute', end, endLoc);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
1706
3200
|
|
|
1707
3201
|
if (this.eat(tt.braceL)) {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
);
|
|
1711
|
-
if (current_template_node?.type === 'TsxCompat') {
|
|
3202
|
+
if (this.type === tt.ellipsis || this.input.slice(this.start, this.start + 3) === '...') {
|
|
3203
|
+
this.#suppressTemplateRawTextToken = true;
|
|
1712
3204
|
if (this.type === tt.ellipsis) {
|
|
1713
3205
|
this.expect(tt.ellipsis);
|
|
3206
|
+
} else {
|
|
3207
|
+
this.pos = this.start + 3;
|
|
3208
|
+
this.nextToken();
|
|
3209
|
+
}
|
|
3210
|
+
this.#templateScriptParsingDepth++;
|
|
3211
|
+
try {
|
|
1714
3212
|
/** @type {ESTreeJSX.JSXSpreadAttribute} */ (node).argument = this.parseMaybeAssign();
|
|
1715
|
-
|
|
1716
|
-
|
|
3213
|
+
} finally {
|
|
3214
|
+
this.#templateScriptParsingDepth--;
|
|
1717
3215
|
}
|
|
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
3216
|
this.expect(tt.braceR);
|
|
1725
|
-
return this.finishNode(node, '
|
|
3217
|
+
return this.finishNode(node, 'JSXSpreadAttribute');
|
|
1726
3218
|
} else if (this.lookahead().type === tt.ellipsis) {
|
|
3219
|
+
this.#suppressTemplateRawTextToken = true;
|
|
1727
3220
|
this.expect(tt.ellipsis);
|
|
1728
|
-
|
|
3221
|
+
this.#templateScriptParsingDepth++;
|
|
3222
|
+
try {
|
|
3223
|
+
/** @type {ESTreeJSX.JSXSpreadAttribute} */ (node).argument = this.parseMaybeAssign();
|
|
3224
|
+
} finally {
|
|
3225
|
+
this.#templateScriptParsingDepth--;
|
|
3226
|
+
}
|
|
1729
3227
|
this.expect(tt.braceR);
|
|
1730
|
-
return this.finishNode(node, '
|
|
3228
|
+
return this.finishNode(node, 'JSXSpreadAttribute');
|
|
1731
3229
|
} else {
|
|
1732
|
-
|
|
3230
|
+
if (!(this.type === tt.name || this.type.keyword || this.type === tstt.jsxName)) {
|
|
3231
|
+
this.unexpected();
|
|
3232
|
+
}
|
|
3233
|
+
const name_start = this.start;
|
|
3234
|
+
const name_start_loc = this.startLoc;
|
|
3235
|
+
const name_end = this.end;
|
|
3236
|
+
const name_end_loc = this.endLoc;
|
|
3237
|
+
const name_value = /** @type {string} */ (this.value);
|
|
3238
|
+
const id = /** @type {ESTreeJSX.JSXIdentifier} */ (
|
|
3239
|
+
this.startNodeAt(name_start, name_start_loc)
|
|
3240
|
+
);
|
|
3241
|
+
id.name = name_value;
|
|
1733
3242
|
id.tracked = false;
|
|
1734
|
-
this.
|
|
1735
|
-
/** @type {AST.
|
|
1736
|
-
|
|
1737
|
-
|
|
3243
|
+
this.finishNodeAt(id, 'JSXIdentifier', name_end, name_end_loc);
|
|
3244
|
+
const name = /** @type {AST.Identifier} */ (
|
|
3245
|
+
this.startNodeAt(name_start, name_start_loc)
|
|
3246
|
+
);
|
|
3247
|
+
name.name = name_value;
|
|
3248
|
+
this.finishNodeAt(name, 'Identifier', name_end, name_end_loc);
|
|
3249
|
+
const expression = /** @type {ESTreeJSX.JSXExpressionContainer} */ (
|
|
3250
|
+
this.startNodeAt(
|
|
3251
|
+
/** @type {number} */ (node.start),
|
|
3252
|
+
/** @type {AST.NodeWithLocation} */ (node).loc.start,
|
|
3253
|
+
)
|
|
3254
|
+
);
|
|
3255
|
+
expression.expression = name;
|
|
3256
|
+
/** @type {ESTreeJSX.JSXAttribute} */ (node).name = id;
|
|
3257
|
+
/** @type {any} */ (node).value = this.finishNodeAt(
|
|
3258
|
+
expression,
|
|
3259
|
+
'JSXExpressionContainer',
|
|
3260
|
+
this.end + 1,
|
|
3261
|
+
this.endLoc,
|
|
3262
|
+
);
|
|
3263
|
+
/** @type {any} */ (node).shorthand = true;
|
|
1738
3264
|
this.next();
|
|
1739
3265
|
this.expect(tt.braceR);
|
|
1740
|
-
return this.finishNode(node, '
|
|
3266
|
+
return this.finishNode(node, 'JSXAttribute');
|
|
1741
3267
|
}
|
|
1742
3268
|
}
|
|
1743
3269
|
/** @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
|
-
) {
|
|
3270
|
+
if (this.#isDynamicJSXElementName(/** @type {ESTreeJSX.JSXAttribute} */ (node).name)) {
|
|
1749
3271
|
this.#report_recoverable_error_range(
|
|
1750
3272
|
/** @type {AST.NodeWithLocation} */ (node).start,
|
|
1751
3273
|
/** @type {AST.NodeWithLocation} */ (/** @type {ESTreeJSX.JSXAttribute} */ (node).name)
|
|
@@ -1790,7 +3312,7 @@ export function TSRXPlugin(config) {
|
|
|
1790
3312
|
|
|
1791
3313
|
if (this.type === tt.name || this.type === tstt.jsxName) {
|
|
1792
3314
|
node.name = /** @type {string} */ (this.value);
|
|
1793
|
-
node.
|
|
3315
|
+
/** @type {any} */ (node).dynamic = true;
|
|
1794
3316
|
this.next();
|
|
1795
3317
|
} else {
|
|
1796
3318
|
// Unexpected token after @
|
|
@@ -1798,7 +3320,6 @@ export function TSRXPlugin(config) {
|
|
|
1798
3320
|
}
|
|
1799
3321
|
} else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
|
|
1800
3322
|
node.name = /** @type {string} */ (this.value);
|
|
1801
|
-
node.tracked = false; // Explicitly mark as not tracked
|
|
1802
3323
|
this.next();
|
|
1803
3324
|
} else {
|
|
1804
3325
|
return super.jsx_parseIdentifier();
|
|
@@ -1867,10 +3388,132 @@ export function TSRXPlugin(config) {
|
|
|
1867
3388
|
}
|
|
1868
3389
|
}
|
|
1869
3390
|
|
|
3391
|
+
/**
|
|
3392
|
+
* `@try`/`@pending`/`@catch`/`finally` blocks lower their direct `return`
|
|
3393
|
+
* values into reactive boundary fallbacks, so unlike `@if`/`@for`/`@switch`
|
|
3394
|
+
* blocks they legitimately allow `return <markup>` statements. Set the flag
|
|
3395
|
+
* immediately before parsing each such block so its body sees it.
|
|
3396
|
+
* @returns {AST.BlockStatement}
|
|
3397
|
+
*/
|
|
3398
|
+
#parseTemplateControlFlowReturnBlock(createNewLexicalScope = true) {
|
|
3399
|
+
this.#controlFlowBlockAllowsNativeReturn = true;
|
|
3400
|
+
return this.#parseTemplateControlFlowBlock(createNewLexicalScope);
|
|
3401
|
+
}
|
|
3402
|
+
|
|
1870
3403
|
/**
|
|
1871
3404
|
* @type {Parse.Parser['parseTryStatement']}
|
|
1872
3405
|
*/
|
|
1873
3406
|
parseTryStatement(node) {
|
|
3407
|
+
if (this.#templateControlFlowTryDepth > 0) {
|
|
3408
|
+
this.#templateControlFlowTryDepth--;
|
|
3409
|
+
try {
|
|
3410
|
+
this.next();
|
|
3411
|
+
node.block = this.#parseTemplateControlFlowReturnBlock();
|
|
3412
|
+
node.handler = null;
|
|
3413
|
+
|
|
3414
|
+
if (this.#eatJSXDirectiveClauseKeyword('pending')) {
|
|
3415
|
+
node.pending = this.#parseTemplateControlFlowReturnBlock();
|
|
3416
|
+
} else if (this.#isUnprefixedDirectiveClauseKeyword('pending')) {
|
|
3417
|
+
this.raise(this.start, 'Expected `@pending` after `@try` block.');
|
|
3418
|
+
} else {
|
|
3419
|
+
node.pending = null;
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
const clauseStart = this.start;
|
|
3423
|
+
const clauseStartLoc = this.startLoc;
|
|
3424
|
+
if (this.#eatJSXDirectiveClauseKeyword('catch')) {
|
|
3425
|
+
if (this.type === tt._catch || this.value === 'catch') {
|
|
3426
|
+
this.next();
|
|
3427
|
+
}
|
|
3428
|
+
const paramStart = skip_whitespace_from(this.input, this.start);
|
|
3429
|
+
if (this.input.charCodeAt(paramStart) === CharCode.openParen) {
|
|
3430
|
+
this.pos = paramStart;
|
|
3431
|
+
this.start = paramStart;
|
|
3432
|
+
this.startLoc = acorn.getLineInfo(this.input, paramStart);
|
|
3433
|
+
this.curLine = this.startLoc.line;
|
|
3434
|
+
this.lineStart = paramStart - this.startLoc.column;
|
|
3435
|
+
this.context = this.context.filter(
|
|
3436
|
+
(context) =>
|
|
3437
|
+
context !== tstc.tc_expr &&
|
|
3438
|
+
context !== tstc.tc_oTag &&
|
|
3439
|
+
context !== tstc.tc_cTag,
|
|
3440
|
+
);
|
|
3441
|
+
if (this.curContext() !== b_stat) {
|
|
3442
|
+
this.context.push(b_stat);
|
|
3443
|
+
}
|
|
3444
|
+
this.exprAllowed = true;
|
|
3445
|
+
this.#suppressTemplateRawTextToken = true;
|
|
3446
|
+
try {
|
|
3447
|
+
this.nextToken();
|
|
3448
|
+
} finally {
|
|
3449
|
+
this.#suppressTemplateRawTextToken = false;
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
const clause = /** @type {AST.CatchClause} */ (
|
|
3453
|
+
this.startNodeAt(clauseStart, clauseStartLoc)
|
|
3454
|
+
);
|
|
3455
|
+
const previous_reading_header = this.#readingJSXControlFlowHeader;
|
|
3456
|
+
this.#readingJSXControlFlowHeader = true;
|
|
3457
|
+
try {
|
|
3458
|
+
if (this.eat(tt.parenL)) {
|
|
3459
|
+
const param = this.parseBindingAtom();
|
|
3460
|
+
const simple = param.type === 'Identifier';
|
|
3461
|
+
this.enterScope(simple ? BINDING_TYPES.BIND_SIMPLE_CATCH : 0);
|
|
3462
|
+
this.checkLValPattern(
|
|
3463
|
+
param,
|
|
3464
|
+
simple ? BINDING_TYPES.BIND_SIMPLE_CATCH : BINDING_TYPES.BIND_LEXICAL,
|
|
3465
|
+
);
|
|
3466
|
+
const type = this.tsTryParseTypeAnnotation();
|
|
3467
|
+
if (type) {
|
|
3468
|
+
param.typeAnnotation = type;
|
|
3469
|
+
this.resetEndLocation(param);
|
|
3470
|
+
}
|
|
3471
|
+
clause.param = param;
|
|
3472
|
+
|
|
3473
|
+
if (this.eat(tt.comma)) {
|
|
3474
|
+
const reset_param = this.parseBindingAtom();
|
|
3475
|
+
this.checkLValSimple(reset_param, BINDING_TYPES.BIND_LEXICAL);
|
|
3476
|
+
const reset_type = this.tsTryParseTypeAnnotation();
|
|
3477
|
+
if (reset_type) {
|
|
3478
|
+
reset_param.typeAnnotation = reset_type;
|
|
3479
|
+
this.resetEndLocation(reset_param);
|
|
3480
|
+
}
|
|
3481
|
+
clause.resetParam = reset_param;
|
|
3482
|
+
} else {
|
|
3483
|
+
clause.resetParam = null;
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
this.expect(tt.parenR);
|
|
3487
|
+
} else {
|
|
3488
|
+
clause.param = null;
|
|
3489
|
+
clause.resetParam = null;
|
|
3490
|
+
this.enterScope(0);
|
|
3491
|
+
}
|
|
3492
|
+
} finally {
|
|
3493
|
+
this.#readingJSXControlFlowHeader = previous_reading_header;
|
|
3494
|
+
}
|
|
3495
|
+
clause.body = this.#parseTemplateControlFlowReturnBlock(false);
|
|
3496
|
+
this.exitScope();
|
|
3497
|
+
node.handler = this.finishNode(clause, 'CatchClause');
|
|
3498
|
+
} else if (this.#isUnprefixedDirectiveClauseKeyword('catch')) {
|
|
3499
|
+
this.raise(this.start, 'Expected `@catch` after `@try` block.');
|
|
3500
|
+
}
|
|
3501
|
+
node.finalizer = this.eat(tt._finally)
|
|
3502
|
+
? this.#parseTemplateControlFlowReturnBlock()
|
|
3503
|
+
: null;
|
|
3504
|
+
|
|
3505
|
+
if (!node.handler && !node.finalizer && !node.pending) {
|
|
3506
|
+
this.raise(
|
|
3507
|
+
/** @type {AST.NodeWithLocation} */ (node).start,
|
|
3508
|
+
'Missing catch or finally clause',
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
return this.finishNode(node, 'TryStatement');
|
|
3512
|
+
} finally {
|
|
3513
|
+
this.#templateControlFlowTryDepth++;
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
|
|
1874
3517
|
this.next();
|
|
1875
3518
|
node.block = this.parseBlock();
|
|
1876
3519
|
node.handler = null;
|
|
@@ -1937,22 +3580,55 @@ export function TSRXPlugin(config) {
|
|
|
1937
3580
|
return this.finishNode(node, 'TryStatement');
|
|
1938
3581
|
}
|
|
1939
3582
|
|
|
1940
|
-
/** @type {Parse.Parser['jsx_readToken']} */
|
|
1941
|
-
jsx_readToken() {
|
|
1942
|
-
|
|
1943
|
-
(
|
|
1944
|
-
|
|
1945
|
-
|
|
3583
|
+
/** @type {Parse.Parser['jsx_readToken']} */
|
|
3584
|
+
jsx_readToken() {
|
|
3585
|
+
if (this.#scriptJSXElementDepth > 0 || this.#path.length === 0) {
|
|
3586
|
+
if (
|
|
3587
|
+
this.input.charCodeAt(this.pos) === CharCode.closeBrace &&
|
|
3588
|
+
this.context.includes(tstc.tc_expr)
|
|
3589
|
+
) {
|
|
3590
|
+
this.#resetTokenStartToCurrentPosition();
|
|
3591
|
+
return original.readToken.call(this, CharCode.closeBrace);
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
let index = this.pos;
|
|
3595
|
+
while (
|
|
3596
|
+
this.input.charCodeAt(index) === CharCode.space ||
|
|
3597
|
+
this.input.charCodeAt(index) === CharCode.tab ||
|
|
3598
|
+
this.input.charCodeAt(index) === CharCode.lineFeed ||
|
|
3599
|
+
this.input.charCodeAt(index) === CharCode.carriageReturn
|
|
3600
|
+
) {
|
|
3601
|
+
index++;
|
|
3602
|
+
}
|
|
3603
|
+
if (
|
|
3604
|
+
index !== this.pos &&
|
|
3605
|
+
this.input.charCodeAt(index) === CharCode.slash &&
|
|
3606
|
+
this.input.charCodeAt(index + 1) === CharCode.greaterThan &&
|
|
3607
|
+
this.context.includes(tstc.tc_expr)
|
|
3608
|
+
) {
|
|
3609
|
+
const loc = acorn.getLineInfo(this.input, index);
|
|
3610
|
+
this.pos = index;
|
|
3611
|
+
this.start = index;
|
|
3612
|
+
this.startLoc = loc;
|
|
3613
|
+
this.curLine = loc.line;
|
|
3614
|
+
this.lineStart = index - loc.column;
|
|
3615
|
+
this.exprAllowed = false;
|
|
3616
|
+
if (this.curContext() !== tstc.tc_oTag) {
|
|
3617
|
+
this.context.push(tstc.tc_oTag);
|
|
3618
|
+
}
|
|
3619
|
+
return original.readToken.call(this, CharCode.slash);
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
if (this.#scriptJSXElementDepth > 0 || this.#path.length === 0) {
|
|
1946
3623
|
return super.jsx_readToken();
|
|
1947
3624
|
}
|
|
3625
|
+
|
|
1948
3626
|
let out = '',
|
|
1949
3627
|
chunkStart = this.pos;
|
|
1950
3628
|
|
|
1951
3629
|
while (true) {
|
|
1952
3630
|
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
|
-
);
|
|
3631
|
+
const inside_open_template = this.#path.findLast((n) => this.#isNativeTemplateNode(n));
|
|
1956
3632
|
if (!inside_open_template) {
|
|
1957
3633
|
while (this.curContext() === tstc.tc_expr) {
|
|
1958
3634
|
this.context.pop();
|
|
@@ -1964,8 +3640,29 @@ export function TSRXPlugin(config) {
|
|
|
1964
3640
|
let ch = this.input.charCodeAt(this.pos);
|
|
1965
3641
|
|
|
1966
3642
|
switch (ch) {
|
|
3643
|
+
case CharCode.equals:
|
|
3644
|
+
if (
|
|
3645
|
+
!this.#shouldReadTemplateRawTextToken() &&
|
|
3646
|
+
this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
|
|
3647
|
+
) {
|
|
3648
|
+
this.#resetTokenStartToCurrentPosition();
|
|
3649
|
+
this.pos += 2;
|
|
3650
|
+
return this.finishToken(tt.arrow);
|
|
3651
|
+
}
|
|
3652
|
+
if (this.#shouldReadTemplateRawTextToken()) {
|
|
3653
|
+
++this.pos;
|
|
3654
|
+
break;
|
|
3655
|
+
}
|
|
3656
|
+
this.#resetTokenStartToCurrentPosition();
|
|
3657
|
+
this.context.push(b_stat);
|
|
3658
|
+
this.exprAllowed = true;
|
|
3659
|
+
return original.readToken.call(this, ch);
|
|
3660
|
+
|
|
1967
3661
|
case CharCode.lessThan:
|
|
1968
3662
|
case CharCode.openBrace:
|
|
3663
|
+
if (out || this.pos > chunkStart) {
|
|
3664
|
+
return this.finishToken(tstt.jsxText, out + this.input.slice(chunkStart, this.pos));
|
|
3665
|
+
}
|
|
1969
3666
|
// In JSX text mode, '<' and '{' always start a tag/expression container.
|
|
1970
3667
|
// `exprAllowed` can be false here due to surrounding parser state, but
|
|
1971
3668
|
// throwing breaks valid templates (e.g. sibling tags after a close).
|
|
@@ -2012,6 +3709,7 @@ export function TSRXPlugin(config) {
|
|
|
2012
3709
|
}
|
|
2013
3710
|
|
|
2014
3711
|
// Continue processing from current position
|
|
3712
|
+
chunkStart = this.pos;
|
|
2015
3713
|
break;
|
|
2016
3714
|
} else if (this.input.charCodeAt(this.pos + 1) === CharCode.asterisk) {
|
|
2017
3715
|
// '/*'
|
|
@@ -2051,9 +3749,13 @@ export function TSRXPlugin(config) {
|
|
|
2051
3749
|
}
|
|
2052
3750
|
|
|
2053
3751
|
// Continue processing from current position
|
|
3752
|
+
chunkStart = this.pos;
|
|
3753
|
+
break;
|
|
3754
|
+
}
|
|
3755
|
+
if (this.#shouldReadTemplateRawTextToken()) {
|
|
3756
|
+
++this.pos;
|
|
2054
3757
|
break;
|
|
2055
3758
|
}
|
|
2056
|
-
// If not a comment, fall through to default case
|
|
2057
3759
|
this.#resetTokenStartToCurrentPosition();
|
|
2058
3760
|
this.context.push(b_stat);
|
|
2059
3761
|
this.exprAllowed = true;
|
|
@@ -2068,10 +3770,21 @@ export function TSRXPlugin(config) {
|
|
|
2068
3770
|
case CharCode.greaterThan:
|
|
2069
3771
|
case CharCode.closeBrace: {
|
|
2070
3772
|
if (
|
|
2071
|
-
ch === CharCode.
|
|
2072
|
-
(this
|
|
2073
|
-
|
|
2074
|
-
|
|
3773
|
+
ch === CharCode.greaterThan &&
|
|
3774
|
+
this.input.charCodeAt(this.pos - 1) === CharCode.equals &&
|
|
3775
|
+
!this.#shouldReadTemplateRawTextToken()
|
|
3776
|
+
) {
|
|
3777
|
+
const start = this.pos - 1;
|
|
3778
|
+
const loc = acorn.getLineInfo(this.input, start);
|
|
3779
|
+
this.start = start;
|
|
3780
|
+
this.startLoc = loc;
|
|
3781
|
+
this.pos++;
|
|
3782
|
+
return this.finishToken(tt.arrow);
|
|
3783
|
+
}
|
|
3784
|
+
if (
|
|
3785
|
+
this.#isInsideNativeTemplateScriptSection() ||
|
|
3786
|
+
(ch === CharCode.closeBrace &&
|
|
3787
|
+
(this.#path.length === 0 || this.#isNativeTemplateNode(this.#path.at(-1))))
|
|
2075
3788
|
) {
|
|
2076
3789
|
this.#resetTokenStartToCurrentPosition();
|
|
2077
3790
|
return original.readToken.call(this, ch);
|
|
@@ -2098,6 +3811,10 @@ export function TSRXPlugin(config) {
|
|
|
2098
3811
|
} else if (ch === CharCode.space || ch === CharCode.tab) {
|
|
2099
3812
|
++this.pos;
|
|
2100
3813
|
} else {
|
|
3814
|
+
if (this.#shouldReadTemplateRawTextToken()) {
|
|
3815
|
+
++this.pos;
|
|
3816
|
+
break;
|
|
3817
|
+
}
|
|
2101
3818
|
this.#resetTokenStartToCurrentPosition();
|
|
2102
3819
|
this.context.push(b_stat);
|
|
2103
3820
|
this.exprAllowed = true;
|
|
@@ -2108,465 +3825,326 @@ export function TSRXPlugin(config) {
|
|
|
2108
3825
|
}
|
|
2109
3826
|
|
|
2110
3827
|
/**
|
|
2111
|
-
* Override jsx_parseElement to
|
|
2112
|
-
*
|
|
2113
|
-
* their children.
|
|
3828
|
+
* Override jsx_parseElement to use TSRX template parsing only where the
|
|
3829
|
+
* fragment/element body can contain TSRX-only syntax.
|
|
2114
3830
|
* @type {Parse.Parser['jsx_parseElement']}
|
|
2115
3831
|
*/
|
|
2116
3832
|
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,
|
|
3833
|
+
if (this.#forceScriptJSXElementDepth > 0 || this.#isInsideNativeTemplateScriptSection()) {
|
|
3834
|
+
if (this.#isStyleOpeningTagStart()) {
|
|
3835
|
+
this.next();
|
|
3836
|
+
return /** @type {ESTreeJSX.JSXElement | AST.JSXStyleElement} */ (
|
|
3837
|
+
/** @type {unknown} */ (this.parseElement())
|
|
2129
3838
|
);
|
|
2130
3839
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
3840
|
+
|
|
3841
|
+
this.#scriptJSXElementDepth++;
|
|
3842
|
+
try {
|
|
3843
|
+
return super.jsx_parseElement();
|
|
3844
|
+
} finally {
|
|
3845
|
+
this.#scriptJSXElementDepth--;
|
|
3846
|
+
}
|
|
2133
3847
|
}
|
|
2134
3848
|
|
|
2135
3849
|
this.next();
|
|
2136
3850
|
const parsed = /** @type {import('estree-jsx').JSXElement} */ (
|
|
2137
3851
|
/** @type {unknown} */ (this.parseElement())
|
|
2138
3852
|
);
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
3853
|
+
this.#popTokenContextsAfterTemplateExpressionElement(parsed);
|
|
3854
|
+
return parsed;
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
/**
|
|
3858
|
+
* @type {Parse.Parser['jsx_parseElementAt']}
|
|
3859
|
+
*/
|
|
3860
|
+
jsx_parseElementAt(startPos, startLoc) {
|
|
3861
|
+
if (this.input.charCodeAt(startPos + 1) === CharCode.at) {
|
|
3862
|
+
const previous_script_jsx_element_depth = this.#scriptJSXElementDepth;
|
|
3863
|
+
this.#scriptJSXElementDepth = 0;
|
|
3864
|
+
try {
|
|
3865
|
+
const parsed = /** @type {ESTreeJSX.JSXElement} */ (
|
|
3866
|
+
/** @type {unknown} */ (this.parseElement())
|
|
3867
|
+
);
|
|
3868
|
+
// A dynamic `<@tag>` parsed here goes straight through `parseElement`,
|
|
3869
|
+
// bypassing `jsx_parseElement`'s context cleanup. In expression
|
|
3870
|
+
// position (e.g. a render-prop arrow body inside object params) its
|
|
3871
|
+
// markup contexts must be unwound, or the following JS token (a `,`/`}`)
|
|
3872
|
+
// is mis-tokenized as JSX raw text.
|
|
3873
|
+
this.#popTokenContextsAfterTemplateExpressionElement(parsed);
|
|
3874
|
+
return parsed;
|
|
3875
|
+
} finally {
|
|
3876
|
+
this.#scriptJSXElementDepth = previous_script_jsx_element_depth;
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
return super.jsx_parseElementAt(startPos, startLoc);
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
/**
|
|
3884
|
+
* @type {Parse.Parser['jsx_parseOpeningElementAt']}
|
|
3885
|
+
*/
|
|
3886
|
+
jsx_parseOpeningElementAt(startPos, startLoc) {
|
|
3887
|
+
const node = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
|
|
3888
|
+
this.startNodeAt(/** @type {number} */ (startPos), /** @type {AST.Position} */ (startLoc))
|
|
3889
|
+
);
|
|
3890
|
+
node.attributes = [];
|
|
3891
|
+
const nodeName = this.jsx_parseElementName();
|
|
3892
|
+
if (nodeName) node.name = nodeName;
|
|
3893
|
+
if (this.#isDynamicJSXElementName(nodeName)) {
|
|
3894
|
+
/** @type {any} */ (node).dynamic = true;
|
|
3895
|
+
}
|
|
3896
|
+
if (this.match(tt.relational) || this.match(tt.bitShift)) {
|
|
3897
|
+
const typeArguments = /** @type {any} */ (this).tsTryParseAndCatch(() =>
|
|
3898
|
+
/** @type {any} */ (this).tsParseTypeArgumentsInExpression(),
|
|
2142
3899
|
);
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
3900
|
+
if (typeArguments) node.typeArguments = typeArguments;
|
|
3901
|
+
}
|
|
3902
|
+
while (this.type !== tt.slash && this.type !== tstt.jsxTagEnd) {
|
|
3903
|
+
node.attributes.push(this.jsx_parseAttribute());
|
|
3904
|
+
}
|
|
3905
|
+
node.selfClosing = this.eat(tt.slash);
|
|
3906
|
+
|
|
3907
|
+
const opening_template_node = this.#openingNativeTemplateNode;
|
|
3908
|
+
let pushed_opening_template_node = false;
|
|
3909
|
+
if (opening_template_node) {
|
|
3910
|
+
if (nodeName) {
|
|
3911
|
+
/** @type {any} */ (opening_template_node).type =
|
|
3912
|
+
this.getElementName(nodeName) === 'style' ? 'JSXStyleElement' : 'JSXElement';
|
|
3913
|
+
/** @type {any} */ (opening_template_node).openingElement = node;
|
|
3914
|
+
/** @type {any} */ (opening_template_node).closingElement = null;
|
|
3915
|
+
} else {
|
|
3916
|
+
/** @type {any} */ (opening_template_node).type = 'JSXFragment';
|
|
3917
|
+
/** @type {any} */ (opening_template_node).openingFragment =
|
|
3918
|
+
this.#toOpeningFragment(node);
|
|
3919
|
+
/** @type {any} */ (opening_template_node).closingFragment = null;
|
|
2149
3920
|
}
|
|
3921
|
+
this.#path.push(opening_template_node);
|
|
3922
|
+
pushed_opening_template_node = true;
|
|
2150
3923
|
}
|
|
2151
|
-
|
|
3924
|
+
|
|
3925
|
+
try {
|
|
3926
|
+
this.expect(tstt.jsxTagEnd);
|
|
3927
|
+
} finally {
|
|
3928
|
+
if (pushed_opening_template_node) {
|
|
3929
|
+
this.#path.pop();
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
if (nodeName) {
|
|
3933
|
+
return this.finishNode(node, 'JSXOpeningElement');
|
|
3934
|
+
}
|
|
3935
|
+
return /** @type {any} */ (
|
|
3936
|
+
/** @type {any} */ (this).finishNode(node, 'JSXOpeningFragment')
|
|
3937
|
+
);
|
|
2152
3938
|
}
|
|
2153
3939
|
|
|
2154
3940
|
/**
|
|
2155
3941
|
* @type {Parse.Parser['parseElement']}
|
|
2156
3942
|
*/
|
|
2157
3943
|
parseElement() {
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
)
|
|
3944
|
+
// Depth the tokenizer context must return to once this element closes:
|
|
3945
|
+
// the stack with the element's own opening `<` contexts (a trailing
|
|
3946
|
+
// tc_oTag/tc_expr) stripped off. A balanced element should leave the
|
|
3947
|
+
// stack here; the body (especially a control-flow block) can otherwise
|
|
3948
|
+
// leave residue that breaks tokenizing the following JS token when the
|
|
3949
|
+
// element is in expression position.
|
|
3950
|
+
let pre_element_context_depth = this.context.length;
|
|
3951
|
+
while (pre_element_context_depth > 0) {
|
|
3952
|
+
const ctx = this.context[pre_element_context_depth - 1];
|
|
3953
|
+
if (ctx === tstc.tc_expr || ctx === tstc.tc_oTag || ctx === tstc.tc_cTag) {
|
|
3954
|
+
pre_element_context_depth--;
|
|
3955
|
+
} else {
|
|
3956
|
+
break;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
|
|
2161
3960
|
// Adjust the start so we capture the `<` as part of the element
|
|
2162
3961
|
const start = this.start - 1;
|
|
2163
3962
|
const position = new acorn.Position(this.curLine, start - this.lineStart);
|
|
2164
3963
|
|
|
2165
|
-
const
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
3964
|
+
const node =
|
|
3965
|
+
/** @type {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment | AST.JSXStyleElement} */ (
|
|
3966
|
+
/** @type {unknown} */ (this.startNode())
|
|
3967
|
+
);
|
|
3968
|
+
node.start = start;
|
|
3969
|
+
/** @type {AST.NodeWithLocation} */ (node).loc.start = position;
|
|
3970
|
+
node.metadata = {
|
|
3971
|
+
path: [],
|
|
3972
|
+
native_tsrx: true,
|
|
3973
|
+
templateMode: 'script',
|
|
3974
|
+
};
|
|
3975
|
+
node.children = [];
|
|
3976
|
+
|
|
3977
|
+
const previous_opening_native_template_node = this.#openingNativeTemplateNode;
|
|
3978
|
+
this.#openingNativeTemplateNode = node;
|
|
3979
|
+
let open;
|
|
3980
|
+
try {
|
|
3981
|
+
open = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
|
|
3982
|
+
this.jsx_parseOpeningElementAt(start, position)
|
|
3983
|
+
);
|
|
3984
|
+
} finally {
|
|
3985
|
+
this.#openingNativeTemplateNode = previous_opening_native_template_node;
|
|
3986
|
+
}
|
|
3987
|
+
const tag_name = open.name ? this.getElementName(open.name) : null;
|
|
3988
|
+
const is_style = tag_name === 'style';
|
|
3989
|
+
const inside_head = this.#path.findLast((n) => this.#isNativeElementNamed(n, 'head'));
|
|
2181
3990
|
|
|
2182
3991
|
// Fragments (<>) produce JSXOpeningFragment with no `name` property
|
|
2183
3992
|
const is_fragment = !open.name;
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
3993
|
+
const parent_template_node = this.#currentNativeTemplateNode();
|
|
3994
|
+
const parent_is_template_output =
|
|
3995
|
+
parent_template_node?.metadata?.templateMode === 'template';
|
|
3996
|
+
node.metadata.templateMode =
|
|
3997
|
+
is_fragment && parent_is_template_output ? 'template' : 'script';
|
|
3998
|
+
if (!is_fragment && open.name.type === 'JSXNamespacedName') {
|
|
2189
3999
|
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';
|
|
4000
|
+
const tagName = namespace_node.namespace.name + ':' + namespace_node.name.name;
|
|
4001
|
+
this.raise(
|
|
4002
|
+
open.start,
|
|
4003
|
+
`Namespaced elements are not supported in TSRX templates: <${tagName}>.`,
|
|
4004
|
+
);
|
|
2204
4005
|
}
|
|
2205
4006
|
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
}
|
|
4007
|
+
if (is_fragment) {
|
|
4008
|
+
/** @type {ESTreeJSX.JSXFragment} */ (node).type = 'JSXFragment';
|
|
4009
|
+
/** @type {ESTreeJSX.JSXFragment} */ (node).openingFragment =
|
|
4010
|
+
this.#toOpeningFragment(open);
|
|
4011
|
+
/** @type {any} */ (node).closingFragment = null;
|
|
4012
|
+
} else {
|
|
4013
|
+
if (is_style) {
|
|
4014
|
+
/** @type {AST.JSXStyleElement} */ (node).type = 'JSXStyleElement';
|
|
4015
|
+
/** @type {AST.JSXStyleElement} */ (node).openingElement = open;
|
|
4016
|
+
/** @type {AST.JSXStyleElement} */ (node).closingElement = null;
|
|
4017
|
+
} else {
|
|
4018
|
+
/** @type {ESTreeJSX.JSXElement} */ (node).type = 'JSXElement';
|
|
4019
|
+
/** @type {ESTreeJSX.JSXElement} */ (node).openingElement = open;
|
|
4020
|
+
/** @type {ESTreeJSX.JSXElement} */ (node).closingElement = null;
|
|
4021
|
+
if (/** @type {any} */ (open).dynamic) {
|
|
4022
|
+
/** @type {any} */ (node).dynamic = true;
|
|
2223
4023
|
}
|
|
2224
4024
|
}
|
|
2225
4025
|
}
|
|
2226
4026
|
|
|
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
4027
|
// Opening-tag parsing can tokenize comments that appear before the first
|
|
2239
4028
|
// child. Preserve that early container id so the comment stays associated
|
|
2240
4029
|
// with this element during comment attachment/printing.
|
|
2241
|
-
if (
|
|
2242
|
-
|
|
4030
|
+
if (node.metadata.commentContainerId === undefined) {
|
|
4031
|
+
node.metadata.commentContainerId = ++this.#commentContextId;
|
|
2243
4032
|
}
|
|
2244
4033
|
|
|
2245
|
-
|
|
2246
|
-
this.#path.pop();
|
|
4034
|
+
this.#path.push(node);
|
|
2247
4035
|
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
|
|
4036
|
+
if (!is_fragment && open.selfClosing) {
|
|
4037
|
+
this.#path.pop();
|
|
4038
|
+
} else if (is_style) {
|
|
4039
|
+
this.#parseStyleElement(open, /** @type {AST.JSXStyleElement} */ (node), !!inside_head);
|
|
4040
|
+
this.#path.pop();
|
|
4041
|
+
} else {
|
|
4042
|
+
this.#parseNativeTemplateBody(node, /** @type {AST.Node[]} */ (node.children), {
|
|
2254
4043
|
enterScope: true,
|
|
2255
4044
|
resetFunctionBodyDepth: true,
|
|
2256
4045
|
});
|
|
2257
4046
|
|
|
2258
|
-
this.#path.
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
4047
|
+
if (this.#path[this.#path.length - 1] === node) {
|
|
4048
|
+
const displayTag = is_fragment
|
|
4049
|
+
? ''
|
|
4050
|
+
: this.getElementName(/** @type {ESTreeJSX.JSXElement} */ (node).openingElement.name);
|
|
4051
|
+
this.#report_broken_markup_error(
|
|
4052
|
+
this.start,
|
|
4053
|
+
`Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
|
|
4054
|
+
);
|
|
4055
|
+
/** @type {any} */ (node).unclosed = true;
|
|
4056
|
+
/** @type {AST.SourceLocation} */ (node.loc).end = {
|
|
4057
|
+
.../** @type {AST.SourceLocation} */ (
|
|
4058
|
+
is_fragment
|
|
4059
|
+
? /** @type {ESTreeJSX.JSXFragment} */ (node).openingFragment.loc
|
|
4060
|
+
: /** @type {ESTreeJSX.JSXElement} */ (node).openingElement.loc
|
|
4061
|
+
).end,
|
|
2263
4062
|
};
|
|
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
|
-
}
|
|
4063
|
+
node.end = is_fragment
|
|
4064
|
+
? /** @type {ESTreeJSX.JSXFragment} */ (node).openingFragment.end
|
|
4065
|
+
: /** @type {ESTreeJSX.JSXElement} */ (node).openingElement.end;
|
|
4066
|
+
this.#path.pop();
|
|
2493
4067
|
}
|
|
2494
4068
|
|
|
2495
|
-
//
|
|
2496
|
-
|
|
4069
|
+
// A balanced element must leave the tokenizer context exactly where it
|
|
4070
|
+
// began. The body (especially a control-flow block) can leave residue
|
|
4071
|
+
// above the children context — the children tc_expr plus a spurious
|
|
4072
|
+
// b_stat from an @if/@for block save-restore — which the old single
|
|
4073
|
+
// tc_expr pop missed when the b_stat sat on top. In expression position,
|
|
4074
|
+
// unwind back to the pre-element depth so the following JS token (e.g. a
|
|
4075
|
+
// comma/brace closing an enclosing object) tokenizes as code, not text.
|
|
2497
4076
|
const parent = this.#path.at(-1);
|
|
2498
4077
|
const insideTemplate = this.#isNativeTemplateNode(parent);
|
|
2499
4078
|
|
|
2500
|
-
if (
|
|
2501
|
-
this.context.
|
|
4079
|
+
if (!insideTemplate && this.context.length > pre_element_context_depth) {
|
|
4080
|
+
this.context.length = pre_element_context_depth;
|
|
2502
4081
|
}
|
|
2503
4082
|
}
|
|
2504
4083
|
|
|
2505
|
-
if (
|
|
2506
|
-
/** @type {
|
|
2507
|
-
|
|
4084
|
+
if (is_style && /** @type {AST.JSXStyleElement} */ (node).closingElement) {
|
|
4085
|
+
const closing = /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
|
|
4086
|
+
/** @type {AST.JSXStyleElement} */ (node).closingElement
|
|
2508
4087
|
);
|
|
4088
|
+
return this.finishNodeAt(node, node.type, closing.end, closing.loc.end);
|
|
2509
4089
|
}
|
|
2510
4090
|
|
|
2511
|
-
this.finishNode(
|
|
2512
|
-
return element;
|
|
4091
|
+
return this.finishNode(node, node.type);
|
|
2513
4092
|
}
|
|
2514
4093
|
|
|
2515
4094
|
/**
|
|
2516
4095
|
* @type {Parse.Parser['parseTemplateBody']}
|
|
2517
4096
|
*/
|
|
2518
4097
|
parseTemplateBody(body) {
|
|
2519
|
-
const
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
4098
|
+
const current_template_node = this.#currentNativeTemplateNode();
|
|
4099
|
+
if (!current_template_node) return;
|
|
4100
|
+
// Outside a `@{ … }` block every element/fragment body is plain JSX (§2,
|
|
4101
|
+
// §5). There is no script section and no `---` fence to infer — text is
|
|
4102
|
+
// text, and setup code lives only inside a code block.
|
|
4103
|
+
current_template_node.metadata ??= { path: [] };
|
|
4104
|
+
current_template_node.metadata.templateMode = 'template';
|
|
4105
|
+
|
|
4106
|
+
// `@{ … }` code block as element/fragment content (§2 rule 1). Sibling
|
|
4107
|
+
// code blocks are allowed, so this is not gated on an empty body;
|
|
4108
|
+
// reposition onto the `@` if leading whitespace was tokenized ahead of it.
|
|
4109
|
+
if (this.#atCodeBlockStart()) {
|
|
4110
|
+
const at_index = skip_whitespace_from(this.input, this.start);
|
|
4111
|
+
if (this.start !== at_index) {
|
|
4112
|
+
const loc = acorn.getLineInfo(this.input, at_index);
|
|
4113
|
+
this.pos = at_index;
|
|
4114
|
+
this.start = at_index;
|
|
4115
|
+
this.startLoc = new acorn.Position(loc.line, loc.column);
|
|
4116
|
+
this.curLine = loc.line;
|
|
4117
|
+
this.lineStart = at_index - loc.column;
|
|
2530
4118
|
}
|
|
2531
|
-
|
|
2532
|
-
this.next();
|
|
4119
|
+
body.push(/** @type {any} */ (this.#parseCodeBlock()));
|
|
2533
4120
|
this.parseTemplateBody(body);
|
|
2534
4121
|
return;
|
|
2535
4122
|
}
|
|
2536
4123
|
|
|
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
4124
|
if (this.type === tt.braceL) {
|
|
2564
4125
|
body.push(this.#parseNativeTemplateExpressionContainer());
|
|
2565
|
-
} else if (
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
4126
|
+
} else if (this.type === tstt.jsxText) {
|
|
4127
|
+
// A nested element with its own body can leak a JSX expression context,
|
|
4128
|
+
// so the whitespace after its closing tag is mis-tokenized as a stale
|
|
4129
|
+
// text token whose start was advanced onto the following `<`. Real JSX
|
|
4130
|
+
// text never starts at `<`, so drop the leaked context and re-read the
|
|
4131
|
+
// tag instead of emitting an empty node.
|
|
4132
|
+
if (this.input.charCodeAt(this.start) === CharCode.lessThan) {
|
|
4133
|
+
while (this.curContext() === tstc.tc_expr) {
|
|
4134
|
+
this.context.pop();
|
|
4135
|
+
}
|
|
4136
|
+
this.pos = this.start;
|
|
4137
|
+
this.exprAllowed = true;
|
|
4138
|
+
this.next();
|
|
4139
|
+
this.parseTemplateBody(body);
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
const text = this.#parseTemplateRawText();
|
|
4143
|
+
if (this.#shouldKeepTemplateTextNode(text)) {
|
|
4144
|
+
body.push(text);
|
|
4145
|
+
}
|
|
4146
|
+
} else if (this.#isJSXControlFlowDirectiveStart()) {
|
|
4147
|
+
body.push(this.#parseJSXControlFlowExpression());
|
|
2570
4148
|
} else if (this.type === tt.braceR) {
|
|
2571
4149
|
// Leaving a native template body. We may still be in TSX/JSX tokenization
|
|
2572
4150
|
// context (e.g. after parsing markup), but the closing `}` is a JS token.
|
|
@@ -2578,8 +4156,7 @@ export function TSRXPlugin(config) {
|
|
|
2578
4156
|
return;
|
|
2579
4157
|
} else if (
|
|
2580
4158
|
this.type === tstt.jsxTagStart ||
|
|
2581
|
-
|
|
2582
|
-
this.input.charCodeAt(this.start + 1) === CharCode.slash)
|
|
4159
|
+
this.input.charCodeAt(this.start) === CharCode.lessThan
|
|
2583
4160
|
) {
|
|
2584
4161
|
const startPos = this.start;
|
|
2585
4162
|
const startLoc = this.startLoc;
|
|
@@ -2600,20 +4177,20 @@ export function TSRXPlugin(config) {
|
|
|
2600
4177
|
// Consume '/'
|
|
2601
4178
|
this.next();
|
|
2602
4179
|
|
|
2603
|
-
|
|
2604
|
-
|
|
4180
|
+
let closingElement;
|
|
4181
|
+
this.#closingNativeTemplateNode = true;
|
|
4182
|
+
try {
|
|
4183
|
+
closingElement = /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
|
|
2605
4184
|
this.jsx_parseClosingElementAt(startPos, startLoc)
|
|
2606
4185
|
);
|
|
4186
|
+
} finally {
|
|
4187
|
+
this.#closingNativeTemplateNode = false;
|
|
4188
|
+
}
|
|
2607
4189
|
this.exprAllowed = false;
|
|
2608
4190
|
|
|
2609
4191
|
// 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
|
-
) {
|
|
4192
|
+
const currentElement = /** @type {any} */ (this.#path[this.#path.length - 1]);
|
|
4193
|
+
if (!this.#isNativeTemplateNode(currentElement)) {
|
|
2617
4194
|
this.raise(this.start, 'Unexpected closing tag');
|
|
2618
4195
|
}
|
|
2619
4196
|
|
|
@@ -2622,21 +4199,17 @@ export function TSRXPlugin(config) {
|
|
|
2622
4199
|
/** @type {string | null} */
|
|
2623
4200
|
let closingTagName;
|
|
2624
4201
|
|
|
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') {
|
|
4202
|
+
if (currentElement.type === 'JSXFragment') {
|
|
2632
4203
|
openingTagName = '';
|
|
2633
|
-
closingTagName =
|
|
2634
|
-
|
|
4204
|
+
closingTagName = !closingElement.name
|
|
4205
|
+
? ''
|
|
4206
|
+
: closingElement.name.type === 'JSXNamespacedName'
|
|
2635
4207
|
? closingElement.name.namespace.name + ':' + closingElement.name.name.name
|
|
2636
4208
|
: this.getElementName(closingElement.name);
|
|
2637
4209
|
} else {
|
|
2638
|
-
|
|
2639
|
-
|
|
4210
|
+
openingTagName = currentElement.openingElement?.name
|
|
4211
|
+
? this.getElementName(currentElement.openingElement.name)
|
|
4212
|
+
: null;
|
|
2640
4213
|
closingTagName = closingElement.name
|
|
2641
4214
|
? closingElement.name?.type === 'JSXNamespacedName'
|
|
2642
4215
|
? closingElement.name.namespace.name + ':' + closingElement.name.name.name
|
|
@@ -2645,6 +4218,24 @@ export function TSRXPlugin(config) {
|
|
|
2645
4218
|
}
|
|
2646
4219
|
|
|
2647
4220
|
if (openingTagName !== closingTagName) {
|
|
4221
|
+
// A closing tag that matches no open element on the path is not a
|
|
4222
|
+
// mismatch we can recover from by marking ancestors unclosed — it is
|
|
4223
|
+
// simply an unexpected closing tag (e.g. `<div></span>`).
|
|
4224
|
+
const normalized_closing_name = closingTagName ?? '';
|
|
4225
|
+
const matches_open_element = this.#path.some((node) => {
|
|
4226
|
+
const elem = /** @type {any} */ (node);
|
|
4227
|
+
if (!this.#isNativeTemplateNode(elem)) return false;
|
|
4228
|
+
const elemName =
|
|
4229
|
+
elem.type === 'JSXFragment'
|
|
4230
|
+
? ''
|
|
4231
|
+
: elem.openingElement?.name
|
|
4232
|
+
? this.getElementName(elem.openingElement.name)
|
|
4233
|
+
: null;
|
|
4234
|
+
return elemName === normalized_closing_name;
|
|
4235
|
+
});
|
|
4236
|
+
if (!matches_open_element && this.#collect) {
|
|
4237
|
+
this.raise(closingElement.start, 'Unexpected closing tag');
|
|
4238
|
+
}
|
|
2648
4239
|
// this will throw if not collecting errors
|
|
2649
4240
|
this.#report_broken_markup_error(
|
|
2650
4241
|
closingElement.start,
|
|
@@ -2653,25 +4244,19 @@ export function TSRXPlugin(config) {
|
|
|
2653
4244
|
);
|
|
2654
4245
|
// Loop through all unclosed elements on the stack
|
|
2655
4246
|
while (this.#path.length > 0) {
|
|
2656
|
-
const elem = this.#path[this.#path.length - 1];
|
|
4247
|
+
const elem = /** @type {any} */ (this.#path[this.#path.length - 1]);
|
|
2657
4248
|
|
|
2658
4249
|
// Stop at non-template boundaries.
|
|
2659
|
-
if (
|
|
2660
|
-
elem.type !== 'Element' &&
|
|
2661
|
-
elem.type !== 'TsrxFragment' &&
|
|
2662
|
-
elem.type !== 'TsxCompat'
|
|
2663
|
-
) {
|
|
4250
|
+
if (!this.#isNativeTemplateNode(elem)) {
|
|
2664
4251
|
break;
|
|
2665
4252
|
}
|
|
2666
4253
|
|
|
2667
4254
|
const elemName =
|
|
2668
|
-
elem.type === '
|
|
2669
|
-
? '
|
|
2670
|
-
: elem.
|
|
2671
|
-
?
|
|
2672
|
-
:
|
|
2673
|
-
? this.getElementName(elem.id)
|
|
2674
|
-
: null;
|
|
4255
|
+
elem.type === 'JSXFragment'
|
|
4256
|
+
? ''
|
|
4257
|
+
: elem.openingElement?.name
|
|
4258
|
+
? this.getElementName(elem.openingElement.name)
|
|
4259
|
+
: null;
|
|
2675
4260
|
|
|
2676
4261
|
// Found matching opening tag
|
|
2677
4262
|
if (elemName === closingTagName) {
|
|
@@ -2681,28 +4266,31 @@ export function TSRXPlugin(config) {
|
|
|
2681
4266
|
// Mark as unclosed and adjust location
|
|
2682
4267
|
elem.unclosed = true;
|
|
2683
4268
|
/** @type {AST.NodeWithLocation} */ (elem).loc.end = {
|
|
2684
|
-
.../** @type {AST.SourceLocation} */ (
|
|
4269
|
+
.../** @type {AST.SourceLocation} */ (
|
|
4270
|
+
elem.type === 'JSXFragment' ? elem.openingFragment.loc : elem.openingElement.loc
|
|
4271
|
+
).end,
|
|
2685
4272
|
};
|
|
2686
|
-
elem.end =
|
|
4273
|
+
elem.end =
|
|
4274
|
+
elem.type === 'JSXFragment' ? elem.openingFragment.end : elem.openingElement.end;
|
|
2687
4275
|
|
|
2688
4276
|
this.#path.pop(); // Remove from stack
|
|
2689
4277
|
}
|
|
2690
4278
|
}
|
|
2691
4279
|
|
|
2692
|
-
const elementToClose = this.#path[this.#path.length - 1];
|
|
2693
|
-
if (
|
|
2694
|
-
elementToClose &&
|
|
2695
|
-
(elementToClose.type === 'Element' || elementToClose.type === 'TsrxFragment')
|
|
2696
|
-
) {
|
|
4280
|
+
const elementToClose = /** @type {any} */ (this.#path[this.#path.length - 1]);
|
|
4281
|
+
if (this.#isNativeTemplateNode(elementToClose)) {
|
|
2697
4282
|
const elementToCloseName =
|
|
2698
|
-
elementToClose.type === '
|
|
4283
|
+
elementToClose.type === 'JSXFragment'
|
|
2699
4284
|
? ''
|
|
2700
|
-
:
|
|
2701
|
-
? this.getElementName(
|
|
4285
|
+
: elementToClose.openingElement?.name
|
|
4286
|
+
? this.getElementName(elementToClose.openingElement.name)
|
|
2702
4287
|
: null;
|
|
2703
4288
|
if (elementToCloseName === closingTagName) {
|
|
2704
|
-
|
|
2705
|
-
closingElement;
|
|
4289
|
+
if (elementToClose.type === 'JSXFragment') {
|
|
4290
|
+
elementToClose.closingFragment = this.#toClosingFragment(closingElement);
|
|
4291
|
+
} else {
|
|
4292
|
+
elementToClose.closingElement = closingElement;
|
|
4293
|
+
}
|
|
2706
4294
|
}
|
|
2707
4295
|
}
|
|
2708
4296
|
|
|
@@ -2714,16 +4302,12 @@ export function TSRXPlugin(config) {
|
|
|
2714
4302
|
if (node !== null) {
|
|
2715
4303
|
body.push(node);
|
|
2716
4304
|
}
|
|
4305
|
+
} else if (this.type === tt.eof) {
|
|
4306
|
+
return;
|
|
2717
4307
|
} 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();
|
|
4308
|
+
const text = this.#parseTemplateRawText();
|
|
4309
|
+
if (this.#shouldKeepTemplateTextNode(text)) {
|
|
4310
|
+
body.push(text);
|
|
2727
4311
|
}
|
|
2728
4312
|
}
|
|
2729
4313
|
|
|
@@ -2802,16 +4386,20 @@ export function TSRXPlugin(config) {
|
|
|
2802
4386
|
this.type === tt.braceL &&
|
|
2803
4387
|
this.context.some((c) => c === tstc.tc_expr)
|
|
2804
4388
|
) {
|
|
2805
|
-
return /** @type {ESTreeJSX.
|
|
2806
|
-
|
|
4389
|
+
return /** @type {ESTreeJSX.JSXExpressionContainer} */ (
|
|
4390
|
+
this.#parseNativeTemplateExpressionContainer()
|
|
2807
4391
|
);
|
|
2808
4392
|
}
|
|
2809
4393
|
|
|
2810
4394
|
if (this.type === tstt.jsxTagStart) {
|
|
2811
|
-
this
|
|
2812
|
-
|
|
2813
|
-
|
|
4395
|
+
if (this.#forceScriptJSXElementDepth > 0) {
|
|
4396
|
+
return /** @type {AST.Statement} */ (
|
|
4397
|
+
/** @type {unknown} */ (super.parseStatement(context, topLevel, exports))
|
|
4398
|
+
);
|
|
2814
4399
|
}
|
|
4400
|
+
|
|
4401
|
+
this.next();
|
|
4402
|
+
if (this.value === '/') this.unexpected();
|
|
2815
4403
|
const node = this.parseElement();
|
|
2816
4404
|
|
|
2817
4405
|
if (!node) {
|
|
@@ -2819,7 +4407,7 @@ export function TSRXPlugin(config) {
|
|
|
2819
4407
|
}
|
|
2820
4408
|
if (
|
|
2821
4409
|
this.#functionBodyDepth > 0 &&
|
|
2822
|
-
node.type === '
|
|
4410
|
+
node.type === 'JSXFragment' &&
|
|
2823
4411
|
this.curContext() === b_stat
|
|
2824
4412
|
) {
|
|
2825
4413
|
this.context.pop();
|
|
@@ -2833,19 +4421,6 @@ export function TSRXPlugin(config) {
|
|
|
2833
4421
|
return node;
|
|
2834
4422
|
}
|
|
2835
4423
|
|
|
2836
|
-
if (
|
|
2837
|
-
this.#functionBodyDepth === 0 &&
|
|
2838
|
-
this.type === tt.string &&
|
|
2839
|
-
this.input.charCodeAt(this.start) === CharCode.doubleQuote &&
|
|
2840
|
-
(this.#path.at(-1)?.type === 'Element' || this.#path.at(-1)?.type === 'TsrxFragment')
|
|
2841
|
-
) {
|
|
2842
|
-
this.pos = this.start;
|
|
2843
|
-
this.#readDoubleQuotedTextChildToken();
|
|
2844
|
-
const node = this.parseDoubleQuotedTextChild();
|
|
2845
|
-
this.semicolon();
|
|
2846
|
-
return node;
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
4424
|
// &[ or &{ at statement level — lazy destructuring assignment
|
|
2850
4425
|
// e.g., &[data] = track(0); or &{x, y} = obj;
|
|
2851
4426
|
if (this.type === tt.bitwiseAND) {
|
|
@@ -2886,30 +4461,28 @@ export function TSRXPlugin(config) {
|
|
|
2886
4461
|
parseBlock(createNewLexicalScope, node, exitStrict) {
|
|
2887
4462
|
const parent = this.#path.at(-1);
|
|
2888
4463
|
|
|
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;
|
|
4464
|
+
if (this.#isNativeTemplateNode(parent) && this.#templateControlFlowBlockDepth > 0) {
|
|
4465
|
+
this.#templateControlFlowBlockDepth--;
|
|
4466
|
+
try {
|
|
4467
|
+
return this.#parseTemplateControlFlowBlock(createNewLexicalScope, node, exitStrict);
|
|
4468
|
+
} finally {
|
|
4469
|
+
this.#templateControlFlowBlockDepth++;
|
|
2908
4470
|
}
|
|
2909
|
-
|
|
4471
|
+
}
|
|
2910
4472
|
|
|
2911
|
-
|
|
2912
|
-
|
|
4473
|
+
if (this.#functionBodyDepth > 0 && this.#isNativeTemplateNode(parent)) {
|
|
4474
|
+
let pushed_statement_context = false;
|
|
4475
|
+
if (this.curContext() !== b_stat) {
|
|
4476
|
+
this.context.push(b_stat);
|
|
4477
|
+
pushed_statement_context = true;
|
|
4478
|
+
}
|
|
4479
|
+
try {
|
|
4480
|
+
return super.parseBlock(createNewLexicalScope, node, exitStrict);
|
|
4481
|
+
} finally {
|
|
4482
|
+
if (pushed_statement_context && this.curContext() === b_stat) {
|
|
4483
|
+
this.context.pop();
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
2913
4486
|
}
|
|
2914
4487
|
|
|
2915
4488
|
return super.parseBlock(createNewLexicalScope, node, exitStrict);
|