summaryrefslogtreecommitdiff
path: root/node_modules/sucrase/dist/transformers/JSXTransformer.js
diff options
context:
space:
mode:
authorPhilipp Tanlak <philipp.tanlak@gmail.com>2025-11-24 20:54:57 +0100
committerPhilipp Tanlak <philipp.tanlak@gmail.com>2025-11-24 20:57:48 +0100
commitb1e2c8fd5cb5dfa46bc440a12eafaf56cd844b1c (patch)
tree49d360fd6cbc6a2754efe93524ac47ff0fbe0f7d /node_modules/sucrase/dist/transformers/JSXTransformer.js
Docs
Diffstat (limited to 'node_modules/sucrase/dist/transformers/JSXTransformer.js')
-rw-r--r--node_modules/sucrase/dist/transformers/JSXTransformer.js733
1 files changed, 733 insertions, 0 deletions
diff --git a/node_modules/sucrase/dist/transformers/JSXTransformer.js b/node_modules/sucrase/dist/transformers/JSXTransformer.js
new file mode 100644
index 0000000..df51be3
--- /dev/null
+++ b/node_modules/sucrase/dist/transformers/JSXTransformer.js
@@ -0,0 +1,733 @@
+"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+
+var _xhtml = require('../parser/plugins/jsx/xhtml'); var _xhtml2 = _interopRequireDefault(_xhtml);
+var _tokenizer = require('../parser/tokenizer');
+var _types = require('../parser/tokenizer/types');
+var _charcodes = require('../parser/util/charcodes');
+
+var _getJSXPragmaInfo = require('../util/getJSXPragmaInfo'); var _getJSXPragmaInfo2 = _interopRequireDefault(_getJSXPragmaInfo);
+
+var _Transformer = require('./Transformer'); var _Transformer2 = _interopRequireDefault(_Transformer);
+
+ class JSXTransformer extends _Transformer2.default {
+
+
+
+
+ // State for calculating the line number of each JSX tag in development.
+ __init() {this.lastLineNumber = 1}
+ __init2() {this.lastIndex = 0}
+
+ // In development, variable name holding the name of the current file.
+ __init3() {this.filenameVarName = null}
+ // Mapping of claimed names for imports in the automatic transform, e,g.
+ // {jsx: "_jsx"}. This determines which imports to generate in the prefix.
+ __init4() {this.esmAutomaticImportNameResolutions = {}}
+ // When automatically adding imports in CJS mode, we store the variable name
+ // holding the imported CJS module so we can require it in the prefix.
+ __init5() {this.cjsAutomaticModuleNameResolutions = {}}
+
+ constructor(
+ rootTransformer,
+ tokens,
+ importProcessor,
+ nameManager,
+ options,
+ ) {
+ super();this.rootTransformer = rootTransformer;this.tokens = tokens;this.importProcessor = importProcessor;this.nameManager = nameManager;this.options = options;JSXTransformer.prototype.__init.call(this);JSXTransformer.prototype.__init2.call(this);JSXTransformer.prototype.__init3.call(this);JSXTransformer.prototype.__init4.call(this);JSXTransformer.prototype.__init5.call(this);;
+ this.jsxPragmaInfo = _getJSXPragmaInfo2.default.call(void 0, options);
+ this.isAutomaticRuntime = options.jsxRuntime === "automatic";
+ this.jsxImportSource = options.jsxImportSource || "react";
+ }
+
+ process() {
+ if (this.tokens.matches1(_types.TokenType.jsxTagStart)) {
+ this.processJSXTag();
+ return true;
+ }
+ return false;
+ }
+
+ getPrefixCode() {
+ let prefix = "";
+ if (this.filenameVarName) {
+ prefix += `const ${this.filenameVarName} = ${JSON.stringify(this.options.filePath || "")};`;
+ }
+ if (this.isAutomaticRuntime) {
+ if (this.importProcessor) {
+ // CJS mode: emit require statements for all modules that were referenced.
+ for (const [path, resolvedName] of Object.entries(this.cjsAutomaticModuleNameResolutions)) {
+ prefix += `var ${resolvedName} = require("${path}");`;
+ }
+ } else {
+ // ESM mode: consolidate and emit import statements for referenced names.
+ const {createElement: createElementResolution, ...otherResolutions} =
+ this.esmAutomaticImportNameResolutions;
+ if (createElementResolution) {
+ prefix += `import {createElement as ${createElementResolution}} from "${this.jsxImportSource}";`;
+ }
+ const importSpecifiers = Object.entries(otherResolutions)
+ .map(([name, resolvedName]) => `${name} as ${resolvedName}`)
+ .join(", ");
+ if (importSpecifiers) {
+ const importPath =
+ this.jsxImportSource + (this.options.production ? "/jsx-runtime" : "/jsx-dev-runtime");
+ prefix += `import {${importSpecifiers}} from "${importPath}";`;
+ }
+ }
+ }
+ return prefix;
+ }
+
+ processJSXTag() {
+ const {jsxRole, start} = this.tokens.currentToken();
+ // Calculate line number information at the very start (if in development
+ // mode) so that the information is guaranteed to be queried in token order.
+ const elementLocationCode = this.options.production ? null : this.getElementLocationCode(start);
+ if (this.isAutomaticRuntime && jsxRole !== _tokenizer.JSXRole.KeyAfterPropSpread) {
+ this.transformTagToJSXFunc(elementLocationCode, jsxRole);
+ } else {
+ this.transformTagToCreateElement(elementLocationCode);
+ }
+ }
+
+ getElementLocationCode(firstTokenStart) {
+ const lineNumber = this.getLineNumberForIndex(firstTokenStart);
+ return `lineNumber: ${lineNumber}`;
+ }
+
+ /**
+ * Get the line number for this source position. This is calculated lazily and
+ * must be called in increasing order by index.
+ */
+ getLineNumberForIndex(index) {
+ const code = this.tokens.code;
+ while (this.lastIndex < index && this.lastIndex < code.length) {
+ if (code[this.lastIndex] === "\n") {
+ this.lastLineNumber++;
+ }
+ this.lastIndex++;
+ }
+ return this.lastLineNumber;
+ }
+
+ /**
+ * Convert the current JSX element to a call to jsx, jsxs, or jsxDEV. This is
+ * the primary transformation for the automatic transform.
+ *
+ * Example:
+ * <div a={1} key={2}>Hello{x}</div>
+ * becomes
+ * jsxs('div', {a: 1, children: ["Hello", x]}, 2)
+ */
+ transformTagToJSXFunc(elementLocationCode, jsxRole) {
+ const isStatic = jsxRole === _tokenizer.JSXRole.StaticChildren;
+ // First tag is always jsxTagStart.
+ this.tokens.replaceToken(this.getJSXFuncInvocationCode(isStatic));
+
+ let keyCode = null;
+ if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
+ // Fragment syntax.
+ this.tokens.replaceToken(`${this.getFragmentCode()}, {`);
+ this.processAutomaticChildrenAndEndProps(jsxRole);
+ } else {
+ // Normal open tag or self-closing tag.
+ this.processTagIntro();
+ this.tokens.appendCode(", {");
+ keyCode = this.processProps(true);
+
+ if (this.tokens.matches2(_types.TokenType.slash, _types.TokenType.jsxTagEnd)) {
+ // Self-closing tag, no children to add, so close the props.
+ this.tokens.appendCode("}");
+ } else if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
+ // Tag with children.
+ this.tokens.removeToken();
+ this.processAutomaticChildrenAndEndProps(jsxRole);
+ } else {
+ throw new Error("Expected either /> or > at the end of the tag.");
+ }
+ // If a key was present, move it to its own arg. Note that moving code
+ // like this will cause line numbers to get out of sync within the JSX
+ // element if the key expression has a newline in it. This is unfortunate,
+ // but hopefully should be rare.
+ if (keyCode) {
+ this.tokens.appendCode(`, ${keyCode}`);
+ }
+ }
+ if (!this.options.production) {
+ // If the key wasn't already added, add it now so we can correctly set
+ // positional args for jsxDEV.
+ if (keyCode === null) {
+ this.tokens.appendCode(", void 0");
+ }
+ this.tokens.appendCode(`, ${isStatic}, ${this.getDevSource(elementLocationCode)}, this`);
+ }
+ // We're at the close-tag or the end of a self-closing tag, so remove
+ // everything else and close the function call.
+ this.tokens.removeInitialToken();
+ while (!this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
+ this.tokens.removeToken();
+ }
+ this.tokens.replaceToken(")");
+ }
+
+ /**
+ * Convert the current JSX element to a createElement call. In the classic
+ * runtime, this is the only case. In the automatic runtime, this is called
+ * as a fallback in some situations.
+ *
+ * Example:
+ * <div a={1} key={2}>Hello{x}</div>
+ * becomes
+ * React.createElement('div', {a: 1, key: 2}, "Hello", x)
+ */
+ transformTagToCreateElement(elementLocationCode) {
+ // First tag is always jsxTagStart.
+ this.tokens.replaceToken(this.getCreateElementInvocationCode());
+
+ if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
+ // Fragment syntax.
+ this.tokens.replaceToken(`${this.getFragmentCode()}, null`);
+ this.processChildren(true);
+ } else {
+ // Normal open tag or self-closing tag.
+ this.processTagIntro();
+ this.processPropsObjectWithDevInfo(elementLocationCode);
+
+ if (this.tokens.matches2(_types.TokenType.slash, _types.TokenType.jsxTagEnd)) {
+ // Self-closing tag; no children to process.
+ } else if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
+ // Tag with children and a close-tag; process the children as args.
+ this.tokens.removeToken();
+ this.processChildren(true);
+ } else {
+ throw new Error("Expected either /> or > at the end of the tag.");
+ }
+ }
+ // We're at the close-tag or the end of a self-closing tag, so remove
+ // everything else and close the function call.
+ this.tokens.removeInitialToken();
+ while (!this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
+ this.tokens.removeToken();
+ }
+ this.tokens.replaceToken(")");
+ }
+
+ /**
+ * Get the code for the relevant function for this context: jsx, jsxs,
+ * or jsxDEV. The following open-paren is included as well.
+ *
+ * These functions are only used for the automatic runtime, so they are always
+ * auto-imported, but the auto-import will be either CJS or ESM based on the
+ * target module format.
+ */
+ getJSXFuncInvocationCode(isStatic) {
+ if (this.options.production) {
+ if (isStatic) {
+ return this.claimAutoImportedFuncInvocation("jsxs", "/jsx-runtime");
+ } else {
+ return this.claimAutoImportedFuncInvocation("jsx", "/jsx-runtime");
+ }
+ } else {
+ return this.claimAutoImportedFuncInvocation("jsxDEV", "/jsx-dev-runtime");
+ }
+ }
+
+ /**
+ * Return the code to use for the createElement function, e.g.
+ * `React.createElement`, including the following open-paren.
+ *
+ * This is the main function to use for the classic runtime. For the
+ * automatic runtime, this function is used as a fallback function to
+ * preserve behavior when there is a prop spread followed by an explicit
+ * key. In that automatic runtime case, the function should be automatically
+ * imported.
+ */
+ getCreateElementInvocationCode() {
+ if (this.isAutomaticRuntime) {
+ return this.claimAutoImportedFuncInvocation("createElement", "");
+ } else {
+ const {jsxPragmaInfo} = this;
+ const resolvedPragmaBaseName = this.importProcessor
+ ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.base) || jsxPragmaInfo.base
+ : jsxPragmaInfo.base;
+ return `${resolvedPragmaBaseName}${jsxPragmaInfo.suffix}(`;
+ }
+ }
+
+ /**
+ * Return the code to use as the component when compiling a shorthand
+ * fragment, e.g. `React.Fragment`.
+ *
+ * This may be called from either the classic or automatic runtime, and
+ * the value should be auto-imported for the automatic runtime.
+ */
+ getFragmentCode() {
+ if (this.isAutomaticRuntime) {
+ return this.claimAutoImportedName(
+ "Fragment",
+ this.options.production ? "/jsx-runtime" : "/jsx-dev-runtime",
+ );
+ } else {
+ const {jsxPragmaInfo} = this;
+ const resolvedFragmentPragmaBaseName = this.importProcessor
+ ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.fragmentBase) ||
+ jsxPragmaInfo.fragmentBase
+ : jsxPragmaInfo.fragmentBase;
+ return resolvedFragmentPragmaBaseName + jsxPragmaInfo.fragmentSuffix;
+ }
+ }
+
+ /**
+ * Return code that invokes the given function.
+ *
+ * When the imports transform is enabled, use the CJSImportTransformer
+ * strategy of using `.call(void 0, ...` to avoid passing a `this` value in a
+ * situation that would otherwise look like a method call.
+ */
+ claimAutoImportedFuncInvocation(funcName, importPathSuffix) {
+ const funcCode = this.claimAutoImportedName(funcName, importPathSuffix);
+ if (this.importProcessor) {
+ return `${funcCode}.call(void 0, `;
+ } else {
+ return `${funcCode}(`;
+ }
+ }
+
+ claimAutoImportedName(funcName, importPathSuffix) {
+ if (this.importProcessor) {
+ // CJS mode: claim a name for the module and mark it for import.
+ const path = this.jsxImportSource + importPathSuffix;
+ if (!this.cjsAutomaticModuleNameResolutions[path]) {
+ this.cjsAutomaticModuleNameResolutions[path] =
+ this.importProcessor.getFreeIdentifierForPath(path);
+ }
+ return `${this.cjsAutomaticModuleNameResolutions[path]}.${funcName}`;
+ } else {
+ // ESM mode: claim a name for this function and add it to the names that
+ // should be auto-imported when the prefix is generated.
+ if (!this.esmAutomaticImportNameResolutions[funcName]) {
+ this.esmAutomaticImportNameResolutions[funcName] = this.nameManager.claimFreeName(
+ `_${funcName}`,
+ );
+ }
+ return this.esmAutomaticImportNameResolutions[funcName];
+ }
+ }
+
+ /**
+ * Process the first part of a tag, before any props.
+ */
+ processTagIntro() {
+ // Walk forward until we see one of these patterns:
+ // jsxName to start the first prop, preceded by another jsxName to end the tag name.
+ // jsxName to start the first prop, preceded by greaterThan to end the type argument.
+ // [open brace] to start the first prop.
+ // [jsxTagEnd] to end the open-tag.
+ // [slash, jsxTagEnd] to end the self-closing tag.
+ let introEnd = this.tokens.currentIndex() + 1;
+ while (
+ this.tokens.tokens[introEnd].isType ||
+ (!this.tokens.matches2AtIndex(introEnd - 1, _types.TokenType.jsxName, _types.TokenType.jsxName) &&
+ !this.tokens.matches2AtIndex(introEnd - 1, _types.TokenType.greaterThan, _types.TokenType.jsxName) &&
+ !this.tokens.matches1AtIndex(introEnd, _types.TokenType.braceL) &&
+ !this.tokens.matches1AtIndex(introEnd, _types.TokenType.jsxTagEnd) &&
+ !this.tokens.matches2AtIndex(introEnd, _types.TokenType.slash, _types.TokenType.jsxTagEnd))
+ ) {
+ introEnd++;
+ }
+ if (introEnd === this.tokens.currentIndex() + 1) {
+ const tagName = this.tokens.identifierName();
+ if (startsWithLowerCase(tagName)) {
+ this.tokens.replaceToken(`'${tagName}'`);
+ }
+ }
+ while (this.tokens.currentIndex() < introEnd) {
+ this.rootTransformer.processToken();
+ }
+ }
+
+ /**
+ * Starting at the beginning of the props, add the props argument to
+ * React.createElement, including the comma before it.
+ */
+ processPropsObjectWithDevInfo(elementLocationCode) {
+ const devProps = this.options.production
+ ? ""
+ : `__self: this, __source: ${this.getDevSource(elementLocationCode)}`;
+ if (!this.tokens.matches1(_types.TokenType.jsxName) && !this.tokens.matches1(_types.TokenType.braceL)) {
+ if (devProps) {
+ this.tokens.appendCode(`, {${devProps}}`);
+ } else {
+ this.tokens.appendCode(`, null`);
+ }
+ return;
+ }
+ this.tokens.appendCode(`, {`);
+ this.processProps(false);
+ if (devProps) {
+ this.tokens.appendCode(` ${devProps}}`);
+ } else {
+ this.tokens.appendCode("}");
+ }
+ }
+
+ /**
+ * Transform the core part of the props, assuming that a { has already been
+ * inserted before us and that a } will be inserted after us.
+ *
+ * If extractKeyCode is true (i.e. when using any jsx... function), any prop
+ * named "key" has its code captured and returned rather than being emitted to
+ * the output code. This shifts line numbers, and emitting the code later will
+ * correct line numbers again. If no key is found or if extractKeyCode is
+ * false, this function returns null.
+ */
+ processProps(extractKeyCode) {
+ let keyCode = null;
+ while (true) {
+ if (this.tokens.matches2(_types.TokenType.jsxName, _types.TokenType.eq)) {
+ // This is a regular key={value} or key="value" prop.
+ const propName = this.tokens.identifierName();
+ if (extractKeyCode && propName === "key") {
+ if (keyCode !== null) {
+ // The props list has multiple keys. Different implementations are
+ // inconsistent about what to do here: as of this writing, Babel and
+ // swc keep the *last* key and completely remove the rest, while
+ // TypeScript uses the *first* key and leaves the others as regular
+ // props. The React team collaborated with Babel on the
+ // implementation of this behavior, so presumably the Babel behavior
+ // is the one to use.
+ // Since we won't ever be emitting the previous key code, we need to
+ // at least emit its newlines here so that the line numbers match up
+ // in the long run.
+ this.tokens.appendCode(keyCode.replace(/[^\n]/g, ""));
+ }
+ // key
+ this.tokens.removeToken();
+ // =
+ this.tokens.removeToken();
+ const snapshot = this.tokens.snapshot();
+ this.processPropValue();
+ keyCode = this.tokens.dangerouslyGetAndRemoveCodeSinceSnapshot(snapshot);
+ // Don't add a comma
+ continue;
+ } else {
+ this.processPropName(propName);
+ this.tokens.replaceToken(": ");
+ this.processPropValue();
+ }
+ } else if (this.tokens.matches1(_types.TokenType.jsxName)) {
+ // This is a shorthand prop like <input disabled />.
+ const propName = this.tokens.identifierName();
+ this.processPropName(propName);
+ this.tokens.appendCode(": true");
+ } else if (this.tokens.matches1(_types.TokenType.braceL)) {
+ // This is prop spread, like <div {...getProps()}>, which we can pass
+ // through fairly directly as an object spread.
+ this.tokens.replaceToken("");
+ this.rootTransformer.processBalancedCode();
+ this.tokens.replaceToken("");
+ } else {
+ break;
+ }
+ this.tokens.appendCode(",");
+ }
+ return keyCode;
+ }
+
+ processPropName(propName) {
+ if (propName.includes("-")) {
+ this.tokens.replaceToken(`'${propName}'`);
+ } else {
+ this.tokens.copyToken();
+ }
+ }
+
+ processPropValue() {
+ if (this.tokens.matches1(_types.TokenType.braceL)) {
+ this.tokens.replaceToken("");
+ this.rootTransformer.processBalancedCode();
+ this.tokens.replaceToken("");
+ } else if (this.tokens.matches1(_types.TokenType.jsxTagStart)) {
+ this.processJSXTag();
+ } else {
+ this.processStringPropValue();
+ }
+ }
+
+ processStringPropValue() {
+ const token = this.tokens.currentToken();
+ const valueCode = this.tokens.code.slice(token.start + 1, token.end - 1);
+ const replacementCode = formatJSXTextReplacement(valueCode);
+ const literalCode = formatJSXStringValueLiteral(valueCode);
+ this.tokens.replaceToken(literalCode + replacementCode);
+ }
+
+ /**
+ * Starting in the middle of the props object literal, produce an additional
+ * prop for the children and close the object literal.
+ */
+ processAutomaticChildrenAndEndProps(jsxRole) {
+ if (jsxRole === _tokenizer.JSXRole.StaticChildren) {
+ this.tokens.appendCode(" children: [");
+ this.processChildren(false);
+ this.tokens.appendCode("]}");
+ } else {
+ // The parser information tells us whether we will see a real child or if
+ // all remaining children (if any) will resolve to empty. If there are no
+ // non-empty children, don't emit a children prop at all, but still
+ // process children so that we properly transform the code into nothing.
+ if (jsxRole === _tokenizer.JSXRole.OneChild) {
+ this.tokens.appendCode(" children: ");
+ }
+ this.processChildren(false);
+ this.tokens.appendCode("}");
+ }
+ }
+
+ /**
+ * Transform children into a comma-separated list, which will be either
+ * arguments to createElement or array elements of a children prop.
+ */
+ processChildren(needsInitialComma) {
+ let needsComma = needsInitialComma;
+ while (true) {
+ if (this.tokens.matches2(_types.TokenType.jsxTagStart, _types.TokenType.slash)) {
+ // Closing tag, so no more children.
+ return;
+ }
+ let didEmitElement = false;
+ if (this.tokens.matches1(_types.TokenType.braceL)) {
+ if (this.tokens.matches2(_types.TokenType.braceL, _types.TokenType.braceR)) {
+ // Empty interpolations and comment-only interpolations are allowed
+ // and don't create an extra child arg.
+ this.tokens.replaceToken("");
+ this.tokens.replaceToken("");
+ } else {
+ // Interpolated expression.
+ this.tokens.replaceToken(needsComma ? ", " : "");
+ this.rootTransformer.processBalancedCode();
+ this.tokens.replaceToken("");
+ didEmitElement = true;
+ }
+ } else if (this.tokens.matches1(_types.TokenType.jsxTagStart)) {
+ // Child JSX element
+ this.tokens.appendCode(needsComma ? ", " : "");
+ this.processJSXTag();
+ didEmitElement = true;
+ } else if (this.tokens.matches1(_types.TokenType.jsxText) || this.tokens.matches1(_types.TokenType.jsxEmptyText)) {
+ didEmitElement = this.processChildTextElement(needsComma);
+ } else {
+ throw new Error("Unexpected token when processing JSX children.");
+ }
+ if (didEmitElement) {
+ needsComma = true;
+ }
+ }
+ }
+
+ /**
+ * Turn a JSX text element into a string literal, or nothing at all if the JSX
+ * text resolves to the empty string.
+ *
+ * Returns true if a string literal is emitted, false otherwise.
+ */
+ processChildTextElement(needsComma) {
+ const token = this.tokens.currentToken();
+ const valueCode = this.tokens.code.slice(token.start, token.end);
+ const replacementCode = formatJSXTextReplacement(valueCode);
+ const literalCode = formatJSXTextLiteral(valueCode);
+ if (literalCode === '""') {
+ this.tokens.replaceToken(replacementCode);
+ return false;
+ } else {
+ this.tokens.replaceToken(`${needsComma ? ", " : ""}${literalCode}${replacementCode}`);
+ return true;
+ }
+ }
+
+ getDevSource(elementLocationCode) {
+ return `{fileName: ${this.getFilenameVarName()}, ${elementLocationCode}}`;
+ }
+
+ getFilenameVarName() {
+ if (!this.filenameVarName) {
+ this.filenameVarName = this.nameManager.claimFreeName("_jsxFileName");
+ }
+ return this.filenameVarName;
+ }
+} exports.default = JSXTransformer;
+
+/**
+ * Spec for identifiers: https://tc39.github.io/ecma262/#prod-IdentifierStart.
+ *
+ * Really only treat anything starting with a-z as tag names. `_`, `$`, `é`
+ * should be treated as component names
+ */
+ function startsWithLowerCase(s) {
+ const firstChar = s.charCodeAt(0);
+ return firstChar >= _charcodes.charCodes.lowercaseA && firstChar <= _charcodes.charCodes.lowercaseZ;
+} exports.startsWithLowerCase = startsWithLowerCase;
+
+/**
+ * Turn the given jsxText string into a JS string literal. Leading and trailing
+ * whitespace on lines is removed, except immediately after the open-tag and
+ * before the close-tag. Empty lines are completely removed, and spaces are
+ * added between lines after that.
+ *
+ * We use JSON.stringify to introduce escape characters as necessary, and trim
+ * the start and end of each line and remove blank lines.
+ */
+function formatJSXTextLiteral(text) {
+ let result = "";
+ let whitespace = "";
+
+ let isInInitialLineWhitespace = false;
+ let seenNonWhitespace = false;
+ for (let i = 0; i < text.length; i++) {
+ const c = text[i];
+ if (c === " " || c === "\t" || c === "\r") {
+ if (!isInInitialLineWhitespace) {
+ whitespace += c;
+ }
+ } else if (c === "\n") {
+ whitespace = "";
+ isInInitialLineWhitespace = true;
+ } else {
+ if (seenNonWhitespace && isInInitialLineWhitespace) {
+ result += " ";
+ }
+ result += whitespace;
+ whitespace = "";
+ if (c === "&") {
+ const {entity, newI} = processEntity(text, i + 1);
+ i = newI - 1;
+ result += entity;
+ } else {
+ result += c;
+ }
+ seenNonWhitespace = true;
+ isInInitialLineWhitespace = false;
+ }
+ }
+ if (!isInInitialLineWhitespace) {
+ result += whitespace;
+ }
+ return JSON.stringify(result);
+}
+
+/**
+ * Produce the code that should be printed after the JSX text string literal,
+ * with most content removed, but all newlines preserved and all spacing at the
+ * end preserved.
+ */
+function formatJSXTextReplacement(text) {
+ let numNewlines = 0;
+ let numSpaces = 0;
+ for (const c of text) {
+ if (c === "\n") {
+ numNewlines++;
+ numSpaces = 0;
+ } else if (c === " ") {
+ numSpaces++;
+ }
+ }
+ return "\n".repeat(numNewlines) + " ".repeat(numSpaces);
+}
+
+/**
+ * Format a string in the value position of a JSX prop.
+ *
+ * Use the same implementation as convertAttribute from
+ * babel-helper-builder-react-jsx.
+ */
+function formatJSXStringValueLiteral(text) {
+ let result = "";
+ for (let i = 0; i < text.length; i++) {
+ const c = text[i];
+ if (c === "\n") {
+ if (/\s/.test(text[i + 1])) {
+ result += " ";
+ while (i < text.length && /\s/.test(text[i + 1])) {
+ i++;
+ }
+ } else {
+ result += "\n";
+ }
+ } else if (c === "&") {
+ const {entity, newI} = processEntity(text, i + 1);
+ result += entity;
+ i = newI - 1;
+ } else {
+ result += c;
+ }
+ }
+ return JSON.stringify(result);
+}
+
+/**
+ * Starting at a &, see if there's an HTML entity (specified by name, decimal
+ * char code, or hex char code) and return it if so.
+ *
+ * Modified from jsxReadString in babel-parser.
+ */
+function processEntity(text, indexAfterAmpersand) {
+ let str = "";
+ let count = 0;
+ let entity;
+ let i = indexAfterAmpersand;
+
+ if (text[i] === "#") {
+ let radix = 10;
+ i++;
+ let numStart;
+ if (text[i] === "x") {
+ radix = 16;
+ i++;
+ numStart = i;
+ while (i < text.length && isHexDigit(text.charCodeAt(i))) {
+ i++;
+ }
+ } else {
+ numStart = i;
+ while (i < text.length && isDecimalDigit(text.charCodeAt(i))) {
+ i++;
+ }
+ }
+ if (text[i] === ";") {
+ const numStr = text.slice(numStart, i);
+ if (numStr) {
+ i++;
+ entity = String.fromCodePoint(parseInt(numStr, radix));
+ }
+ }
+ } else {
+ while (i < text.length && count++ < 10) {
+ const ch = text[i];
+ i++;
+ if (ch === ";") {
+ entity = _xhtml2.default.get(str);
+ break;
+ }
+ str += ch;
+ }
+ }
+
+ if (!entity) {
+ return {entity: "&", newI: indexAfterAmpersand};
+ }
+ return {entity, newI: i};
+}
+
+function isDecimalDigit(code) {
+ return code >= _charcodes.charCodes.digit0 && code <= _charcodes.charCodes.digit9;
+}
+
+function isHexDigit(code) {
+ return (
+ (code >= _charcodes.charCodes.digit0 && code <= _charcodes.charCodes.digit9) ||
+ (code >= _charcodes.charCodes.lowercaseA && code <= _charcodes.charCodes.lowercaseF) ||
+ (code >= _charcodes.charCodes.uppercaseA && code <= _charcodes.charCodes.uppercaseF)
+ );
+}