`), where `/` is division or a self-closing tag's and
+ // `#` is a private-field access.
// The element being opened goes on `#path` only for the token after
// its `>`, which is its first child.
const template_parent = this.#path.at(-1);
@@ -5866,25 +5873,19 @@ export function TSRXPlugin(config) {
}
if (this.eat(tt.braceL)) {
- if (this.type === tt.ellipsis) {
+ if (this.type === tt.ellipsis || this.lookahead().type === tt.ellipsis) {
+ // The brace's context is on top
+ const brace_context_depth = this.context.length;
this.#suppressTemplateRawTextToken = true;
this.expect(tt.ellipsis);
- this.#templateScriptParsingDepth++;
- try {
- /** @type {ESTreeJSX.JSXSpreadAttribute} */ (node).argument = this.parseMaybeAssign();
- } finally {
- this.#templateScriptParsingDepth--;
- }
- this.#expectContainerClosingBrace();
- return this.finishNode(node, 'JSXSpreadAttribute');
- } else if (this.lookahead().type === tt.ellipsis) {
- this.#suppressTemplateRawTextToken = true;
- this.expect(tt.ellipsis);
- this.#templateScriptParsingDepth++;
- try {
- /** @type {ESTreeJSX.JSXSpreadAttribute} */ (node).argument = this.parseMaybeAssign();
- } finally {
- this.#templateScriptParsingDepth--;
+ /** @type {ESTreeJSX.JSXSpreadAttribute} */ (node).argument = this.#parseOpeningTagCode(
+ () => this.parseMaybeAssign(),
+ );
+ // A control-flow directive in an element in the argument can leave
+ // contexts above the brace's, as in a container (see
+ // `jsx_parseExpressionContainer`). The `}` read pops the brace's.
+ if (this.type === tt.braceR) {
+ this.#truncateContext(brace_context_depth - 1);
}
this.#expectContainerClosingBrace();
return this.finishNode(node, 'JSXSpreadAttribute');
@@ -6061,6 +6062,16 @@ export function TSRXPlugin(config) {
}
}
case tstt.jsxTagStart:
+ return this.#parseOpeningTagCode(() => {
+ // The element's `<` pushed its contexts on the opening tag's, which
+ // the tag goes on in after the element.
+ this.#tagValueContextDepth = this.context.length - 2;
+ try {
+ return this.parseExprAtom();
+ } finally {
+ this.#tagValueContextDepth = -1;
+ }
+ });
case tt.string:
return this.parseExprAtom();
default:
@@ -6068,6 +6079,39 @@ export function TSRXPlugin(config) {
}
}
+ /**
+ * Parses code in an opening tag outside a `{ … }` container: a spread
+ * attribute's argument, whose first token is read, or an element that is an
+ * attribute value without braces (`
… />`). While the tag is
+ * read, the element it opens is `#openingNativeTemplateNode`, still in
+ * script mode, so `jsx_parseElement` would hand an element in that code to
+ * acorn-typescript's JSX parser. Hide it and read the code as a container's
+ * (see `jsx_parseExpressionContainer`), as a braced value is read: its
+ * elements are template markup, and the code around them is not template
+ * text. The token after the code is read in the tag again.
+ * @template T
+ * @param {() => T} parse
+ * @returns {T}
+ */
+ #parseOpeningTagCode(parse) {
+ const opening_node = this.#openingNativeTemplateNode;
+ this.#openingNativeTemplateNode = null;
+ this.#jsxExpressionContainerDepth++;
+ this.#expressionContainerContextBaselines.push(this.context.length);
+ this.#expressionContainerPathBaselines.push({
+ path: this.#path,
+ length: this.#path.length,
+ });
+ try {
+ return parse();
+ } finally {
+ this.#jsxExpressionContainerDepth--;
+ this.#expressionContainerContextBaselines.pop();
+ this.#expressionContainerPathBaselines.pop();
+ this.#openingNativeTemplateNode = opening_node;
+ }
+ }
+
/**
* `@try`/`@pending`/`@catch` blocks are template control-flow blocks like
* `@if`/`@for`/`@switch`: `return` is not allowed inside them. A `return`
@@ -6684,9 +6728,13 @@ export function TSRXPlugin(config) {
// stack here; the body (especially a control-flow block) can otherwise
// leave residue that breaks tokenizing the following JS token when the
// element is in expression position. The token after `<` is already read,
- // so the `{` of a dynamic tag name (`<{tag}>`) doesn't count.
+ // so the `{` of a dynamic tag name (`<{tag}>`) doesn't count. An element
+ // that is an attribute value without braces keeps its host's opening tag
+ // contexts below it (see `jsx_parseAttributeValue`).
+ const tag_value_context_depth = this.#tagValueContextDepth;
+ this.#tagValueContextDepth = -1;
let pre_element_context_depth = this.context.length - this.#currentTokenContextCount();
- while (pre_element_context_depth > 0) {
+ while (pre_element_context_depth > Math.max(0, tag_value_context_depth)) {
const ctx = this.context[pre_element_context_depth - 1];
if (ctx === tstc.tc_expr || ctx === tstc.tc_oTag || ctx === tstc.tc_cTag) {
pre_element_context_depth--;
@@ -6719,6 +6767,9 @@ export function TSRXPlugin(config) {
switchCaseScriptDepth: this.#parsingJSXSwitchCaseScriptStatementDepth,
scriptJSXDepth: this.#scriptJSXElementDepth,
});
+ if (tag_value_context_depth >= 0) {
+ this.#tagValueElements.add(node);
+ }
const previous_opening_native_template_node = this.#openingNativeTemplateNode;
this.#openingNativeTemplateNode = node;
@@ -7024,7 +7075,8 @@ export function TSRXPlugin(config) {
this.#closingNativeTemplateNode = true;
}
// `>` reads the token after the element. When the element closes and no
- // template encloses it, that token is code: the element leaves `#path`
+ // template encloses it, or in its host's opening tag as an attribute
+ // value without braces, that token is code: the element leaves `#path`
// (`
x / 2` divides; it is popped for good below), and the
// tokenizer context returns to where the element began, as after a
// balanced closing tag, dropping residue its body left (a control-flow
@@ -7032,7 +7084,8 @@ export function TSRXPlugin(config) {
const closes_outside_template =
this.#isNativeTemplateNode(current) &&
current_name === closing_name_str &&
- !this.#isNativeTemplateNode(this.#path.at(-2));
+ (!this.#isNativeTemplateNode(this.#path.at(-2)) ||
+ this.#tagValueElements.has(current));
if (closes_outside_template) {
this.#path.pop();
const context_depth = this.#elementContextDepths.get(current);
diff --git a/packages/tsrx/src/transform/jsx/index.js b/packages/tsrx/src/transform/jsx/index.js
index fd44d8e3d..ea895f6ec 100644
--- a/packages/tsrx/src/transform/jsx/index.js
+++ b/packages/tsrx/src/transform/jsx/index.js
@@ -714,11 +714,12 @@ export function createJsxTransform(platform) {
}
if (!node.metadata?.native_tsrx) {
- // JSX inside a spread attribute's argument is parsed as plain JSX,
- // but the JSXOpeningElement visitor still lowers its host ref/spread
- // to a setup declaration. Wrap it here: left for the nearest native
- // ancestor, it would be declared outside any callback between the
- // two, where the callback's parameters are not in scope.
+ // JSX the parser reads as plain JSX (an element in a dynamic tag
+ // name, `<{…}>`), but the JSXOpeningElement visitor still lowers
+ // its host ref/spread to a setup declaration. Wrap it here: left
+ // for the nearest native ancestor, it would be declared outside
+ // any callback between the two, where the callback's parameters
+ // are not in scope.
return wrap_jsx_setup_declarations(
/** @type {AST.Expression} */ (next() ?? node),
in_jsx_child_context(path),
diff --git a/packages/tsrx/tests/shared/compile.js b/packages/tsrx/tests/shared/compile.js
index a34aeaa15..cb5ba4407 100644
--- a/packages/tsrx/tests/shared/compile.js
+++ b/packages/tsrx/tests/shared/compile.js
@@ -2615,6 +2615,19 @@ export function App() @{
expect(code).toContain('\u00a03');
});
+ it("keeps the text of an element in a spread attribute's argument and in an attribute value without braces", () => {
+ const { code } = compile(
+ 'export function App() @{\n\t
\n\t\t1 /* c */ 2