@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/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?.type === 'Element' || node?.type === 'TsrxFragment' || node?.type === 'TsxCompat'
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[]} children
442
+ * @param {AST.Node | undefined} node
443
+ * @param {string} name
350
444
  */
351
- #reportDynamicJsxElementsInTsx(children) {
352
- for (const child of children) {
353
- if (child?.type === 'JSXElement') {
354
- const name = child.openingElement?.name;
355
- const is_dynamic_name =
356
- (name?.type === 'JSXIdentifier' && name.tracked) ||
357
- (name?.type === 'JSXMemberExpression' &&
358
- name.object.type === 'JSXIdentifier' &&
359
- name.object.tracked);
360
- if (is_dynamic_name) {
361
- this.#report_recoverable_error_range(
362
- /** @type {AST.NodeWithLocation} */ (name).start ?? child.start,
363
- /** @type {AST.NodeWithLocation} */ (name).end ?? child.end,
364
- DYNAMIC_ELEMENT_IN_TSX_ERROR,
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
- this.#reportDynamicJsxElementsInTsx(/** @type {AST.Node[]} */ (child.children));
368
- } else if (child?.type === 'TsxCompat') {
369
- this.#reportDynamicJsxElementsInTsx(/** @type {AST.Node[]} */ (child.children));
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
- #parseNativeTemplateExpressionContainer() {
375
- const allow_trailing_semicolon = this.#allowExpressionContainerTrailingSemicolon;
376
- this.#allowExpressionContainerTrailingSemicolon = true;
377
- let node;
378
- try {
379
- node = this.jsx_parseExpressionContainer();
380
- } finally {
381
- this.#allowExpressionContainerTrailingSemicolon = allow_trailing_semicolon;
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 /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
391
- /** @type {unknown} */ (node)
392
- );
607
+ return this.finishNodeAt(node, 'JSXText', index, endLoc);
393
608
  }
394
609
 
395
610
  /**
396
- * @param {AST.TsxCompat} island
397
- * @param {AST.Node[]} body
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
- #parseTsxIslandBody(island, body) {
400
- const tagName = `tsx:${island.kind}`;
401
-
402
- this.exprAllowed = true;
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
- while (true) {
405
- if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
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
- if (this.#isAtTsxIslandClosing()) {
420
- this.exprAllowed = false;
421
- return;
422
- }
628
+ const first = this.input.charCodeAt(index);
423
629
 
424
- if (this.type === tt.braceL) {
425
- body.push(this.#parseTsxIslandExpressionContainer());
426
- } else if (this.type === tstt.jsxTagStart) {
427
- body.push(super.jsx_parseElement());
428
- } else {
429
- const node = this.#parseTsxIslandText();
430
- if (node) {
431
- body.push(node);
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
- #parseTsxIslandExpressionContainer() {
440
- this.#tsxIslandExpressionDepth++;
441
- try {
442
- if (!this.#isAtReservedTemplateExpressionContainer()) {
443
- return this.jsx_parseExpressionContainer();
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
- const node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
447
- this.next();
448
- this.next();
449
- const expression = /** @type {AST.Expression | ESTreeJSX.JSXEmptyExpression} */ (
450
- /** @type {unknown} */ (this.parseElement())
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
- #isAtReservedTemplateExpressionContainer() {
464
- if (this.type !== tt.braceL) {
465
- return false;
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
- let index = this.start + 1;
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.space ||
473
- ch === CharCode.tab ||
474
- ch === CharCode.lineFeed ||
475
- ch === CharCode.carriageReturn
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
- if (this.input.charCodeAt(index) !== CharCode.lessThan) {
484
- return false;
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
- return this.#isReservedTemplateTagNameStart(index + 1);
1791
+ const text = this.#parseJSXSwitchCaseRawText();
1792
+ if (!isWhitespaceTextNode(text)) {
1793
+ consequent.push(text);
1794
+ }
488
1795
  }
489
1796
 
490
1797
  /**
491
- * @param {number} index
1798
+ * @param {ESTreeJSX.JSXOpeningElement} openingElement
1799
+ * @returns {ESTreeJSX.JSXOpeningFragment}
492
1800
  */
493
- #isReservedTemplateTagNameStart(index) {
494
- return this.input.startsWith('tsx:', index);
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
- #isAtTsxIslandClosing() {
500
- return this.input.slice(this.pos, this.pos + 5) === '/tsx:';
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
- #parseTsxIslandText() {
504
- const start = this.start;
505
- this.pos = start;
506
- let text = '';
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
- while (this.pos < this.input.length) {
509
- const ch = this.input.charCodeAt(this.pos);
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
- // Stop at opening tag, expression, or the template-closing brace
512
- if (ch === CharCode.lessThan || ch === CharCode.openBrace || ch === CharCode.closeBrace) {
513
- break;
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
- text += this.input[this.pos];
517
- this.pos++;
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
- if (!text) {
521
- return null;
522
- }
1919
+ node.css = content;
1920
+ node.children = [parsedCss];
1921
+ }
523
1922
 
524
- return /** @type {ESTreeJSX.JSXText} */ ({
525
- type: 'JSXText',
526
- value: text,
527
- raw: text,
528
- start,
529
- end: this.pos,
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
- #popTsxTokenContextBeforeTemplateExpressionChild() {
1940
+ #popTemplateTokenContextBeforeExpressionChild() {
534
1941
  let index = this.pos;
535
1942
  let has_newline = false;
536
1943
 
537
- // Text-only compat islands can leave the tokenizer in JSX text mode.
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 {AST.TsrxFragment | AST.TsxCompat} node
2041
+ * @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
635
2042
  * @returns {boolean}
636
2043
  */
637
2044
  #hasDirectStatementChild(node) {
638
- return node.children?.some(
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 {AST.TsrxFragment | AST.TsxCompat} node
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.#report_recoverable_error(
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 || container.type !== 'Element') {
2419
+ if (!this.#isNativeTemplateNode(container)) {
1068
2420
  return null;
1069
2421
  }
1070
2422
 
1071
- const children = Array.isArray(container.children) ? container.children : [];
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
- if (code === CharCode.lessThan && looks_like_generic_arrow(this.input, this.pos)) {
1119
- ++this.pos;
1120
- return this.finishToken(tt.relational, '<');
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 (this.context.length > 0 && this.curContext() !== tstc.tc_oTag) {
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.exprAllowed = false;
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
- } else {
1150
- this.#allowDoubleQuotedTextChildAfterBrace = false;
2572
+ this.exprAllowed = false;
1151
2573
  }
1152
2574
 
1153
- if (code !== CharCode.lessThan) {
1154
- this.#allowTagStartAfterDoubleQuotedText = false;
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 after: identifiers, closing parens, 'new' keyword
1168
- // For example: Array<T>, func<T>(), new Map<K,V>(), method<T>()
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
- let lookback = this.pos - 1;
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
- // If preceded by identifier character (letter, digit, _, $) or closing paren,
1187
- // this is likely TypeScript generics, not JSX
1188
- const isIdentifierChar =
1189
- (prevChar >= CharCode.uppercaseA && prevChar <= CharCode.uppercaseZ) ||
1190
- (prevChar >= CharCode.lowercaseA && prevChar <= CharCode.lowercaseZ) ||
1191
- (prevChar >= CharCode.digit0 && prevChar <= CharCode.digit9) ||
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
- node.body = /** @type {AST.BlockStatement} */ (this.parseStatement('for'));
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.next();
3079
+ this.#jsxExpressionContainerDepth++;
3080
+ try {
3081
+ this.next();
1628
3082
 
1629
- node.expression =
1630
- this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
1631
- if (this.#allowExpressionContainerTrailingSemicolon && this.type === tt.semi) {
1632
- if (this.#collect) {
1633
- this.#report_recoverable_error(
1634
- this.start,
1635
- 'TSRX expression containers do not use semicolons. Remove this semicolon.',
1636
- DIAGNOSTIC_CODES.TEMPLATE_EXPRESSION_TRAILING_SEMICOLON,
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
- this.next();
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
- /** @type {AST.TSRXAttribute | ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} */ (
1704
- this.startNode()
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
- const current_template_node = this.#path.findLast(
1709
- (n) => n.type === 'Element' || n.type === 'TsrxFragment' || n.type === 'TsxCompat',
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
- this.expect(tt.braceR);
1716
- return this.finishNode(node, 'JSXSpreadAttribute');
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, 'SpreadAttribute');
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
- /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
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, 'SpreadAttribute');
3228
+ return this.finishNode(node, 'JSXSpreadAttribute');
1731
3229
  } else {
1732
- const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
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.finishNode(id, 'Identifier');
1735
- /** @type {AST.Attribute} */ (node).name = id;
1736
- /** @type {AST.Attribute} */ (node).value = id;
1737
- /** @type {AST.Attribute} */ (node).shorthand = true; // Mark as shorthand since name and value are the same
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, 'Attribute');
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.tracked = true;
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
- const current_template_node = this.#path.findLast(
1943
- (n) => n.type === 'Element' || n.type === 'TsrxFragment' || n.type === 'TsxCompat',
1944
- );
1945
- if (current_template_node?.type === 'TsxCompat') {
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.closeBrace &&
2072
- (this.#path.length === 0 ||
2073
- this.#path.at(-1)?.type === 'Element' ||
2074
- this.#path.at(-1)?.type === 'TsrxFragment')
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 parse tags and bare fragments as native TSRX
2112
- * by default. Explicit <tsx:*> islands keep ordinary TSX parsing for
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
- // Current token is jsxTagStart, this.end is position after '<'
2118
- const tag_name_start = this.end;
2119
- const current_template_node = this.#path.findLast(
2120
- (n) => n.type === 'Element' || n.type === 'TsrxFragment' || n.type === 'TsxCompat',
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
- // Inside tsx/tsx:*, let acorn-jsx handle regular TSX tags normally.
2132
- return super.jsx_parseElement();
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
- if (!inside_tsx_island) {
2140
- this.#popTokenContextsAfterTemplateExpressionElement(
2141
- /** @type {AST.TsrxFragment | AST.TsxCompat} */ (/** @type {unknown} */ (parsed)),
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
- } else if (this.type === tt.braceR && this.curContext() === tstc.tc_expr) {
2144
- if (this.#tsxIslandExpressionDepth === 0) {
2145
- // Acorn still owns the surrounding JSX expression container.
2146
- // Keep a block-expression context for its closing `}` so the
2147
- // parent TSX tag continues tokenizing as JSX afterward.
2148
- this.context.push(b_expr);
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
- return parsed;
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
- const inside_head = this.#path.findLast(
2159
- (n) => n.type === 'Element' && n.id && n.id.type === 'Identifier' && n.id.name === 'head',
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 element = /** @type {AST.Element | AST.TsrxFragment | AST.TsxCompat} */ (
2166
- this.startNode()
2167
- );
2168
- element.start = start;
2169
- /** @type {AST.NodeWithLocation} */ (element).loc.start = position;
2170
- element.metadata = { path: [] };
2171
- element.children = [];
2172
- element.type = 'Element';
2173
- this.#path.push(element);
2174
-
2175
- const open = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
2176
- this.jsx_parseOpeningElementAt(start, position)
2177
- );
2178
-
2179
- // Always attach the concrete opening element node for accurate source mapping
2180
- element.openingElement = open;
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 is_tsx_compat =
2185
- !is_fragment &&
2186
- open.name.type === 'JSXNamespacedName' &&
2187
- open.name.namespace.name === 'tsx';
2188
- if (is_tsx_compat) {
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
- /** @type {AST.TsxCompat} */ (element).type = 'TsxCompat';
2191
- /** @type {AST.TsxCompat} */ (element).kind = namespace_node.name.name; // e.g., "react" from "tsx:react"
2192
-
2193
- if (open.selfClosing) {
2194
- const tagName = namespace_node.namespace.name + ':' + namespace_node.name.name;
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
- for (const attr of open.attributes) {
2207
- if (attr.type === 'JSXAttribute') {
2208
- /** @type {AST.Attribute} */ (/** @type {unknown} */ (attr)).type = 'Attribute';
2209
- if (attr.name.type === 'JSXIdentifier') {
2210
- /** @type {AST.Identifier} */ (/** @type {unknown} */ (attr.name)).type =
2211
- 'Identifier';
2212
- }
2213
- if (attr.value !== null) {
2214
- if (attr.value.type === 'JSXExpressionContainer') {
2215
- const expression = attr.value.expression;
2216
- if (expression.type === 'Literal') {
2217
- expression.was_expression = true;
2218
- }
2219
- // @ts-ignore intentional AST node conversion from JSX to Ripple
2220
- /** @type {ESTreeJSX.JSXAttribute} */ (attr).value =
2221
- /** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (expression);
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 (element.metadata.commentContainerId === undefined) {
2242
- element.metadata.commentContainerId = ++this.#commentContextId;
4030
+ if (node.metadata.commentContainerId === undefined) {
4031
+ node.metadata.commentContainerId = ++this.#commentContextId;
2243
4032
  }
2244
4033
 
2245
- if (element.selfClosing) {
2246
- this.#path.pop();
4034
+ this.#path.push(node);
2247
4035
 
2248
- if (this.type.label === '</>/<=/>=') {
2249
- this.pos--;
2250
- this.next();
2251
- }
2252
- } else if (is_fragment) {
2253
- this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
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.pop();
2259
-
2260
- if (!element.unclosed) {
2261
- const raise_error = () => {
2262
- this.raise(this.start, `Expected closing tag '</>'`);
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
- this.next();
2266
- if (this.value !== '/') {
2267
- raise_error();
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
- // Ensure we escape JSX <tag></tag> context
2496
- const curContext = this.curContext();
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 (curContext === tstc.tc_expr && !insideTemplate) {
2501
- this.context.pop();
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 (element.closingElement && !is_tsx_compat && element.closingElement.name) {
2506
- /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
2507
- element.closingElement.name,
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(element, element.type);
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 inside_func =
2520
- this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
2521
- const current_template_node = this.#path.findLast(
2522
- (n) => n.type === 'Element' || n.type === 'TsrxFragment' || n.type === 'TsxCompat',
2523
- );
2524
- const inside_tsx_island =
2525
- current_template_node?.type === 'TsxCompat' ? current_template_node : null;
2526
-
2527
- if (current_template_node?.type === 'TsrxFragment' && this.type === tstt.jsxText) {
2528
- while (this.curContext() === tstc.tc_expr) {
2529
- this.context.pop();
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
- this.pos = this.start;
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
- this.type === tt.string &&
2567
- this.input.charCodeAt(this.start) === CharCode.doubleQuote
2568
- ) {
2569
- body.push(this.parseDoubleQuotedTextChild());
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
- (this.input.charCodeAt(this.start) === CharCode.lessThan &&
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
- const closingElement =
2604
- /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
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 === 'TsxCompat') {
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
- closingElement.name?.type === 'JSXNamespacedName'
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
- // Regular Element node (or fragment)
2639
- openingTagName = currentElement.id ? this.getElementName(currentElement.id) : null;
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 === 'TsxCompat'
2669
- ? 'tsx:' + elem.kind
2670
- : elem.type === 'TsrxFragment'
2671
- ? ''
2672
- : elem.id
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} */ (elem.openingElement.loc).end,
4269
+ .../** @type {AST.SourceLocation} */ (
4270
+ elem.type === 'JSXFragment' ? elem.openingFragment.loc : elem.openingElement.loc
4271
+ ).end,
2685
4272
  };
2686
- elem.end = elem.openingElement.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 === 'TsrxFragment'
4283
+ elementToClose.type === 'JSXFragment'
2699
4284
  ? ''
2700
- : /** @type {AST.Element} */ (elementToClose).id
2701
- ? this.getElementName(/** @type {AST.Element} */ (elementToClose).id)
4285
+ : elementToClose.openingElement?.name
4286
+ ? this.getElementName(elementToClose.openingElement.name)
2702
4287
  : null;
2703
4288
  if (elementToCloseName === closingTagName) {
2704
- /** @type {AST.Element | AST.TsrxFragment} */ (elementToClose).closingElement =
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
- skipWhitespace(this);
2719
- const node = this.parseStatement(null);
2720
- this.#report_invalid_template_return_statements(node);
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.JSXEmptyExpression | AST.TSRXExpression | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2806
- /** @type {unknown} */ (this.#parseNativeTemplateExpressionContainer())
4389
+ return /** @type {ESTreeJSX.JSXExpressionContainer} */ (
4390
+ this.#parseNativeTemplateExpressionContainer()
2807
4391
  );
2808
4392
  }
2809
4393
 
2810
4394
  if (this.type === tstt.jsxTagStart) {
2811
- this.next();
2812
- if (this.value === '/') {
2813
- this.unexpected();
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 === 'TsrxFragment' &&
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
- // Inside a JS function body, parse `{...}` as a regular block statement,
2890
- // even if the nearest `#path` entry is a native template — we're in a
2891
- // nested function callable, not in a template.
2892
- if (
2893
- this.#functionBodyDepth === 0 &&
2894
- (parent?.type === 'Element' || parent?.type === 'TsrxFragment')
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
- this.exprAllowed = true;
4471
+ }
2910
4472
 
2911
- this.next();
2912
- return this.finishNode(node, 'BlockStatement');
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);