Files
overlookmotel d50c985fc2 refactor(packages/codegen): split categories into separate file (#26083)
Pure refactor. Move the category consts (`CAT_IDENT` etc) into a separate file.

2 reasons:

1. `write.ts` had got too large.
2. `index.ts`, which is not part of printer builds, imports `CAT_OTHER`. Importing it from a file containing only consts avoids any problems with minifier not being able to recognise dead code and remove it.
2026-08-25 13:46:38 +00:00

253 lines
7.8 KiB
TypeScript

// JSX.
// Port of `gen.rs` JSX section.
import { CAT_OTHER } from "./categories.ts";
import {
write,
writeNoLast,
writeWithMap,
writeWithMapEnd,
writeWithMapNamedJSXNoLast,
writeWithMapNoLast,
} from "./write.ts";
import { printExpression } from "./expression.ts";
import { CTX_NONE } from "./operators.ts";
import { PREC_COMMA, PREC_LOWEST } from "./precedence.ts";
import { printTypeArguments } from "./typescript.ts";
import type { State } from "../state.ts";
import type { UnknownNode } from "./types.ts";
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
/**
* Print a JSX element, its attributes and its children.
*
* An element with no closing element is self-closing and gets ` />`, which is also the only place
* a space is required before the slash.
*/
export function printJSXElement(node: ESTree.JSXElement, state: State): void {
const { openingElement } = node;
writeWithMapNoLast(state, "<", openingElement.start, openingElement.end, openingElement);
printJSXElementName(openingElement.name, state);
if (TS) printTypeArguments(openingElement.typeArguments, state);
const { attributes } = openingElement;
const { length } = attributes;
for (let i = 0; i < length; i++) {
writeNoLast(state, " ");
const attribute = attributes[i];
if (attribute.type === "JSXSpreadAttribute") {
writeWithMap(state, "{...", CAT_OTHER, attribute.start, attribute.end, attribute);
printExpression(attribute.argument, state, PREC_COMMA, CTX_NONE);
writeWithMapEnd(state, "}", CAT_OTHER, attribute.start, attribute.end, attribute);
} else {
printJSXAttribute(attribute, state);
}
}
const { closingElement } = node;
if (closingElement == null) {
write(state, " />", CAT_OTHER);
return;
}
writeNoLast(state, ">");
const { children } = node;
const childCount = children.length;
for (let i = 0; i < childCount; i++) {
printJSXChild(children[i], state);
}
writeWithMapNoLast(state, "</", closingElement.start, closingElement.end, closingElement);
printJSXElementName(closingElement.name, state);
write(state, ">", CAT_OTHER);
}
/**
* Print the name in an opening or closing tag - a name, a namespaced name, a member chain, or `this`.
*/
function printJSXElementName(
node: ESTree.JSXElementName | ESTree.ThisExpression | UnknownNode,
state: State,
): void {
switch (node.type) {
case "JSXIdentifier":
writeWithMapNamedJSXNoLast(state, node.name, node.start, node.end, node);
break;
case "JSXMemberExpression":
printJSXElementName(node.object, state);
writeNoLast(state, ".");
printJSXElementName(node.property, state);
break;
case "JSXNamespacedName":
writeWithMapNamedJSXNoLast(
state,
node.namespace.name,
node.namespace.start,
node.namespace.end,
node.namespace,
);
writeNoLast(state, ":");
writeWithMapNamedJSXNoLast(state, node.name.name, node.name.start, node.name.end, node.name);
break;
case "ThisExpression":
writeWithMapNoLast(state, "this", node.start, node.end, node);
break;
default:
throw new Error(`Unknown JSX name type: ${node.type}`);
}
}
/**
* Print one attribute, with its value where it has one - a bare attribute is `true`.
*/
function printJSXAttribute(node: ESTree.JSXAttribute, state: State): void {
// Attribute names never update `last`.
// Everything which can follow one - `=`, the next attribute's leading space, `>`, ` />` -
// is punctuation needing no separation from what precedes it, and nothing between here
// and the next real write reads `last`.
const { name } = node;
if (name.type === "JSXNamespacedName") {
writeWithMapNamedJSXNoLast(
state,
name.namespace.name,
name.namespace.start,
name.namespace.end,
name.namespace,
);
writeNoLast(state, ":");
writeWithMapNamedJSXNoLast(state, name.name.name, name.name.start, name.name.end, name.name);
} else {
writeWithMapNamedJSXNoLast(state, name.name, name.start, name.end, name);
}
const { value } = node;
if (value != null) {
writeNoLast(state, "=");
printJSXAttributeValue(value, state);
}
}
/**
* Print an attribute's value.
*
* A string value is printed from its raw text, not through the string escaper, because JSX attribute strings
* have no escape sequences - the quote is picked to suit the contents instead.
*/
function printJSXAttributeValue(node: ESTree.JSXAttributeValue | UnknownNode, state: State): void {
switch (node.type) {
case "Literal": {
// JSX strings have no escape sequences and HTML entities are not decoded by parser at present.
// Print the raw source text (matching Oxc), choosing only the quote.
//
// Once HTML entities are decoded (which they should be), this check stops being enough -
// `<Foo bar="'&quot;" />` decodes to a value holding both quote characters, which no choice
// of quote mark can contain, so the text will have to be escaped.
// The cases in `test/print.test.ts` pin the current output, so they will fail the moment
// the parser starts decoding, rather than the change slipping through here unnoticed.
const { raw } = node;
const text = raw != null ? raw.slice(1, -1) : String(node.value);
const quote = text.includes('"') ? "'" : '"';
writeNoLast(state, quote);
writeNoLast(state, text);
writeNoLast(state, quote);
break;
}
case "JSXExpressionContainer":
printJSXExpressionContainer(node, state);
break;
case "JSXElement":
printJSXElement(node, state);
break;
case "JSXFragment":
printJSXFragment(node, state);
break;
default:
throw new Error(`Unknown JSX attribute value type: ${node.type}`);
}
}
/**
* Print a `{ … }` container, which may hold nothing but a comment.
*/
function printJSXExpressionContainer(node: ESTree.JSXExpressionContainer, state: State): void {
write(state, "{", CAT_OTHER);
if (node.expression.type !== "JSXEmptyExpression") {
printExpression(node.expression, state, PREC_LOWEST, CTX_NONE);
}
// `}` needs no category. What follows it in either caller context (attribute value, child)
// is punctuation or JSX text, none of which needs separating from it, and nothing reads
// `last` before the next real write.
writeNoLast(state, "}");
}
/**
* Print a `<>...</>` fragment.
*/
export function printJSXFragment(node: ESTree.JSXFragment, state: State): void {
writeWithMapNoLast(
state,
"<>",
node.openingFragment.start,
node.openingFragment.end,
node.openingFragment,
);
const { children } = node;
const { length } = children;
for (let i = 0; i < length; i++) {
printJSXChild(children[i], state);
}
writeWithMap(
state,
"</>",
CAT_OTHER,
node.closingFragment.start,
node.closingFragment.end,
node.closingFragment,
);
}
/**
* Print one child of an element or fragment.
*
* Text is written from its raw source, since whitespace between elements is significant
* and any entities in it must survive untouched.
*/
function printJSXChild(node: ESTree.JSXChild | UnknownNode, state: State): void {
switch (node.type) {
case "JSXText":
writeWithMapNoLast(
state,
node.raw != null ? node.raw : node.value,
node.start,
node.end,
node,
);
break;
case "JSXExpressionContainer":
printJSXExpressionContainer(node, state);
break;
case "JSXElement":
printJSXElement(node, state);
break;
case "JSXFragment":
printJSXFragment(node, state);
break;
case "JSXSpreadChild":
write(state, "{...", CAT_OTHER);
printExpression(node.expression, state, PREC_LOWEST, CTX_NONE);
writeNoLast(state, "}");
break;
default:
throw new Error(`Unknown JSX child type: ${node.type}`);
}
}