@tsrx/core 0.1.19 → 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,238 +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' ||
345
- node?.type === 'Tsx' ||
346
- node?.type === 'Tsrx' ||
347
- 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))
348
438
  );
349
439
  }
350
440
 
351
441
  /**
352
- * @param {AST.Node[]} children
442
+ * @param {AST.Node | undefined} node
443
+ * @param {string} name
353
444
  */
354
- #reportDynamicJsxElementsInTsx(children) {
355
- for (const child of children) {
356
- if (child?.type === 'JSXElement') {
357
- const name = child.openingElement?.name;
358
- const is_dynamic_name =
359
- (name?.type === 'JSXIdentifier' && name.tracked) ||
360
- (name?.type === 'JSXMemberExpression' &&
361
- name.object.type === 'JSXIdentifier' &&
362
- name.object.tracked);
363
- if (is_dynamic_name) {
364
- this.#report_recoverable_error_range(
365
- /** @type {AST.NodeWithLocation} */ (name).start ?? child.start,
366
- /** @type {AST.NodeWithLocation} */ (name).end ?? child.end,
367
- DYNAMIC_ELEMENT_IN_TSX_ERROR,
368
- );
369
- }
370
- this.#reportDynamicJsxElementsInTsx(/** @type {AST.Node[]} */ (child.children));
371
- } else if (child?.type === 'Tsx' || child?.type === 'TsxCompat') {
372
- this.#reportDynamicJsxElementsInTsx(/** @type {AST.Node[]} */ (child.children));
373
- }
374
- }
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
+ );
375
451
  }
376
452
 
377
- #parseNativeTemplateExpressionContainer() {
378
- const allow_trailing_semicolon = this.#allowExpressionContainerTrailingSemicolon;
379
- this.#allowExpressionContainerTrailingSemicolon = true;
380
- let node;
381
- try {
382
- node = this.jsx_parseExpressionContainer();
383
- } finally {
384
- this.#allowExpressionContainerTrailingSemicolon = allow_trailing_semicolon;
385
- }
386
- // Keep JSXEmptyExpression as-is (for prettier to handle comments)
387
- // but convert other expressions to native TSRX child nodes.
388
- if (node.expression.type !== 'JSXEmptyExpression') {
389
- /** @type {AST.TSRXExpression | AST.TextNode} */ (/** @type {unknown} */ (node)).type =
390
- 'TSRXExpression';
391
- }
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;
392
472
 
393
- return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
394
- /** @type {unknown} */ (node)
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
395
481
  );
396
482
  }
397
483
 
398
484
  /**
399
- * @param {AST.Tsx | AST.TsxCompat} island
400
- * @param {AST.Node[]} body
485
+ * @param {number} index
401
486
  */
402
- #parseTsxIslandBody(island, body) {
403
- const tagName =
404
- island.type === 'TsxCompat'
405
- ? `tsx:${island.kind}`
406
- : island.openingElement.name
407
- ? 'tsx'
408
- : '';
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
+ }
409
495
 
410
- this.exprAllowed = true;
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
+ }
411
510
 
412
- while (true) {
413
- if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
414
- const displayTag = tagName || '';
415
- this.#report_broken_markup_error(
416
- this.start,
417
- `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
418
- );
419
- island.unclosed = true;
420
- /** @type {AST.NodeWithLocation} */ (island).loc.end = {
421
- .../** @type {AST.SourceLocation} */ (island.openingElement.loc).end,
422
- };
423
- island.end = island.openingElement.end;
424
- return;
425
- }
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
+ }
426
521
 
427
- if (this.#isAtTsxIslandClosing(island)) {
428
- this.exprAllowed = false;
429
- return;
430
- }
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
+ }
431
540
 
432
- if (this.type === tt.braceL) {
433
- body.push(this.#parseTsxIslandExpressionContainer());
434
- } else if (this.type === tstt.jsxTagStart) {
435
- body.push(super.jsx_parseElement());
436
- } else {
437
- const node = this.#parseTsxIslandText();
438
- if (node) {
439
- body.push(node);
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++;
440
563
  }
441
- this.#popTemplateLiteralTokenContext();
442
- this.next();
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),
577
+ );
578
+ }
579
+ continue;
443
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;
589
+ }
590
+ value += this.input[index];
591
+ index++;
444
592
  }
445
- }
446
593
 
447
- #parseTsxIslandExpressionContainer() {
448
- this.#tsxIslandExpressionDepth++;
449
- try {
450
- if (!this.#isAtReservedTemplateExpressionContainer()) {
451
- return this.jsx_parseExpressionContainer();
452
- }
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);
453
598
 
454
- const node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
455
- this.next();
456
- this.next();
457
- const expression = /** @type {AST.Expression | ESTreeJSX.JSXEmptyExpression} */ (
458
- /** @type {unknown} */ (this.parseElement())
459
- );
460
- node.expression = expression;
461
- this.#popTokenContextsAfterTemplateExpressionElement(
462
- /** @type {AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (/** @type {unknown} */ (expression)),
463
- );
464
- this.expect(tt.braceR);
465
- return this.finishNode(node, 'JSXExpressionContainer');
466
- } finally {
467
- this.#tsxIslandExpressionDepth--;
599
+ if (node.raw.match(regex_newline_characters)) {
600
+ this.curLine = endLoc.line;
601
+ this.lineStart = index - endLoc.column;
602
+ }
603
+ this.pos = index;
604
+ this.#popTemplateLiteralTokenContext();
605
+ this.next();
606
+
607
+ return this.finishNodeAt(node, 'JSXText', index, endLoc);
608
+ }
609
+
610
+ /**
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
617
+ */
618
+ #shouldKeepTemplateTextNode(node) {
619
+ if (!isWhitespaceTextNode(node)) {
620
+ return true;
468
621
  }
622
+ return node.value !== '' && !regex_newline_characters.test(node.value);
469
623
  }
470
624
 
471
- #isAtReservedTemplateExpressionContainer() {
472
- if (this.type !== tt.braceL) {
625
+ #isSwitchCaseScriptStatementStart() {
626
+ let index = skip_whitespace_from(this.input, this.start);
627
+
628
+ const first = this.input.charCodeAt(index);
629
+
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
+ }
651
+ }
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
+ ) {
473
664
  return false;
474
665
  }
475
666
 
476
- let index = this.start + 1;
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;
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;
692
+ }
693
+
694
+ #switchCaseLabelStart(index = this.start) {
477
695
  while (index < this.input.length) {
478
696
  const ch = this.input.charCodeAt(index);
479
697
  if (
480
- ch === CharCode.space ||
481
- ch === CharCode.tab ||
482
- ch === CharCode.lineFeed ||
483
- ch === CharCode.carriageReturn
698
+ ch !== CharCode.space &&
699
+ ch !== CharCode.tab &&
700
+ ch !== CharCode.lineFeed &&
701
+ ch !== CharCode.carriageReturn
484
702
  ) {
485
- index++;
486
- } else {
487
703
  break;
488
704
  }
705
+ index++;
489
706
  }
490
-
491
- if (this.input.charCodeAt(index) !== CharCode.lessThan) {
492
- return false;
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;
493
721
  }
722
+ return -1;
723
+ }
494
724
 
495
- return this.#isReservedTemplateTagNameStart(index + 1);
725
+ #rewindToSwitchCaseLabel() {
726
+ const start = this.#switchCaseLabelStart();
727
+ if (start === -1) return false;
728
+ while (this.curContext() === tstc.tc_expr) {
729
+ this.context.pop();
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;
496
738
  }
497
739
 
498
740
  /**
499
741
  * @param {number} index
500
742
  */
501
- #isReservedTemplateTagNameStart(index) {
502
- const char_after_tsx = this.input.charCodeAt(index + 3);
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++;
750
+ }
751
+
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) {
503
787
  return (
504
- this.input.startsWith('tsx', index) &&
505
- (index + 3 >= this.input.length ||
506
- char_after_tsx === CharCode.greaterThan ||
507
- char_after_tsx === CharCode.slash ||
508
- char_after_tsx === CharCode.space ||
509
- char_after_tsx === CharCode.tab ||
510
- char_after_tsx === CharCode.lineFeed ||
511
- char_after_tsx === CharCode.carriageReturn ||
512
- char_after_tsx === CharCode.colon)
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
513
793
  );
514
794
  }
515
795
 
516
796
  /**
517
- * @param {AST.Tsx | AST.TsxCompat} island
797
+ * @param {number} ch
518
798
  */
519
- #isAtTsxIslandClosing(island) {
520
- if (island.type === 'TsxCompat') {
521
- return this.input.slice(this.pos, this.pos + 5) === '/tsx:';
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;
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
+
816
+ const ch = this.input.charCodeAt(index);
817
+ if (
818
+ ch === CharCode.lessThan ||
819
+ ch === CharCode.openBrace ||
820
+ ch === CharCode.closeBrace ||
821
+ ch === CharCode.at
822
+ ) {
823
+ break;
824
+ }
825
+ index++;
522
826
  }
523
827
 
524
- if (!island.openingElement.name) {
525
- return this.input.slice(this.pos, this.pos + 2) === '/>';
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;
526
848
  }
849
+ this.next();
850
+
851
+ return this.finishNodeAt(node, 'JSXText', index, endLoc);
852
+ }
527
853
 
528
- if (this.input.slice(this.pos, this.pos + 4) !== '/tsx') {
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
+ ) {
529
902
  return false;
530
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;
1767
+ }
1768
+
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;
1789
+ }
1790
+
1791
+ const text = this.#parseJSXSwitchCaseRawText();
1792
+ if (!isWhitespaceTextNode(text)) {
1793
+ consequent.push(text);
1794
+ }
1795
+ }
1796
+
1797
+ /**
1798
+ * @param {ESTreeJSX.JSXOpeningElement} openingElement
1799
+ * @returns {ESTreeJSX.JSXOpeningFragment}
1800
+ */
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;
1810
+ }
1811
+
1812
+ /**
1813
+ * @param {ESTreeJSX.JSXClosingElement} closingElement
1814
+ * @returns {ESTreeJSX.JSXClosingFragment}
1815
+ */
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;
1823
+ }
1824
+
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
+ }
1840
+
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
+ }
1846
+
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();
1886
+ }
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;
1917
+ }
531
1918
 
532
- const after = this.input.charCodeAt(this.pos + 4);
533
- return after === CharCode.greaterThan;
1919
+ node.css = content;
1920
+ node.children = [parsedCss];
534
1921
  }
535
1922
 
536
- #parseTsxIslandText() {
537
- const start = this.start;
538
- this.pos = start;
539
- let text = '';
540
-
541
- while (this.pos < this.input.length) {
542
- const ch = this.input.charCodeAt(this.pos);
543
-
544
- // Stop at opening tag, expression, or the template-closing brace
545
- if (ch === CharCode.lessThan || ch === CharCode.openBrace || ch === CharCode.closeBrace) {
546
- break;
547
- }
548
-
549
- text += this.input[this.pos];
550
- this.pos++;
551
- }
552
-
553
- if (!text) {
554
- return null;
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;
555
1936
  }
556
-
557
- return /** @type {ESTreeJSX.JSXText} */ ({
558
- type: 'JSXText',
559
- value: text,
560
- raw: text,
561
- start,
562
- end: this.pos,
563
- });
1937
+ return /** @type {ESTreeJSX.JSXExpressionContainer} */ (/** @type {unknown} */ (node));
564
1938
  }
565
1939
 
566
- #popTsxTokenContextBeforeTemplateExpressionChild() {
1940
+ #popTemplateTokenContextBeforeExpressionChild() {
567
1941
  let index = this.pos;
568
1942
  let has_newline = false;
569
1943
 
570
- // Text-only Tsx nodes can leave the tokenizer in JSX text mode.
1944
+ // JSXText-only template fragments can leave the tokenizer in JSX text mode.
571
1945
  // Only unwind it for ASI before a following TSRX `{expr}` child;
572
1946
  // fragment props like `content={<></>}` still need the JSX context.
573
1947
  while (index < this.input.length) {
@@ -664,19 +2038,27 @@ export function TSRXPlugin(config) {
664
2038
  }
665
2039
 
666
2040
  /**
667
- * @param {AST.Tsx | AST.Tsrx | AST.TsxCompat} node
2041
+ * @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
668
2042
  * @returns {boolean}
669
2043
  */
670
2044
  #hasDirectStatementChild(node) {
671
- return node.children?.some(
2045
+ const children = /** @type {AST.Node[]} */ (/** @type {unknown} */ (node.children ?? []));
2046
+ return children.some(
672
2047
  (child) => child.type.endsWith('Statement') || child.type === 'VariableDeclaration',
673
2048
  );
674
2049
  }
675
2050
 
676
2051
  /**
677
- * @param {AST.Tsx | AST.Tsrx | AST.TsxCompat} node
2052
+ * @param {ESTreeJSX.JSXElement | ESTreeJSX.JSXFragment} node
678
2053
  */
679
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
+ }
680
2062
  const ctx = this.context;
681
2063
  const ci = ctx.length - 1;
682
2064
  const top = ctx[ci];
@@ -767,71 +2149,6 @@ export function TSRXPlugin(config) {
767
2149
  }
768
2150
  }
769
2151
 
770
- #isDoubleQuotedTextChildStart() {
771
- const current_template_node = this.#path.findLast(
772
- (n) =>
773
- n.type === 'Element' || n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'TsxCompat',
774
- );
775
- if (current_template_node?.type === 'TsxCompat' || current_template_node?.type === 'Tsx') {
776
- return false;
777
- }
778
-
779
- const parent = this.#path.at(-1);
780
- if (!parent || (parent.type !== 'Element' && parent.type !== 'Tsrx')) {
781
- return false;
782
- }
783
-
784
- const context = this.curContext();
785
- if (context === tstc.tc_oTag || context === tstc.tc_cTag) {
786
- return false;
787
- }
788
-
789
- const prev = this.#previousNonWhitespaceChar();
790
- return (
791
- prev === null ||
792
- prev === CharCode.doubleQuote ||
793
- prev === CharCode.semicolon ||
794
- prev === CharCode.greaterThan ||
795
- (prev === CharCode.openBrace && this.#allowDoubleQuotedTextChildAfterBrace) ||
796
- prev === CharCode.closeBrace
797
- );
798
- }
799
-
800
- #readDoubleQuotedTextChildToken() {
801
- const start = this.pos;
802
- let out = '';
803
- this.pos++;
804
- let chunkStart = this.pos;
805
-
806
- while (this.pos < this.input.length) {
807
- const ch = this.input.charCodeAt(this.pos);
808
-
809
- if (ch === CharCode.doubleQuote) {
810
- out += this.input.slice(chunkStart, this.pos);
811
- this.pos++;
812
- return this.finishToken(tt.string, out);
813
- }
814
-
815
- if (ch === CharCode.ampersand) {
816
- out += this.input.slice(chunkStart, this.pos);
817
- out += this.jsx_readEntity();
818
- chunkStart = this.pos;
819
- continue;
820
- }
821
-
822
- if (acorn.isNewLine(ch)) {
823
- out += this.input.slice(chunkStart, this.pos);
824
- out += this.jsx_readNewLine(true);
825
- chunkStart = this.pos;
826
- continue;
827
- }
828
-
829
- this.pos++;
830
- }
831
-
832
- this.raise(start, 'Unterminated double-quoted text child');
833
- }
834
-
835
2152
  /**
836
2153
  * @param {number} position
837
2154
  * @param {number} end
@@ -922,8 +2239,9 @@ export function TSRXPlugin(config) {
922
2239
  ...node.metadata,
923
2240
  invalid_tsrx_template_return: true,
924
2241
  };
925
- this.#report_recoverable_error(
2242
+ this.#report_recoverable_error_range(
926
2243
  /** @type {AST.NodeWithLocation} */ (node).start ?? this.start,
2244
+ /** @type {AST.NodeWithLocation} */ (node).end ?? this.start + 1,
927
2245
  TSRX_RETURN_STATEMENT_ERROR,
928
2246
  DIAGNOSTIC_CODES.TEMPLATE_RETURN_STATEMENT,
929
2247
  );
@@ -1098,13 +2416,15 @@ export function TSRXPlugin(config) {
1098
2416
  }
1099
2417
 
1100
2418
  const container = this.#path[this.#path.length - 1];
1101
- if (!container || container.type !== 'Element') {
2419
+ if (!this.#isNativeTemplateNode(container)) {
1102
2420
  return null;
1103
2421
  }
1104
2422
 
1105
- const children = Array.isArray(container.children) ? container.children : [];
2423
+ const children = Array.isArray(/** @type {any} */ (container).children)
2424
+ ? /** @type {any} */ (container).children
2425
+ : [];
1106
2426
  const hasMeaningfulChildren = children.some(
1107
- (child) => child && !isWhitespaceTextNode(child),
2427
+ (/** @type {any} */ child) => child && !isWhitespaceTextNode(child),
1108
2428
  );
1109
2429
 
1110
2430
  if (hasMeaningfulChildren) {
@@ -1149,9 +2469,58 @@ export function TSRXPlugin(config) {
1149
2469
  * @type {Parse.Parser['readToken']}
1150
2470
  */
1151
2471
  readToken(code) {
1152
- if (code === CharCode.lessThan && looks_like_generic_arrow(this.input, this.pos)) {
1153
- ++this.pos;
1154
- 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
+ }
1155
2524
  }
1156
2525
  return super.readToken(code);
1157
2526
  }
@@ -1161,74 +2530,91 @@ export function TSRXPlugin(config) {
1161
2530
  * @type {Parse.Parser['getTokenFromCode']}
1162
2531
  */
1163
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
+
1164
2555
  // Callback props that return native templates without a semicolon can
1165
2556
  // leave the attribute expression context above the still-open tag. Drop
1166
2557
  // it before tokenizing `/>`, otherwise Acorn treats `/` as a regexp.
1167
2558
  if (
1168
2559
  code === CharCode.slash &&
1169
- this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan &&
1170
- this.context.includes(tstc.tc_oTag)
2560
+ this.input.charCodeAt(this.pos + 1) === CharCode.greaterThan
1171
2561
  ) {
1172
- 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
+ ) {
1173
2567
  this.context.pop();
1174
2568
  }
1175
- this.exprAllowed = false;
1176
- }
1177
- if (code === CharCode.doubleQuote) {
1178
- const is_double_quoted_text_child = this.#isDoubleQuotedTextChildStart();
1179
- this.#allowDoubleQuotedTextChildAfterBrace = false;
1180
- if (is_double_quoted_text_child) {
1181
- return this.#readDoubleQuotedTextChildToken();
2569
+ if (this.curContext() !== tstc.tc_oTag) {
2570
+ this.context.push(tstc.tc_oTag);
1182
2571
  }
1183
- } else {
1184
- this.#allowDoubleQuotedTextChildAfterBrace = false;
2572
+ this.exprAllowed = false;
1185
2573
  }
1186
2574
 
1187
- if (code !== CharCode.lessThan) {
1188
- 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));
1189
2587
  }
1190
2588
 
1191
2589
  if (code === CharCode.lessThan) {
1192
2590
  // < character
1193
2591
  const parent = this.#path.at(-1);
1194
2592
  const inNativeTemplate =
1195
- this.#functionBodyDepth === 0 &&
1196
- (parent?.type === 'Element' || parent?.type === 'Tsrx');
2593
+ this.#functionBodyDepth === 0 && this.#isNativeTemplateNode(parent);
1197
2594
  /** @type {number | null} */
1198
2595
  let prevNonWhitespaceChar = null;
2596
+ const nextChar =
2597
+ this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
1199
2598
 
1200
2599
  // Check if this could be TypeScript generics instead of JSX
1201
- // TypeScript generics appear after: identifiers, closing parens, 'new' keyword
1202
- // 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>().
1203
2602
  // This check applies everywhere, not just inside components
1204
2603
 
1205
2604
  // Look back to see what precedes the <
1206
- let lookback = this.pos - 1;
1207
-
1208
- // Skip whitespace backwards
1209
- while (lookback >= 0) {
1210
- const ch = this.input.charCodeAt(lookback);
1211
- if (ch !== CharCode.space && ch !== CharCode.tab) break; // not space or tab
1212
- lookback--;
1213
- }
2605
+ const lookback = this.#previousNonSpaceTabIndex(this.pos);
1214
2606
 
1215
2607
  // Check what character/token precedes the <
1216
2608
  if (lookback >= 0) {
1217
2609
  const prevChar = this.input.charCodeAt(lookback);
1218
2610
  prevNonWhitespaceChar = prevChar;
1219
2611
 
1220
- // If preceded by identifier character (letter, digit, _, $) or closing paren,
1221
- // this is likely TypeScript generics, not JSX
1222
- const isIdentifierChar =
1223
- (prevChar >= CharCode.uppercaseA && prevChar <= CharCode.uppercaseZ) ||
1224
- (prevChar >= CharCode.lowercaseA && prevChar <= CharCode.lowercaseZ) ||
1225
- (prevChar >= CharCode.digit0 && prevChar <= CharCode.digit9) ||
1226
- prevChar === CharCode.underscore ||
1227
- prevChar === CharCode.dollar ||
1228
- prevChar === CharCode.closeParen;
1229
-
1230
- if (isIdentifierChar) {
1231
- 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, '<');
1232
2618
  }
1233
2619
  }
1234
2620
 
@@ -1237,8 +2623,6 @@ export function TSRXPlugin(config) {
1237
2623
  // <Something>...</Something>\n\n<Child />
1238
2624
  // <head><style>...</style></head>
1239
2625
  // We only do this when '<' is in a tag-like position.
1240
- const nextChar =
1241
- this.pos + 1 < this.input.length ? this.input.charCodeAt(this.pos + 1) : -1;
1242
2626
  const isWhitespaceAfterLt =
1243
2627
  nextChar === CharCode.space ||
1244
2628
  nextChar === CharCode.tab ||
@@ -1261,6 +2645,11 @@ export function TSRXPlugin(config) {
1261
2645
  prevNonWhitespaceChar === CharCode.closeBrace ||
1262
2646
  prevNonWhitespaceChar === CharCode.greaterThan;
1263
2647
 
2648
+ if (!inNativeTemplate && this.exprAllowed && isTagLikeAfterLt) {
2649
+ ++this.pos;
2650
+ return this.finishToken(tstt.jsxTagStart);
2651
+ }
2652
+
1264
2653
  if (!inNativeTemplate && prevAllowsTagStart && isTagLikeAfterLt) {
1265
2654
  ++this.pos;
1266
2655
  return this.finishToken(tstt.jsxTagStart);
@@ -1271,13 +2660,10 @@ export function TSRXPlugin(config) {
1271
2660
  // a newline/indentation before the next '<'. This is important for inputs
1272
2661
  // like `<div />` and `</div><style>...</style>` which Prettier formats.
1273
2662
  if (
1274
- (prevNonWhitespaceChar === CharCode.doubleQuote &&
1275
- this.#allowTagStartAfterDoubleQuotedText) ||
1276
2663
  prevNonWhitespaceChar === CharCode.openBrace ||
1277
2664
  prevNonWhitespaceChar === CharCode.greaterThan
1278
2665
  ) {
1279
2666
  if (!isWhitespaceAfterLt) {
1280
- this.#allowTagStartAfterDoubleQuotedText = false;
1281
2667
  ++this.pos;
1282
2668
  return this.finishToken(tstt.jsxTagStart);
1283
2669
  }
@@ -1316,7 +2702,6 @@ export function TSRXPlugin(config) {
1316
2702
  }
1317
2703
  }
1318
2704
 
1319
- this.#allowTagStartAfterDoubleQuotedText = false;
1320
2705
  return super.getTokenFromCode(code);
1321
2706
  }
1322
2707
 
@@ -1635,7 +3020,13 @@ export function TSRXPlugin(config) {
1635
3020
  }
1636
3021
 
1637
3022
  this.expect(tt.parenR);
1638
- 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
+ }
1639
3030
  this.exitScope();
1640
3031
  this.labels.pop();
1641
3032
  return this.finishNode(node, isForIn ? 'ForInStatement' : 'ForOfStatement');
@@ -1647,6 +3038,25 @@ export function TSRXPlugin(config) {
1647
3038
  parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args) {
1648
3039
  this.#functionBodyDepth++;
1649
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
+ }
1650
3060
  return super.parseFunctionBody(node, isArrowFunction, isMethod, forInit, ...args);
1651
3061
  } finally {
1652
3062
  this.#functionBodyDepth--;
@@ -1657,22 +3067,41 @@ export function TSRXPlugin(config) {
1657
3067
  * @return {ESTreeJSX.JSXExpressionContainer}
1658
3068
  */
1659
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;
1660
3078
  let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1661
- this.next();
3079
+ this.#jsxExpressionContainerDepth++;
3080
+ try {
3081
+ this.next();
1662
3082
 
1663
- node.expression =
1664
- this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
1665
- if (this.#allowExpressionContainerTrailingSemicolon && this.type === tt.semi) {
1666
- if (this.#collect) {
1667
- this.#report_recoverable_error(
1668
- this.start,
1669
- 'TSRX expression containers do not use semicolons. Remove this semicolon.',
1670
- DIAGNOSTIC_CODES.TEMPLATE_EXPRESSION_TRAILING_SEMICOLON,
1671
- );
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();
1672
3094
  }
1673
- 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);
1674
3104
  }
1675
- this.expect(tt.braceR);
1676
3105
 
1677
3106
  return this.finishNode(node, 'JSXExpressionContainer');
1678
3107
  }
@@ -1705,88 +3134,140 @@ export function TSRXPlugin(config) {
1705
3134
  );
1706
3135
  }
1707
3136
 
1708
- /**
1709
- * @returns {AST.TextNode}
1710
- */
1711
- parseDoubleQuotedTextChild() {
1712
- const node = /** @type {AST.TextNode} */ (this.startNode());
1713
- const expression = /** @type {AST.Literal} */ (this.startNode());
1714
- node.raw = this.input.slice(this.start, this.end);
1715
- const end = this.end;
1716
- const endLoc = this.endLoc;
1717
-
1718
- expression.value = this.value;
1719
- expression.raw = JSON.stringify(this.value);
1720
- node.expression = this.finishNodeAt(expression, 'Literal', end, endLoc);
1721
-
1722
- this.#allowTagStartAfterDoubleQuotedText = true;
1723
- try {
1724
- this.next();
1725
- } finally {
1726
- this.#allowTagStartAfterDoubleQuotedText = false;
1727
- }
1728
-
1729
- return this.finishNodeAt(node, 'Text', end, endLoc);
1730
- }
1731
-
1732
3137
  /**
1733
3138
  * @type {Parse.Parser['jsx_parseAttribute']}
1734
3139
  */
1735
3140
  jsx_parseAttribute() {
1736
- let node =
1737
- /** @type {AST.TSRXAttribute | ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} */ (
1738
- this.startNode()
1739
- );
3141
+ let node = /** @type {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute} */ (
3142
+ this.startNode()
3143
+ );
1740
3144
 
1741
- if (this.eat(tt.braceL)) {
1742
- const current_template_node = this.#path.findLast(
1743
- (n) =>
1744
- n.type === 'Element' ||
1745
- n.type === 'Tsx' ||
1746
- n.type === 'Tsrx' ||
1747
- n.type === 'TsxCompat',
1748
- );
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);
1749
3148
  if (
1750
- current_template_node?.type === 'Tsx' ||
1751
- current_template_node?.type === 'TsxCompat'
3149
+ this.#isIdentifierChar(first) &&
3150
+ !(first >= CharCode.digit0 && first <= CharCode.digit9)
1752
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
+ }
3200
+
3201
+ if (this.eat(tt.braceL)) {
3202
+ if (this.type === tt.ellipsis || this.input.slice(this.start, this.start + 3) === '...') {
3203
+ this.#suppressTemplateRawTextToken = true;
1753
3204
  if (this.type === tt.ellipsis) {
1754
3205
  this.expect(tt.ellipsis);
3206
+ } else {
3207
+ this.pos = this.start + 3;
3208
+ this.nextToken();
3209
+ }
3210
+ this.#templateScriptParsingDepth++;
3211
+ try {
1755
3212
  /** @type {ESTreeJSX.JSXSpreadAttribute} */ (node).argument = this.parseMaybeAssign();
1756
- this.expect(tt.braceR);
1757
- return this.finishNode(node, 'JSXSpreadAttribute');
3213
+ } finally {
3214
+ this.#templateScriptParsingDepth--;
1758
3215
  }
1759
- this.unexpected();
1760
- }
1761
-
1762
- if (this.type === tt.ellipsis) {
1763
- this.expect(tt.ellipsis);
1764
- /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
1765
3216
  this.expect(tt.braceR);
1766
- return this.finishNode(node, 'SpreadAttribute');
3217
+ return this.finishNode(node, 'JSXSpreadAttribute');
1767
3218
  } else if (this.lookahead().type === tt.ellipsis) {
3219
+ this.#suppressTemplateRawTextToken = true;
1768
3220
  this.expect(tt.ellipsis);
1769
- /** @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
+ }
1770
3227
  this.expect(tt.braceR);
1771
- return this.finishNode(node, 'SpreadAttribute');
3228
+ return this.finishNode(node, 'JSXSpreadAttribute');
1772
3229
  } else {
1773
- 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;
1774
3242
  id.tracked = false;
1775
- this.finishNode(id, 'Identifier');
1776
- /** @type {AST.Attribute} */ (node).name = id;
1777
- /** @type {AST.Attribute} */ (node).value = id;
1778
- /** @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;
1779
3264
  this.next();
1780
3265
  this.expect(tt.braceR);
1781
- return this.finishNode(node, 'Attribute');
3266
+ return this.finishNode(node, 'JSXAttribute');
1782
3267
  }
1783
3268
  }
1784
3269
  /** @type {ESTreeJSX.JSXAttribute} */ (node).name = this.jsx_parseNamespacedName();
1785
- if (
1786
- /** @type {ESTreeJSX.JSXAttribute} */ (node).name.type === 'JSXIdentifier' &&
1787
- /** @type {ESTreeJSX.JSXIdentifier} */ (/** @type {ESTreeJSX.JSXAttribute} */ (node).name)
1788
- .tracked
1789
- ) {
3270
+ if (this.#isDynamicJSXElementName(/** @type {ESTreeJSX.JSXAttribute} */ (node).name)) {
1790
3271
  this.#report_recoverable_error_range(
1791
3272
  /** @type {AST.NodeWithLocation} */ (node).start,
1792
3273
  /** @type {AST.NodeWithLocation} */ (/** @type {ESTreeJSX.JSXAttribute} */ (node).name)
@@ -1831,7 +3312,7 @@ export function TSRXPlugin(config) {
1831
3312
 
1832
3313
  if (this.type === tt.name || this.type === tstt.jsxName) {
1833
3314
  node.name = /** @type {string} */ (this.value);
1834
- node.tracked = true;
3315
+ /** @type {any} */ (node).dynamic = true;
1835
3316
  this.next();
1836
3317
  } else {
1837
3318
  // Unexpected token after @
@@ -1839,7 +3320,6 @@ export function TSRXPlugin(config) {
1839
3320
  }
1840
3321
  } else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
1841
3322
  node.name = /** @type {string} */ (this.value);
1842
- node.tracked = false; // Explicitly mark as not tracked
1843
3323
  this.next();
1844
3324
  } else {
1845
3325
  return super.jsx_parseIdentifier();
@@ -1908,10 +3388,132 @@ export function TSRXPlugin(config) {
1908
3388
  }
1909
3389
  }
1910
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
+
1911
3403
  /**
1912
3404
  * @type {Parse.Parser['parseTryStatement']}
1913
3405
  */
1914
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
+
1915
3517
  this.next();
1916
3518
  node.block = this.parseBlock();
1917
3519
  node.handler = null;
@@ -1980,25 +3582,53 @@ export function TSRXPlugin(config) {
1980
3582
 
1981
3583
  /** @type {Parse.Parser['jsx_readToken']} */
1982
3584
  jsx_readToken() {
1983
- const current_template_node = this.#path.findLast(
1984
- (n) =>
1985
- n.type === 'Element' || n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'TsxCompat',
1986
- );
1987
- if (current_template_node?.type === 'TsxCompat' || current_template_node?.type === 'Tsx') {
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) {
1988
3623
  return super.jsx_readToken();
1989
3624
  }
3625
+
1990
3626
  let out = '',
1991
3627
  chunkStart = this.pos;
1992
3628
 
1993
3629
  while (true) {
1994
3630
  if (this.pos >= this.input.length) {
1995
- const inside_open_template = this.#path.findLast(
1996
- (n) =>
1997
- n.type === 'Element' ||
1998
- n.type === 'Tsrx' ||
1999
- n.type === 'TsxCompat' ||
2000
- n.type === 'Tsx',
2001
- );
3631
+ const inside_open_template = this.#path.findLast((n) => this.#isNativeTemplateNode(n));
2002
3632
  if (!inside_open_template) {
2003
3633
  while (this.curContext() === tstc.tc_expr) {
2004
3634
  this.context.pop();
@@ -2009,9 +3639,30 @@ export function TSRXPlugin(config) {
2009
3639
  }
2010
3640
  let ch = this.input.charCodeAt(this.pos);
2011
3641
 
2012
- switch (ch) {
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
+
2013
3661
  case CharCode.lessThan:
2014
3662
  case CharCode.openBrace:
3663
+ if (out || this.pos > chunkStart) {
3664
+ return this.finishToken(tstt.jsxText, out + this.input.slice(chunkStart, this.pos));
3665
+ }
2015
3666
  // In JSX text mode, '<' and '{' always start a tag/expression container.
2016
3667
  // `exprAllowed` can be false here due to surrounding parser state, but
2017
3668
  // throwing breaks valid templates (e.g. sibling tags after a close).
@@ -2058,6 +3709,7 @@ export function TSRXPlugin(config) {
2058
3709
  }
2059
3710
 
2060
3711
  // Continue processing from current position
3712
+ chunkStart = this.pos;
2061
3713
  break;
2062
3714
  } else if (this.input.charCodeAt(this.pos + 1) === CharCode.asterisk) {
2063
3715
  // '/*'
@@ -2097,9 +3749,13 @@ export function TSRXPlugin(config) {
2097
3749
  }
2098
3750
 
2099
3751
  // Continue processing from current position
3752
+ chunkStart = this.pos;
3753
+ break;
3754
+ }
3755
+ if (this.#shouldReadTemplateRawTextToken()) {
3756
+ ++this.pos;
2100
3757
  break;
2101
3758
  }
2102
- // If not a comment, fall through to default case
2103
3759
  this.#resetTokenStartToCurrentPosition();
2104
3760
  this.context.push(b_stat);
2105
3761
  this.exprAllowed = true;
@@ -2114,10 +3770,21 @@ export function TSRXPlugin(config) {
2114
3770
  case CharCode.greaterThan:
2115
3771
  case CharCode.closeBrace: {
2116
3772
  if (
2117
- ch === CharCode.closeBrace &&
2118
- (this.#path.length === 0 ||
2119
- this.#path.at(-1)?.type === 'Element' ||
2120
- this.#path.at(-1)?.type === 'Tsrx')
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))))
2121
3788
  ) {
2122
3789
  this.#resetTokenStartToCurrentPosition();
2123
3790
  return original.readToken.call(this, ch);
@@ -2144,6 +3811,10 @@ export function TSRXPlugin(config) {
2144
3811
  } else if (ch === CharCode.space || ch === CharCode.tab) {
2145
3812
  ++this.pos;
2146
3813
  } else {
3814
+ if (this.#shouldReadTemplateRawTextToken()) {
3815
+ ++this.pos;
3816
+ break;
3817
+ }
2147
3818
  this.#resetTokenStartToCurrentPosition();
2148
3819
  this.context.push(b_stat);
2149
3820
  this.exprAllowed = true;
@@ -2154,523 +3825,326 @@ export function TSRXPlugin(config) {
2154
3825
  }
2155
3826
 
2156
3827
  /**
2157
- * Override jsx_parseElement to parse tags and bare fragments as native TSRX
2158
- * by default. Explicit <tsx> and <tsx:*> islands keep ordinary TSX parsing
2159
- * for their children.
3828
+ * Override jsx_parseElement to use TSRX template parsing only where the
3829
+ * fragment/element body can contain TSRX-only syntax.
2160
3830
  * @type {Parse.Parser['jsx_parseElement']}
2161
3831
  */
2162
3832
  jsx_parseElement() {
2163
- // Current token is jsxTagStart, this.end is position after '<'
2164
- const tag_name_start = this.end;
2165
- const current_template_node = this.#path.findLast(
2166
- (n) =>
2167
- n.type === 'Element' || n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'TsxCompat',
2168
- );
2169
- const inside_tsx_island =
2170
- current_template_node?.type === 'Tsx' || current_template_node?.type === 'TsxCompat';
2171
- if (inside_tsx_island) {
2172
- if (this.input.charCodeAt(tag_name_start) === CharCode.at) {
2173
- this.#report_recoverable_error_range(
2174
- this.start,
2175
- tag_name_start + 1,
2176
- 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())
2177
3838
  );
2178
3839
  }
2179
- // Inside tsx/tsx:*, let acorn-jsx handle regular TSX tags normally.
2180
- return super.jsx_parseElement();
3840
+
3841
+ this.#scriptJSXElementDepth++;
3842
+ try {
3843
+ return super.jsx_parseElement();
3844
+ } finally {
3845
+ this.#scriptJSXElementDepth--;
3846
+ }
2181
3847
  }
2182
3848
 
2183
3849
  this.next();
2184
3850
  const parsed = /** @type {import('estree-jsx').JSXElement} */ (
2185
3851
  /** @type {unknown} */ (this.parseElement())
2186
3852
  );
2187
- if (!inside_tsx_island) {
2188
- this.#popTokenContextsAfterTemplateExpressionElement(
2189
- /** @type {AST.Tsx | AST.Tsrx | 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(),
2190
3899
  );
2191
- } else if (this.type === tt.braceR && this.curContext() === tstc.tc_expr) {
2192
- if (this.#tsxIslandExpressionDepth === 0) {
2193
- // Acorn still owns the surrounding JSX expression container.
2194
- // Keep a block-expression context for its closing `}` so the
2195
- // parent TSX tag continues tokenizing as JSX afterward.
2196
- 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;
2197
3920
  }
3921
+ this.#path.push(opening_template_node);
3922
+ pushed_opening_template_node = true;
2198
3923
  }
2199
- 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
+ );
2200
3938
  }
2201
3939
 
2202
3940
  /**
2203
3941
  * @type {Parse.Parser['parseElement']}
2204
3942
  */
2205
3943
  parseElement() {
2206
- const inside_head = this.#path.findLast(
2207
- (n) => n.type === 'Element' && n.id && n.id.type === 'Identifier' && n.id.name === 'head',
2208
- );
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
+
2209
3960
  // Adjust the start so we capture the `<` as part of the element
2210
3961
  const start = this.start - 1;
2211
3962
  const position = new acorn.Position(this.curLine, start - this.lineStart);
2212
3963
 
2213
- const element = /** @type {AST.Element | AST.Tsx | AST.Tsrx | AST.TsxCompat} */ (
2214
- this.startNode()
2215
- );
2216
- element.start = start;
2217
- /** @type {AST.NodeWithLocation} */ (element).loc.start = position;
2218
- element.metadata = { path: [] };
2219
- element.children = [];
2220
- element.type = 'Element';
2221
- this.#path.push(element);
2222
-
2223
- const open = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
2224
- this.jsx_parseOpeningElementAt(start, position)
2225
- );
2226
-
2227
- // Always attach the concrete opening element node for accurate source mapping
2228
- 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'));
2229
3990
 
2230
3991
  // Fragments (<>) produce JSXOpeningFragment with no `name` property
2231
3992
  const is_fragment = !open.name;
2232
- const is_tsx_compat = !is_fragment && open.name.type === 'JSXNamespacedName';
2233
- const is_tsx =
2234
- !is_fragment &&
2235
- !is_tsx_compat &&
2236
- open.name.type === 'JSXIdentifier' &&
2237
- open.name.name === 'tsx';
2238
- const is_dynamic_name =
2239
- !is_fragment &&
2240
- ((open.name.type === 'JSXIdentifier' && open.name.tracked) ||
2241
- (open.name.type === 'JSXMemberExpression' &&
2242
- open.name.object.type === 'JSXIdentifier' &&
2243
- open.name.object.tracked));
2244
-
2245
- 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') {
2246
3999
  const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
2247
- /** @type {AST.TsxCompat} */ (element).type = 'TsxCompat';
2248
- /** @type {AST.TsxCompat} */ (element).kind = namespace_node.name.name; // e.g., "react" from "tsx:react"
2249
-
2250
- if (open.selfClosing) {
2251
- const tagName = namespace_node.namespace.name + ':' + namespace_node.name.name;
2252
- this.raise(
2253
- open.start,
2254
- `TSX compatibility elements cannot be self-closing. '<${tagName} />' must have a closing tag '</${tagName}>'.`,
2255
- );
2256
- }
2257
- } else if (is_tsx) {
2258
- /** @type {AST.Tsx} */ (element).type = 'Tsx';
2259
-
2260
- if (open.selfClosing) {
2261
- this.raise(
2262
- open.start,
2263
- `TSX elements cannot be self-closing. '<tsx />' must have a closing tag '</tsx>'.`,
2264
- );
2265
- }
2266
- } else if (is_fragment) {
2267
- /** @type {AST.Tsrx} */ (element).type = 'Tsrx';
2268
- } else {
2269
- element.type = 'Element';
2270
- }
2271
-
2272
- if (is_tsx && is_dynamic_name) {
2273
- this.#report_recoverable_error_range(
2274
- open.name.start ?? open.start,
2275
- open.name.end ?? open.end,
2276
- DYNAMIC_ELEMENT_IN_TSX_ERROR,
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}>.`,
2277
4004
  );
2278
4005
  }
2279
4006
 
2280
- for (const attr of open.attributes) {
2281
- if (attr.type === 'JSXAttribute') {
2282
- /** @type {AST.Attribute} */ (/** @type {unknown} */ (attr)).type = 'Attribute';
2283
- if (attr.name.type === 'JSXIdentifier') {
2284
- /** @type {AST.Identifier} */ (/** @type {unknown} */ (attr.name)).type =
2285
- 'Identifier';
2286
- }
2287
- if (attr.value !== null) {
2288
- if (attr.value.type === 'JSXExpressionContainer') {
2289
- const expression = attr.value.expression;
2290
- if (expression.type === 'Literal') {
2291
- expression.was_expression = true;
2292
- }
2293
- // @ts-ignore intentional AST node conversion from JSX to Ripple
2294
- /** @type {ESTreeJSX.JSXAttribute} */ (attr).value =
2295
- /** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (expression);
2296
- }
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;
2297
4023
  }
2298
4024
  }
2299
4025
  }
2300
4026
 
2301
- if (!is_tsx_compat && !is_tsx && !is_fragment) {
2302
- /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
2303
- convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
2304
- );
2305
- element.selfClosing = open.selfClosing;
2306
- } else if (is_fragment) {
2307
- element.selfClosing = false;
2308
- }
2309
-
2310
- element.attributes = open.attributes;
2311
- element.metadata ??= { path: [] };
2312
4027
  // Opening-tag parsing can tokenize comments that appear before the first
2313
4028
  // child. Preserve that early container id so the comment stays associated
2314
4029
  // with this element during comment attachment/printing.
2315
- if (element.metadata.commentContainerId === undefined) {
2316
- element.metadata.commentContainerId = ++this.#commentContextId;
4030
+ if (node.metadata.commentContainerId === undefined) {
4031
+ node.metadata.commentContainerId = ++this.#commentContextId;
2317
4032
  }
2318
4033
 
2319
- if (element.selfClosing) {
2320
- this.#path.pop();
4034
+ this.#path.push(node);
2321
4035
 
2322
- if (this.type.label === '</>/<=/>=') {
2323
- this.pos--;
2324
- this.next();
2325
- }
2326
- } else if (is_fragment) {
2327
- 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), {
2328
4043
  enterScope: true,
2329
4044
  resetFunctionBodyDepth: true,
2330
4045
  });
2331
4046
 
2332
- this.#path.pop();
2333
-
2334
- if (!element.unclosed) {
2335
- const raise_error = () => {
2336
- 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,
2337
4062
  };
2338
-
2339
- this.next();
2340
- if (this.value !== '/') {
2341
- raise_error();
2342
- }
2343
- this.next();
2344
- if (this.type !== tstt.jsxTagEnd) {
2345
- raise_error();
2346
- }
2347
- this.#popTsxTokenContextBeforeTemplateExpressionChild();
2348
- this.next();
2349
- }
2350
- } else {
2351
- if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'script') {
2352
- let content = '';
2353
-
2354
- // TODO implement this where we get a string for content of the content of the script tag
2355
- // This is a temporary workaround to get the content of the script tag
2356
- const start = open.end;
2357
- const input = this.input.slice(start);
2358
- const end = input.indexOf('</script>');
2359
- content = end === -1 ? input : input.slice(0, end);
2360
-
2361
- const newLines = content.match(regex_newline_characters)?.length;
2362
- if (newLines) {
2363
- this.curLine = open.loc.end.line + newLines;
2364
- this.lineStart = start + content.lastIndexOf('\n') + 1;
2365
- }
2366
- if (end !== -1) {
2367
- const closingStart = start + content.length;
2368
- const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
2369
- const closingStartLoc = new acorn.Position(
2370
- closingLineInfo.line,
2371
- closingLineInfo.column,
2372
- );
2373
-
2374
- // Ensure `</script>` can't be tokenized as `<` followed by a regexp
2375
- // start when we manually advance to the `/`.
2376
- this.exprAllowed = false;
2377
-
2378
- // Position after '<' (so next() reads '/')
2379
- this.pos = closingStart + 1;
2380
- this.type = tstt.jsxTagStart;
2381
- this.start = closingStart;
2382
- this.startLoc = closingStartLoc;
2383
- this.next();
2384
-
2385
- // Consume '/'
2386
- this.next();
2387
-
2388
- const closingElement = this.jsx_parseClosingElementAt(closingStart, closingStartLoc);
2389
- element.closingElement = closingElement;
2390
- this.exprAllowed = false;
2391
-
2392
- const contentStartLineInfo = acorn.getLineInfo(this.input, start);
2393
- const contentStartLoc = new acorn.Position(
2394
- contentStartLineInfo.line,
2395
- contentStartLineInfo.column,
2396
- );
2397
-
2398
- const contentEndLineInfo = acorn.getLineInfo(this.input, closingStart);
2399
- const contentEndLoc = new acorn.Position(
2400
- contentEndLineInfo.line,
2401
- contentEndLineInfo.column,
2402
- );
2403
-
2404
- element.children = [
2405
- /** @type {AST.ScriptContent} */ (
2406
- /** @type {unknown} */ ({
2407
- type: 'ScriptContent',
2408
- content,
2409
- start,
2410
- end: closingStart,
2411
- loc: { start: contentStartLoc, end: contentEndLoc },
2412
- })
2413
- ),
2414
- ];
2415
-
2416
- this.#path.pop();
2417
- } else {
2418
- // No closing tag
2419
- this.#report_broken_markup_error(
2420
- open.end,
2421
- "Unclosed tag '<script>'. Expected '</script>' before end of template.",
2422
- );
2423
- /** @type {AST.Element} */ (element).unclosed = true;
2424
- this.#path.pop();
2425
- }
2426
- } else if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'style') {
2427
- // jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
2428
- // So backtrack to the end of the <style> tag to make sure everything is included
2429
- const start = open.end;
2430
- const input = this.input.slice(start);
2431
- const end = input.indexOf('</style>');
2432
- const content = end === -1 ? input : input.slice(0, end);
2433
-
2434
- const parsed_css = parse_style(content, { loose: this.#loose });
2435
- if (!inside_head) {
2436
- /** @type {AST.Element} */ (element).metadata.styleScopeHash = parsed_css.hash;
2437
- }
2438
-
2439
- const newLines = content.match(regex_newline_characters)?.length;
2440
- if (newLines) {
2441
- this.curLine = open.loc.end.line + newLines;
2442
- this.lineStart = start + content.lastIndexOf('\n') + 1;
2443
- }
2444
- if (end !== -1) {
2445
- const closingStart = start + content.length;
2446
- const closingLineInfo = acorn.getLineInfo(this.input, closingStart);
2447
- const closingStartLoc = new acorn.Position(
2448
- closingLineInfo.line,
2449
- closingLineInfo.column,
2450
- );
2451
-
2452
- // Ensure `</style>` can't be tokenized as `<` followed by a regexp
2453
- // start when we manually advance to the `/`.
2454
- this.exprAllowed = false;
2455
-
2456
- // Position after '<' (so next() reads '/')
2457
- this.pos = closingStart + 1;
2458
- this.type = tstt.jsxTagStart;
2459
- this.start = closingStart;
2460
- this.startLoc = closingStartLoc;
2461
- this.next();
2462
-
2463
- // Consume '/'
2464
- this.next();
2465
-
2466
- const closingElement = this.jsx_parseClosingElementAt(closingStart, closingStartLoc);
2467
- element.closingElement = closingElement;
2468
- this.exprAllowed = false;
2469
- this.#path.pop();
2470
- } else {
2471
- this.#report_broken_markup_error(
2472
- open.end,
2473
- "Unclosed tag '<style>'. Expected '</style>' before end of template.",
2474
- );
2475
- /** @type {AST.Element} */ (element).unclosed = true;
2476
- this.#path.pop();
2477
- }
2478
- // This node is used for Prettier - always add parsed CSS as children
2479
- // for proper formatting, regardless of whether it's inside head or not
2480
- /** @type {AST.Element} */ (element).children = [
2481
- /** @type {AST.Node} */ (/** @type {unknown} */ (parsed_css)),
2482
- ];
2483
-
2484
- // Ensure we escape JSX <tag></tag> context
2485
- const curContext = this.curContext();
2486
- const parent = this.#path.at(-1);
2487
- const insideTemplate = this.#isNativeTemplateNode(parent);
2488
-
2489
- if (curContext === tstc.tc_expr && !insideTemplate) {
2490
- this.context.pop();
2491
- }
2492
-
2493
- /** @type {AST.Element} */ (element).css = content;
2494
- } else {
2495
- this.#parseNativeTemplateBody(element, /** @type {AST.Element} */ (element).children, {
2496
- enterScope: true,
2497
- resetFunctionBodyDepth: true,
2498
- });
2499
- if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
2500
- this.#reportDynamicJsxElementsInTsx(/** @type {AST.Element} */ (element).children);
2501
- }
2502
-
2503
- if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
2504
- this.#path.pop();
2505
-
2506
- if (!element.unclosed) {
2507
- const raise_error = () => {
2508
- this.raise(this.start, `Expected closing tag '</tsx>'`);
2509
- };
2510
-
2511
- this.next();
2512
- // we should expect to see </tsx>
2513
- if (this.value !== '/') {
2514
- raise_error();
2515
- }
2516
- this.next();
2517
- if (this.value !== 'tsx') {
2518
- raise_error();
2519
- }
2520
- this.next();
2521
- if (this.type !== tstt.jsxTagEnd) {
2522
- raise_error();
2523
- }
2524
- this.#popTsxTokenContextBeforeTemplateExpressionChild();
2525
- this.next();
2526
- }
2527
- } else if (/** @type {AST.TsxCompat} */ (element).type === 'TsxCompat') {
2528
- this.#path.pop();
2529
-
2530
- if (!element.unclosed) {
2531
- const raise_error = () => {
2532
- this.raise(
2533
- this.start,
2534
- `Expected closing tag '</tsx:${/** @type {AST.TsxCompat} */ (element).kind}>'`,
2535
- );
2536
- };
2537
-
2538
- this.next();
2539
- // we should expect to see </tsx:kind>
2540
- if (this.value !== '/') {
2541
- raise_error();
2542
- }
2543
- this.next();
2544
- if (this.value !== 'tsx') {
2545
- raise_error();
2546
- }
2547
- this.next();
2548
- if (this.type.label !== ':') {
2549
- raise_error();
2550
- }
2551
- this.next();
2552
- if (this.value !== /** @type {AST.TsxCompat} */ (element).kind) {
2553
- raise_error();
2554
- }
2555
- this.next();
2556
- if (this.type !== tstt.jsxTagEnd) {
2557
- raise_error();
2558
- }
2559
- this.#popTsxTokenContextBeforeTemplateExpressionChild();
2560
- this.next();
2561
- }
2562
- } else if (
2563
- /** @type {AST.Tsrx} */ (element).type === 'Tsrx' &&
2564
- this.#path[this.#path.length - 1] === element
2565
- ) {
2566
- const displayTag = element.openingElement.name ? 'tsrx' : '';
2567
- this.#report_broken_markup_error(
2568
- this.start,
2569
- `Unclosed tag '<${displayTag}>'. Expected '</${displayTag}>' before end of template.`,
2570
- );
2571
- element.unclosed = true;
2572
- /** @type {AST.SourceLocation} */ (element.loc).end = {
2573
- .../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
2574
- };
2575
- element.end = element.openingElement.end;
2576
- this.#path.pop();
2577
- } else if (
2578
- element.type === 'Element' &&
2579
- this.#path[this.#path.length - 1] === element
2580
- ) {
2581
- // Check if this element was properly closed
2582
- const tagName = this.getElementName(element.id);
2583
- this.#report_broken_markup_error(
2584
- this.start,
2585
- `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of template.`,
2586
- );
2587
- element.unclosed = true;
2588
- /** @type {AST.SourceLocation} */ (element.loc).end = {
2589
- .../** @type {AST.SourceLocation} */ (element.openingElement.loc).end,
2590
- };
2591
- element.end = element.openingElement.end;
2592
- this.#path.pop();
2593
- }
4063
+ node.end = is_fragment
4064
+ ? /** @type {ESTreeJSX.JSXFragment} */ (node).openingFragment.end
4065
+ : /** @type {ESTreeJSX.JSXElement} */ (node).openingElement.end;
4066
+ this.#path.pop();
2594
4067
  }
2595
4068
 
2596
- // Ensure we escape JSX <tag></tag> context
2597
- 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.
2598
4076
  const parent = this.#path.at(-1);
2599
4077
  const insideTemplate = this.#isNativeTemplateNode(parent);
2600
4078
 
2601
- if (curContext === tstc.tc_expr && !insideTemplate) {
2602
- this.context.pop();
4079
+ if (!insideTemplate && this.context.length > pre_element_context_depth) {
4080
+ this.context.length = pre_element_context_depth;
2603
4081
  }
2604
4082
  }
2605
4083
 
2606
- if (element.closingElement && !is_tsx_compat && !is_tsx && element.closingElement.name) {
2607
- /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
2608
- 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
2609
4087
  );
4088
+ return this.finishNodeAt(node, node.type, closing.end, closing.loc.end);
2610
4089
  }
2611
4090
 
2612
- this.finishNode(element, element.type);
2613
- return element;
4091
+ return this.finishNode(node, node.type);
2614
4092
  }
2615
4093
 
2616
4094
  /**
2617
4095
  * @type {Parse.Parser['parseTemplateBody']}
2618
4096
  */
2619
4097
  parseTemplateBody(body) {
2620
- const inside_func =
2621
- this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
2622
- const current_template_node = this.#path.findLast(
2623
- (n) =>
2624
- n.type === 'Element' || n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'TsxCompat',
2625
- );
2626
- const inside_tsx_island =
2627
- current_template_node?.type === 'Tsx' || current_template_node?.type === 'TsxCompat'
2628
- ? current_template_node
2629
- : null;
2630
-
2631
- if (current_template_node?.type === 'Tsrx' && this.type === tstt.jsxText) {
2632
- while (this.curContext() === tstc.tc_expr) {
2633
- 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;
2634
4118
  }
2635
- this.pos = this.start;
2636
- this.next();
4119
+ body.push(/** @type {any} */ (this.#parseCodeBlock()));
2637
4120
  this.parseTemplateBody(body);
2638
4121
  return;
2639
4122
  }
2640
4123
 
2641
- if (!inside_func) {
2642
- if (this.type.label === 'continue') {
2643
- throw new Error('`continue` statements are not allowed in native templates');
2644
- }
2645
- if (this.type.label === 'break') {
2646
- throw new Error('`break` statements are not allowed in native templates');
2647
- }
2648
- }
2649
-
2650
- if (inside_tsx_island) {
2651
- this.#parseTsxIslandBody(
2652
- /** @type {AST.Tsx | AST.TsxCompat} */ (inside_tsx_island),
2653
- /** @type {AST.Node[]} */ (/** @type {unknown} */ (body)),
2654
- );
2655
- return;
2656
- }
2657
- if (
2658
- current_template_node?.type === 'Tsrx' &&
2659
- !current_template_node.openingElement.name &&
2660
- ((this.type === tstt.jsxTagStart && this.input.slice(this.pos, this.pos + 2) === '/>') ||
2661
- (this.input.charCodeAt(this.start) === CharCode.lessThan &&
2662
- this.input.slice(this.start + 1, this.start + 3) === '/>'))
2663
- ) {
2664
- this.exprAllowed = false;
2665
- return;
2666
- }
2667
4124
  if (this.type === tt.braceL) {
2668
4125
  body.push(this.#parseNativeTemplateExpressionContainer());
2669
- } else if (
2670
- this.type === tt.string &&
2671
- this.input.charCodeAt(this.start) === CharCode.doubleQuote
2672
- ) {
2673
- 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());
2674
4148
  } else if (this.type === tt.braceR) {
2675
4149
  // Leaving a native template body. We may still be in TSX/JSX tokenization
2676
4150
  // context (e.g. after parsing markup), but the closing `}` is a JS token.
@@ -2682,8 +4156,7 @@ export function TSRXPlugin(config) {
2682
4156
  return;
2683
4157
  } else if (
2684
4158
  this.type === tstt.jsxTagStart ||
2685
- (this.input.charCodeAt(this.start) === CharCode.lessThan &&
2686
- this.input.charCodeAt(this.start + 1) === CharCode.slash)
4159
+ this.input.charCodeAt(this.start) === CharCode.lessThan
2687
4160
  ) {
2688
4161
  const startPos = this.start;
2689
4162
  const startLoc = this.startLoc;
@@ -2704,21 +4177,20 @@ export function TSRXPlugin(config) {
2704
4177
  // Consume '/'
2705
4178
  this.next();
2706
4179
 
2707
- const closingElement =
2708
- /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
4180
+ let closingElement;
4181
+ this.#closingNativeTemplateNode = true;
4182
+ try {
4183
+ closingElement = /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
2709
4184
  this.jsx_parseClosingElementAt(startPos, startLoc)
2710
4185
  );
4186
+ } finally {
4187
+ this.#closingNativeTemplateNode = false;
4188
+ }
2711
4189
  this.exprAllowed = false;
2712
4190
 
2713
4191
  // Validate that the closing tag matches the opening tag
2714
- const currentElement = this.#path[this.#path.length - 1];
2715
- if (
2716
- !currentElement ||
2717
- (currentElement.type !== 'Element' &&
2718
- currentElement.type !== 'Tsx' &&
2719
- currentElement.type !== 'Tsrx' &&
2720
- currentElement.type !== 'TsxCompat')
2721
- ) {
4192
+ const currentElement = /** @type {any} */ (this.#path[this.#path.length - 1]);
4193
+ if (!this.#isNativeTemplateNode(currentElement)) {
2722
4194
  this.raise(this.start, 'Unexpected closing tag');
2723
4195
  }
2724
4196
 
@@ -2727,27 +4199,17 @@ export function TSRXPlugin(config) {
2727
4199
  /** @type {string | null} */
2728
4200
  let closingTagName;
2729
4201
 
2730
- if (currentElement.type === 'TsxCompat') {
2731
- openingTagName = 'tsx:' + currentElement.kind;
2732
- closingTagName =
2733
- closingElement.name?.type === 'JSXNamespacedName'
2734
- ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2735
- : this.getElementName(closingElement.name);
2736
- } else if (currentElement.type === 'Tsx') {
2737
- openingTagName = currentElement.openingElement.name ? 'tsx' : null;
2738
- closingTagName =
2739
- closingElement.name?.type === 'JSXNamespacedName'
2740
- ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2741
- : this.getElementName(closingElement.name);
2742
- } else if (currentElement.type === 'Tsrx') {
4202
+ if (currentElement.type === 'JSXFragment') {
2743
4203
  openingTagName = '';
2744
- closingTagName =
2745
- closingElement.name?.type === 'JSXNamespacedName'
4204
+ closingTagName = !closingElement.name
4205
+ ? ''
4206
+ : closingElement.name.type === 'JSXNamespacedName'
2746
4207
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2747
4208
  : this.getElementName(closingElement.name);
2748
4209
  } else {
2749
- // Regular Element node (or fragment)
2750
- openingTagName = currentElement.id ? this.getElementName(currentElement.id) : null;
4210
+ openingTagName = currentElement.openingElement?.name
4211
+ ? this.getElementName(currentElement.openingElement.name)
4212
+ : null;
2751
4213
  closingTagName = closingElement.name
2752
4214
  ? closingElement.name?.type === 'JSXNamespacedName'
2753
4215
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
@@ -2756,6 +4218,24 @@ export function TSRXPlugin(config) {
2756
4218
  }
2757
4219
 
2758
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
+ }
2759
4239
  // this will throw if not collecting errors
2760
4240
  this.#report_broken_markup_error(
2761
4241
  closingElement.start,
@@ -2764,30 +4244,19 @@ export function TSRXPlugin(config) {
2764
4244
  );
2765
4245
  // Loop through all unclosed elements on the stack
2766
4246
  while (this.#path.length > 0) {
2767
- const elem = this.#path[this.#path.length - 1];
4247
+ const elem = /** @type {any} */ (this.#path[this.#path.length - 1]);
2768
4248
 
2769
4249
  // Stop at non-template boundaries.
2770
- if (
2771
- elem.type !== 'Element' &&
2772
- elem.type !== 'Tsx' &&
2773
- elem.type !== 'Tsrx' &&
2774
- elem.type !== 'TsxCompat'
2775
- ) {
4250
+ if (!this.#isNativeTemplateNode(elem)) {
2776
4251
  break;
2777
4252
  }
2778
4253
 
2779
4254
  const elemName =
2780
- elem.type === 'TsxCompat'
2781
- ? 'tsx:' + elem.kind
2782
- : elem.type === 'Tsx'
2783
- ? elem.openingElement.name
2784
- ? 'tsx'
2785
- : null
2786
- : elem.type === 'Tsrx'
2787
- ? ''
2788
- : elem.id
2789
- ? this.getElementName(elem.id)
2790
- : null;
4255
+ elem.type === 'JSXFragment'
4256
+ ? ''
4257
+ : elem.openingElement?.name
4258
+ ? this.getElementName(elem.openingElement.name)
4259
+ : null;
2791
4260
 
2792
4261
  // Found matching opening tag
2793
4262
  if (elemName === closingTagName) {
@@ -2797,28 +4266,31 @@ export function TSRXPlugin(config) {
2797
4266
  // Mark as unclosed and adjust location
2798
4267
  elem.unclosed = true;
2799
4268
  /** @type {AST.NodeWithLocation} */ (elem).loc.end = {
2800
- .../** @type {AST.SourceLocation} */ (elem.openingElement.loc).end,
4269
+ .../** @type {AST.SourceLocation} */ (
4270
+ elem.type === 'JSXFragment' ? elem.openingFragment.loc : elem.openingElement.loc
4271
+ ).end,
2801
4272
  };
2802
- elem.end = elem.openingElement.end;
4273
+ elem.end =
4274
+ elem.type === 'JSXFragment' ? elem.openingFragment.end : elem.openingElement.end;
2803
4275
 
2804
4276
  this.#path.pop(); // Remove from stack
2805
4277
  }
2806
4278
  }
2807
4279
 
2808
- const elementToClose = this.#path[this.#path.length - 1];
2809
- if (
2810
- elementToClose &&
2811
- (elementToClose.type === 'Element' || elementToClose.type === 'Tsrx')
2812
- ) {
4280
+ const elementToClose = /** @type {any} */ (this.#path[this.#path.length - 1]);
4281
+ if (this.#isNativeTemplateNode(elementToClose)) {
2813
4282
  const elementToCloseName =
2814
- elementToClose.type === 'Tsrx'
4283
+ elementToClose.type === 'JSXFragment'
2815
4284
  ? ''
2816
- : /** @type {AST.Element} */ (elementToClose).id
2817
- ? this.getElementName(/** @type {AST.Element} */ (elementToClose).id)
4285
+ : elementToClose.openingElement?.name
4286
+ ? this.getElementName(elementToClose.openingElement.name)
2818
4287
  : null;
2819
4288
  if (elementToCloseName === closingTagName) {
2820
- /** @type {AST.Element | AST.Tsrx} */ (elementToClose).closingElement =
2821
- closingElement;
4289
+ if (elementToClose.type === 'JSXFragment') {
4290
+ elementToClose.closingFragment = this.#toClosingFragment(closingElement);
4291
+ } else {
4292
+ elementToClose.closingElement = closingElement;
4293
+ }
2822
4294
  }
2823
4295
  }
2824
4296
 
@@ -2830,16 +4302,12 @@ export function TSRXPlugin(config) {
2830
4302
  if (node !== null) {
2831
4303
  body.push(node);
2832
4304
  }
4305
+ } else if (this.type === tt.eof) {
4306
+ return;
2833
4307
  } else {
2834
- skipWhitespace(this);
2835
- const node = this.parseStatement(null);
2836
- this.#report_invalid_template_return_statements(node);
2837
- body.push(node);
2838
-
2839
- // Ensure we're not in JSX context before recursing
2840
- // This is important when elements are parsed at statement level
2841
- if (this.curContext() === tstc.tc_expr) {
2842
- this.context.pop();
4308
+ const text = this.#parseTemplateRawText();
4309
+ if (this.#shouldKeepTemplateTextNode(text)) {
4310
+ body.push(text);
2843
4311
  }
2844
4312
  }
2845
4313
 
@@ -2918,22 +4386,30 @@ export function TSRXPlugin(config) {
2918
4386
  this.type === tt.braceL &&
2919
4387
  this.context.some((c) => c === tstc.tc_expr)
2920
4388
  ) {
2921
- return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TSRXExpression | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2922
- /** @type {unknown} */ (this.#parseNativeTemplateExpressionContainer())
4389
+ return /** @type {ESTreeJSX.JSXExpressionContainer} */ (
4390
+ this.#parseNativeTemplateExpressionContainer()
2923
4391
  );
2924
4392
  }
2925
4393
 
2926
4394
  if (this.type === tstt.jsxTagStart) {
2927
- this.next();
2928
- if (this.value === '/') {
2929
- this.unexpected();
4395
+ if (this.#forceScriptJSXElementDepth > 0) {
4396
+ return /** @type {AST.Statement} */ (
4397
+ /** @type {unknown} */ (super.parseStatement(context, topLevel, exports))
4398
+ );
2930
4399
  }
4400
+
4401
+ this.next();
4402
+ if (this.value === '/') this.unexpected();
2931
4403
  const node = this.parseElement();
2932
4404
 
2933
4405
  if (!node) {
2934
4406
  this.unexpected();
2935
4407
  }
2936
- if (this.#functionBodyDepth > 0 && node.type === 'Tsrx' && this.curContext() === b_stat) {
4408
+ if (
4409
+ this.#functionBodyDepth > 0 &&
4410
+ node.type === 'JSXFragment' &&
4411
+ this.curContext() === b_stat
4412
+ ) {
2937
4413
  this.context.pop();
2938
4414
  if (this.curContext() === tstc.tc_expr) {
2939
4415
  this.context.pop();
@@ -2945,19 +4421,6 @@ export function TSRXPlugin(config) {
2945
4421
  return node;
2946
4422
  }
2947
4423
 
2948
- if (
2949
- this.#functionBodyDepth === 0 &&
2950
- this.type === tt.string &&
2951
- this.input.charCodeAt(this.start) === CharCode.doubleQuote &&
2952
- (this.#path.at(-1)?.type === 'Element' || this.#path.at(-1)?.type === 'Tsrx')
2953
- ) {
2954
- this.pos = this.start;
2955
- this.#readDoubleQuotedTextChildToken();
2956
- const node = this.parseDoubleQuotedTextChild();
2957
- this.semicolon();
2958
- return node;
2959
- }
2960
-
2961
4424
  // &[ or &{ at statement level — lazy destructuring assignment
2962
4425
  // e.g., &[data] = track(0); or &{x, y} = obj;
2963
4426
  if (this.type === tt.bitwiseAND) {
@@ -2998,30 +4461,28 @@ export function TSRXPlugin(config) {
2998
4461
  parseBlock(createNewLexicalScope, node, exitStrict) {
2999
4462
  const parent = this.#path.at(-1);
3000
4463
 
3001
- // Inside a JS function body, parse `{...}` as a regular block statement,
3002
- // even if the nearest `#path` entry is a native template — we're in a
3003
- // nested function callable, not in a template.
3004
- if (
3005
- this.#functionBodyDepth === 0 &&
3006
- (parent?.type === 'Element' || parent?.type === 'Tsrx')
3007
- ) {
3008
- if (createNewLexicalScope === void 0) createNewLexicalScope = true;
3009
- if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());
3010
-
3011
- node.body = [];
3012
- this.#allowDoubleQuotedTextChildAfterBrace = true;
3013
- this.expect(tt.braceL);
3014
- this.#parseNativeTemplateBody(node, node.body, {
3015
- enterScope: createNewLexicalScope,
3016
- });
3017
-
3018
- if (exitStrict) {
3019
- 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++;
3020
4470
  }
3021
- this.exprAllowed = true;
4471
+ }
3022
4472
 
3023
- this.next();
3024
- 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
+ }
3025
4486
  }
3026
4487
 
3027
4488
  return super.parseBlock(createNewLexicalScope, node, exitStrict);