TSRX Syntax Support
Every construct on this page has a fixture in
test/parser/misc/tsrx/ (opens in new tab),
and every snippet below is taken from one. The fixture name is given with each
section so you can read the whole file and its snapshot.
| Construct | Fixture |
|---|---|
@{ } code blocks |
code-block.module.tsrx |
@{ } in expression position |
code-block-expression.module.tsrx |
@{ } as a function body |
code-block-function.module.tsrx |
@if / @else |
control-flow-if.module.tsrx |
@for / @empty |
control-flow-for.module.tsrx |
@switch / @case / @default |
control-flow-switch.module.tsrx |
@try / @pending / @catch |
control-flow-try.module.tsrx |
Dynamic tags <{expr}> |
dynamic-tag.module.tsrx |
<style> elements |
style-element.module.tsrx |
Lazy destructuring &{ } and &[ ] |
lazy-destructuring.module.tsrx |
| Submodule imports | submodule-import.module.tsrx |
| Text entities | text-entities.module.tsrx |
The chips under each example are the node types the parser produced for it. They are not written by hand: this page is built by handing every example below to the WebAssembly build of yuku-tsrx and reading the tree that comes back.
Code blocks#
A code block is @{ }. It lets statements and markup sit next to each other.
The statements run, and the last markup expression is what the block renders.
In child position, inside an element (code-block.module.tsrx):
const view = (
<section>
before
@{ const label = "ready"; const count = 1; }
after
</section>
);
const rendered = (
<section>
@{ const label = "ready"; <span>{label}</span> }
</section>
);JSXCodeBlock
In expression position, and nested inside another block
(code-block-expression.module.tsrx):
const value = @{ const label = "expr"; <span>{label}</span> };
const nested = (
<section>
@{ const label = "outer"; @{ const inner = label; <span>{inner}</span> } }
</section>
);JSXCodeBlock
As a function body, on a declaration and on an arrow
(code-block-function.module.tsrx):
function View(): unknown @{ const label = "fn"; <span>{label}</span> }
const Arrow = (): unknown => @{ const label = "arrow"; <span>{label}</span> };JSXCodeBlock
Each of these parses to a JSXCodeBlock, whose body is the statements and
whose render is the final markup expression or null.
@if, @else if, @else#
Chained, in child position, and standalone as an expression
(control-flow-if.module.tsrx):
const child = (
<section>
@if (ready) {
const label = "ready";
<span>{label}</span>
} @else @if (fallback) {
<span>fallback</span>
} @else {
<span>empty</span>
}
</section>
);
const value = @if (ready) {
<span>yes</span>
} @else {
<span>no</span>
};JSXIfExpression
The node is a JSXIfExpression with test, consequent, and an alternate
that is another JSXIfExpression, a block, or null. A branch body is a block,
so it can hold statements as well as markup.
@for, with index, key, and @empty#
@for takes a for-of head, and after the iterable it accepts ; index <name>
and ; key <expr> tail clauses. @empty is the branch for an iterable with
nothing in it (control-flow-for.module.tsrx):
const list = (
<section>
@for (const item of items) {
const label = item.toUpperCase();
<span>{label}</span>
} @empty {
<span>empty</span>
}
</section>
);
const indexed = @for (const item of items; index item_index) {
<span>{item_index}:{item}</span>
};
const keyed_list = @for (const item of keyed; index item_index; key item.id) {
<span>{item_index}:{item.id}</span>
};JSXForExpressionForOfStatement.indexForOfStatement.key
A counted C-style header works too:
const counted = @for (let i = 0; i < items.length; i++) {
<span>{items[i]}</span>
};JSXForExpression
The node is a JSXForExpression whose statement is either a ForOfStatement
or a ForStatement, and whose empty is the @empty block or null. The
index and key clauses are two extra fields on the ForOfStatement itself,
so the ordinary for-of shape is preserved and the TSRX-only parts hang off it.
@switch, @case, @default#
Clause bodies are braced blocks, and @case takes a value followed by a colon
(control-flow-switch.module.tsrx):
const view = (
<section>
@switch (status) {
@case "ready": {
<span>ready</span>
}
@case "empty": {
const label = "empty";
<span>{label}</span>
}
@default: {
<span>error</span>
}
}
</section>
);JSXSwitchExpression
The node is a JSXSwitchExpression wrapping an ordinary SwitchStatement.
@try, @pending, @catch#
@try is the async boundary: @pending is what shows while the content is
loading, @catch is what shows when it fails. @catch takes an optional
parameter list, and the second parameter is a reset callback
(control-flow-try.module.tsrx):
const view = (
<section>
@try {
const label = ready ? "ready" : "waiting";
<span>{label}</span>
} @pending {
<span>pending</span>
} @catch (error: Error, reset) {
<button onClick={reset}>{error.message}</button>
}
</section>
);JSXTryExpressionCatchClause.resetParam
Either branch alone is fine:
const pending_only = @try {
<span>ready</span>
} @pending {
<span>pending</span>
};
const catch_only = @try {
<span>ready</span>
} @catch {
<span>failed</span>
};JSXTryExpression
The node is a JSXTryExpression wrapping a TryStatement, with pending as
the @pending block or null. A @try with neither @pending nor @catch is
reported: TSRX try directive requires '@pending' or '@catch'.
Dynamic tags#
A tag name can be an expression, written <{expr}>. The closing tag repeats it
(dynamic-tag.module.tsrx):
export function Panel({ as, title }: { as: Tag; title: string }) {
return (
<{as} className="panel">
<h2>{title}</h2>
</{as}>
);
}
export const Icon = ({ as }: { as: Tag }) => <{as} />;JSXOpeningElement.name
Whitespace inside the braces is allowed, and the opening and closing names still have to match:
export const WithWhitespace = ({ as }: { as: Tag }) => (
<{ as }>
<span />
</{as}>
);JSXOpeningElement.name
<style> elements#
A <style> element holds raw CSS. Its contents are not parsed as JSX, so
braces, comments, and at-rules inside it stay CSS
(style-element.module.tsrx):
const view = (
<section>
<style>
.card {
color: red;
}
/* stays in css */
@media (min-width: 40rem) {
.card {
display: grid;
}
}
</style>
<article className="card">content</article>
</section>
);
const standalone = <style>.inline { color: blue; }</style>;JSXStyleElementStyleSheet
It works from inside a code block as well:
const from_code_block = @{
const tone = "green";
<style>.from-code-block { color: green; }</style>
};JSXCodeBlockJSXStyleElementStyleSheet
The node is a JSXStyleElement. Its children are StyleSheet nodes carrying
the raw source, and the element itself carries the whole css string.
Lazy destructuring patterns#
A destructuring pattern marked with & is a lazy pattern
(lazy-destructuring.module.tsrx). It works in declarations, in assignments,
and in a parameter list:
let &{ title, count: total = 0 } = props;
const &[first, , ...rest] = values;
&{ title } = props;
&[first] = values;
export function pick(&{ title: label }: Props) {
return label;
}ObjectPattern.lazyArrayPattern.lazy
The pattern keeps its ordinary ObjectPattern or ArrayPattern type; the
dialect records the lazy marking alongside it rather than inventing a new node
type, so a consumer that only cares about the bindings does not have to learn
anything new.
Submodule imports#
A module block can be imported from by name, with no string specifier
(submodule-import.module.tsrx):
module server {
export function load() {
return "ok";
}
}
import { load } from server;
export function View(): unknown @{
const value = load();
<span>{value}</span>
}JSXCodeBlockImportDeclaration.source
Text entities#
Entities in JSX text are decoded: the five named ones (", &,
<, >, '), decimal B, and hex B. Anything else is
copied through exactly as written, so &unknown; stays &unknown;
(text-entities.module.tsrx, decoded in src/dialect/text.zig):
const view = (
<p>"ABB&<>'&unknown;</p>
);JSXText.value
What is rejected#
Three fixtures exist to hold the cases that must not parse silently. Each one produces diagnostics rather than a plausible-looking tree.
return inside a template block#
template-return-invalid.module.tsrx. A return inside a @{ } block that
sits in child position is invalid, because the block is a template, not a
function body:
const child = (
<section>
@{
return <span>invalid</span>;
}
</section>
);JSXCodeBlock1 diagnostic
The message is `return` is invalid inside TSRX template blocks, with the
help line "Use rendered output as the final expression instead." The same
fixture shows the legal case: a @{ } that is a function body may return.
break and return inside @switch cases#
control-flow-switch-invalid.module.tsrx. A @case body is not a JavaScript
switch clause, so control transfer out of it is refused:
const view = @switch (status) {
@case "ready": {
break;
}
@case "empty": {
if (status) {
return <span>empty</span>;
}
}
};JSXSwitchExpression2 diagnostics
The messages are `break` is invalid inside `@switch` cases. and
`return` is invalid inside `@switch` cases. A break that belongs to a
loop written inside the case is fine; only a break targeting the case itself
is reported.
Dynamic tag names that are not element names#
dynamic-tag-invalid.module.tsrx. The expression in <{ }> has to resolve to
something that can name an element, and these do not:
const call_tag = <{getTag()} />;
const concat_tag = <{"x" + name} />;
const interpolated_tag = <{`x${name}`} />;
const object_tag = <{{ tag: "div" }} />;
const undefined_tag = <{undefined} />;
const void_tag = <{void 0} />;JSXOpeningElement.name6 diagnostics
The message is TSRX dynamic tag expression must resolve to an element name.
Malformed directives#
A directive that starts but does not complete is reported where it breaks, with a help line naming the shape it expected. The set includes:
| Message | Help |
|---|---|
Expected 'if' after '@' |
TSRX if directives are written '@if (...) { ... }' |
Expected 'else' after '@' |
TSRX else clauses are written '@else { ... }' |
Expected 'switch' after '@' |
TSRX switch directives are written '@switch (...) { ... }' |
Expected '{' to start TSRX switch body |
TSRX switch bodies contain '@case' and '@default' clauses. |
Expected 'try' after '@' |
TSRX try directives are written '@try { ... }'. |
Expected 'catch' after '@' |
TSRX catch clauses are written '@catch { ... }' or '@catch (error) { ... }'. |
Expected '{' after TSRX control-flow directive |
TSRX control-flow bodies are written with braces. |
Every one of these is a diagnostic on the parse result, not a thrown exception.
Parser covers how diagnostics are returned and which of them
parseModule treats as fatal.