@tsrx/core 0.1.20 → 0.1.24

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