JSXTransformer.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  1. import XHTMLEntities from "../parser/plugins/jsx/xhtml";
  2. import {JSXRole} from "../parser/tokenizer";
  3. import {TokenType as tt} from "../parser/tokenizer/types";
  4. import {charCodes} from "../parser/util/charcodes";
  5. import getJSXPragmaInfo, {} from "../util/getJSXPragmaInfo";
  6. import Transformer from "./Transformer";
  7. export default class JSXTransformer extends Transformer {
  8. // State for calculating the line number of each JSX tag in development.
  9. __init() {this.lastLineNumber = 1}
  10. __init2() {this.lastIndex = 0}
  11. // In development, variable name holding the name of the current file.
  12. __init3() {this.filenameVarName = null}
  13. // Mapping of claimed names for imports in the automatic transform, e,g.
  14. // {jsx: "_jsx"}. This determines which imports to generate in the prefix.
  15. __init4() {this.esmAutomaticImportNameResolutions = {}}
  16. // When automatically adding imports in CJS mode, we store the variable name
  17. // holding the imported CJS module so we can require it in the prefix.
  18. __init5() {this.cjsAutomaticModuleNameResolutions = {}}
  19. constructor(
  20. rootTransformer,
  21. tokens,
  22. importProcessor,
  23. nameManager,
  24. options,
  25. ) {
  26. 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);;
  27. this.jsxPragmaInfo = getJSXPragmaInfo(options);
  28. this.isAutomaticRuntime = options.jsxRuntime === "automatic";
  29. this.jsxImportSource = options.jsxImportSource || "react";
  30. }
  31. process() {
  32. if (this.tokens.matches1(tt.jsxTagStart)) {
  33. this.processJSXTag();
  34. return true;
  35. }
  36. return false;
  37. }
  38. getPrefixCode() {
  39. let prefix = "";
  40. if (this.filenameVarName) {
  41. prefix += `const ${this.filenameVarName} = ${JSON.stringify(this.options.filePath || "")};`;
  42. }
  43. if (this.isAutomaticRuntime) {
  44. if (this.importProcessor) {
  45. // CJS mode: emit require statements for all modules that were referenced.
  46. for (const [path, resolvedName] of Object.entries(this.cjsAutomaticModuleNameResolutions)) {
  47. prefix += `var ${resolvedName} = require("${path}");`;
  48. }
  49. } else {
  50. // ESM mode: consolidate and emit import statements for referenced names.
  51. const {createElement: createElementResolution, ...otherResolutions} =
  52. this.esmAutomaticImportNameResolutions;
  53. if (createElementResolution) {
  54. prefix += `import {createElement as ${createElementResolution}} from "${this.jsxImportSource}";`;
  55. }
  56. const importSpecifiers = Object.entries(otherResolutions)
  57. .map(([name, resolvedName]) => `${name} as ${resolvedName}`)
  58. .join(", ");
  59. if (importSpecifiers) {
  60. const importPath =
  61. this.jsxImportSource + (this.options.production ? "/jsx-runtime" : "/jsx-dev-runtime");
  62. prefix += `import {${importSpecifiers}} from "${importPath}";`;
  63. }
  64. }
  65. }
  66. return prefix;
  67. }
  68. processJSXTag() {
  69. const {jsxRole, start} = this.tokens.currentToken();
  70. // Calculate line number information at the very start (if in development
  71. // mode) so that the information is guaranteed to be queried in token order.
  72. const elementLocationCode = this.options.production ? null : this.getElementLocationCode(start);
  73. if (this.isAutomaticRuntime && jsxRole !== JSXRole.KeyAfterPropSpread) {
  74. this.transformTagToJSXFunc(elementLocationCode, jsxRole);
  75. } else {
  76. this.transformTagToCreateElement(elementLocationCode);
  77. }
  78. }
  79. getElementLocationCode(firstTokenStart) {
  80. const lineNumber = this.getLineNumberForIndex(firstTokenStart);
  81. return `lineNumber: ${lineNumber}`;
  82. }
  83. /**
  84. * Get the line number for this source position. This is calculated lazily and
  85. * must be called in increasing order by index.
  86. */
  87. getLineNumberForIndex(index) {
  88. const code = this.tokens.code;
  89. while (this.lastIndex < index && this.lastIndex < code.length) {
  90. if (code[this.lastIndex] === "\n") {
  91. this.lastLineNumber++;
  92. }
  93. this.lastIndex++;
  94. }
  95. return this.lastLineNumber;
  96. }
  97. /**
  98. * Convert the current JSX element to a call to jsx, jsxs, or jsxDEV. This is
  99. * the primary transformation for the automatic transform.
  100. *
  101. * Example:
  102. * <div a={1} key={2}>Hello{x}</div>
  103. * becomes
  104. * jsxs('div', {a: 1, children: ["Hello", x]}, 2)
  105. */
  106. transformTagToJSXFunc(elementLocationCode, jsxRole) {
  107. const isStatic = jsxRole === JSXRole.StaticChildren;
  108. // First tag is always jsxTagStart.
  109. this.tokens.replaceToken(this.getJSXFuncInvocationCode(isStatic));
  110. let keyCode = null;
  111. if (this.tokens.matches1(tt.jsxTagEnd)) {
  112. // Fragment syntax.
  113. this.tokens.replaceToken(`${this.getFragmentCode()}, {`);
  114. this.processAutomaticChildrenAndEndProps(jsxRole);
  115. } else {
  116. // Normal open tag or self-closing tag.
  117. this.processTagIntro();
  118. this.tokens.appendCode(", {");
  119. keyCode = this.processProps(true);
  120. if (this.tokens.matches2(tt.slash, tt.jsxTagEnd)) {
  121. // Self-closing tag, no children to add, so close the props.
  122. this.tokens.appendCode("}");
  123. } else if (this.tokens.matches1(tt.jsxTagEnd)) {
  124. // Tag with children.
  125. this.tokens.removeToken();
  126. this.processAutomaticChildrenAndEndProps(jsxRole);
  127. } else {
  128. throw new Error("Expected either /> or > at the end of the tag.");
  129. }
  130. // If a key was present, move it to its own arg. Note that moving code
  131. // like this will cause line numbers to get out of sync within the JSX
  132. // element if the key expression has a newline in it. This is unfortunate,
  133. // but hopefully should be rare.
  134. if (keyCode) {
  135. this.tokens.appendCode(`, ${keyCode}`);
  136. }
  137. }
  138. if (!this.options.production) {
  139. // If the key wasn't already added, add it now so we can correctly set
  140. // positional args for jsxDEV.
  141. if (keyCode === null) {
  142. this.tokens.appendCode(", void 0");
  143. }
  144. this.tokens.appendCode(`, ${isStatic}, ${this.getDevSource(elementLocationCode)}, this`);
  145. }
  146. // We're at the close-tag or the end of a self-closing tag, so remove
  147. // everything else and close the function call.
  148. this.tokens.removeInitialToken();
  149. while (!this.tokens.matches1(tt.jsxTagEnd)) {
  150. this.tokens.removeToken();
  151. }
  152. this.tokens.replaceToken(")");
  153. }
  154. /**
  155. * Convert the current JSX element to a createElement call. In the classic
  156. * runtime, this is the only case. In the automatic runtime, this is called
  157. * as a fallback in some situations.
  158. *
  159. * Example:
  160. * <div a={1} key={2}>Hello{x}</div>
  161. * becomes
  162. * React.createElement('div', {a: 1, key: 2}, "Hello", x)
  163. */
  164. transformTagToCreateElement(elementLocationCode) {
  165. // First tag is always jsxTagStart.
  166. this.tokens.replaceToken(this.getCreateElementInvocationCode());
  167. if (this.tokens.matches1(tt.jsxTagEnd)) {
  168. // Fragment syntax.
  169. this.tokens.replaceToken(`${this.getFragmentCode()}, null`);
  170. this.processChildren(true);
  171. } else {
  172. // Normal open tag or self-closing tag.
  173. this.processTagIntro();
  174. this.processPropsObjectWithDevInfo(elementLocationCode);
  175. if (this.tokens.matches2(tt.slash, tt.jsxTagEnd)) {
  176. // Self-closing tag; no children to process.
  177. } else if (this.tokens.matches1(tt.jsxTagEnd)) {
  178. // Tag with children and a close-tag; process the children as args.
  179. this.tokens.removeToken();
  180. this.processChildren(true);
  181. } else {
  182. throw new Error("Expected either /> or > at the end of the tag.");
  183. }
  184. }
  185. // We're at the close-tag or the end of a self-closing tag, so remove
  186. // everything else and close the function call.
  187. this.tokens.removeInitialToken();
  188. while (!this.tokens.matches1(tt.jsxTagEnd)) {
  189. this.tokens.removeToken();
  190. }
  191. this.tokens.replaceToken(")");
  192. }
  193. /**
  194. * Get the code for the relevant function for this context: jsx, jsxs,
  195. * or jsxDEV. The following open-paren is included as well.
  196. *
  197. * These functions are only used for the automatic runtime, so they are always
  198. * auto-imported, but the auto-import will be either CJS or ESM based on the
  199. * target module format.
  200. */
  201. getJSXFuncInvocationCode(isStatic) {
  202. if (this.options.production) {
  203. if (isStatic) {
  204. return this.claimAutoImportedFuncInvocation("jsxs", "/jsx-runtime");
  205. } else {
  206. return this.claimAutoImportedFuncInvocation("jsx", "/jsx-runtime");
  207. }
  208. } else {
  209. return this.claimAutoImportedFuncInvocation("jsxDEV", "/jsx-dev-runtime");
  210. }
  211. }
  212. /**
  213. * Return the code to use for the createElement function, e.g.
  214. * `React.createElement`, including the following open-paren.
  215. *
  216. * This is the main function to use for the classic runtime. For the
  217. * automatic runtime, this function is used as a fallback function to
  218. * preserve behavior when there is a prop spread followed by an explicit
  219. * key. In that automatic runtime case, the function should be automatically
  220. * imported.
  221. */
  222. getCreateElementInvocationCode() {
  223. if (this.isAutomaticRuntime) {
  224. return this.claimAutoImportedFuncInvocation("createElement", "");
  225. } else {
  226. const {jsxPragmaInfo} = this;
  227. const resolvedPragmaBaseName = this.importProcessor
  228. ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.base) || jsxPragmaInfo.base
  229. : jsxPragmaInfo.base;
  230. return `${resolvedPragmaBaseName}${jsxPragmaInfo.suffix}(`;
  231. }
  232. }
  233. /**
  234. * Return the code to use as the component when compiling a shorthand
  235. * fragment, e.g. `React.Fragment`.
  236. *
  237. * This may be called from either the classic or automatic runtime, and
  238. * the value should be auto-imported for the automatic runtime.
  239. */
  240. getFragmentCode() {
  241. if (this.isAutomaticRuntime) {
  242. return this.claimAutoImportedName(
  243. "Fragment",
  244. this.options.production ? "/jsx-runtime" : "/jsx-dev-runtime",
  245. );
  246. } else {
  247. const {jsxPragmaInfo} = this;
  248. const resolvedFragmentPragmaBaseName = this.importProcessor
  249. ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.fragmentBase) ||
  250. jsxPragmaInfo.fragmentBase
  251. : jsxPragmaInfo.fragmentBase;
  252. return resolvedFragmentPragmaBaseName + jsxPragmaInfo.fragmentSuffix;
  253. }
  254. }
  255. /**
  256. * Return code that invokes the given function.
  257. *
  258. * When the imports transform is enabled, use the CJSImportTransformer
  259. * strategy of using `.call(void 0, ...` to avoid passing a `this` value in a
  260. * situation that would otherwise look like a method call.
  261. */
  262. claimAutoImportedFuncInvocation(funcName, importPathSuffix) {
  263. const funcCode = this.claimAutoImportedName(funcName, importPathSuffix);
  264. if (this.importProcessor) {
  265. return `${funcCode}.call(void 0, `;
  266. } else {
  267. return `${funcCode}(`;
  268. }
  269. }
  270. claimAutoImportedName(funcName, importPathSuffix) {
  271. if (this.importProcessor) {
  272. // CJS mode: claim a name for the module and mark it for import.
  273. const path = this.jsxImportSource + importPathSuffix;
  274. if (!this.cjsAutomaticModuleNameResolutions[path]) {
  275. this.cjsAutomaticModuleNameResolutions[path] =
  276. this.importProcessor.getFreeIdentifierForPath(path);
  277. }
  278. return `${this.cjsAutomaticModuleNameResolutions[path]}.${funcName}`;
  279. } else {
  280. // ESM mode: claim a name for this function and add it to the names that
  281. // should be auto-imported when the prefix is generated.
  282. if (!this.esmAutomaticImportNameResolutions[funcName]) {
  283. this.esmAutomaticImportNameResolutions[funcName] = this.nameManager.claimFreeName(
  284. `_${funcName}`,
  285. );
  286. }
  287. return this.esmAutomaticImportNameResolutions[funcName];
  288. }
  289. }
  290. /**
  291. * Process the first part of a tag, before any props.
  292. */
  293. processTagIntro() {
  294. // Walk forward until we see one of these patterns:
  295. // jsxName to start the first prop, preceded by another jsxName to end the tag name.
  296. // jsxName to start the first prop, preceded by greaterThan to end the type argument.
  297. // [open brace] to start the first prop.
  298. // [jsxTagEnd] to end the open-tag.
  299. // [slash, jsxTagEnd] to end the self-closing tag.
  300. let introEnd = this.tokens.currentIndex() + 1;
  301. while (
  302. this.tokens.tokens[introEnd].isType ||
  303. (!this.tokens.matches2AtIndex(introEnd - 1, tt.jsxName, tt.jsxName) &&
  304. !this.tokens.matches2AtIndex(introEnd - 1, tt.greaterThan, tt.jsxName) &&
  305. !this.tokens.matches1AtIndex(introEnd, tt.braceL) &&
  306. !this.tokens.matches1AtIndex(introEnd, tt.jsxTagEnd) &&
  307. !this.tokens.matches2AtIndex(introEnd, tt.slash, tt.jsxTagEnd))
  308. ) {
  309. introEnd++;
  310. }
  311. if (introEnd === this.tokens.currentIndex() + 1) {
  312. const tagName = this.tokens.identifierName();
  313. if (startsWithLowerCase(tagName)) {
  314. this.tokens.replaceToken(`'${tagName}'`);
  315. }
  316. }
  317. while (this.tokens.currentIndex() < introEnd) {
  318. this.rootTransformer.processToken();
  319. }
  320. }
  321. /**
  322. * Starting at the beginning of the props, add the props argument to
  323. * React.createElement, including the comma before it.
  324. */
  325. processPropsObjectWithDevInfo(elementLocationCode) {
  326. const devProps = this.options.production
  327. ? ""
  328. : `__self: this, __source: ${this.getDevSource(elementLocationCode)}`;
  329. if (!this.tokens.matches1(tt.jsxName) && !this.tokens.matches1(tt.braceL)) {
  330. if (devProps) {
  331. this.tokens.appendCode(`, {${devProps}}`);
  332. } else {
  333. this.tokens.appendCode(`, null`);
  334. }
  335. return;
  336. }
  337. this.tokens.appendCode(`, {`);
  338. this.processProps(false);
  339. if (devProps) {
  340. this.tokens.appendCode(` ${devProps}}`);
  341. } else {
  342. this.tokens.appendCode("}");
  343. }
  344. }
  345. /**
  346. * Transform the core part of the props, assuming that a { has already been
  347. * inserted before us and that a } will be inserted after us.
  348. *
  349. * If extractKeyCode is true (i.e. when using any jsx... function), any prop
  350. * named "key" has its code captured and returned rather than being emitted to
  351. * the output code. This shifts line numbers, and emitting the code later will
  352. * correct line numbers again. If no key is found or if extractKeyCode is
  353. * false, this function returns null.
  354. */
  355. processProps(extractKeyCode) {
  356. let keyCode = null;
  357. while (true) {
  358. if (this.tokens.matches2(tt.jsxName, tt.eq)) {
  359. // This is a regular key={value} or key="value" prop.
  360. const propName = this.tokens.identifierName();
  361. if (extractKeyCode && propName === "key") {
  362. if (keyCode !== null) {
  363. // The props list has multiple keys. Different implementations are
  364. // inconsistent about what to do here: as of this writing, Babel and
  365. // swc keep the *last* key and completely remove the rest, while
  366. // TypeScript uses the *first* key and leaves the others as regular
  367. // props. The React team collaborated with Babel on the
  368. // implementation of this behavior, so presumably the Babel behavior
  369. // is the one to use.
  370. // Since we won't ever be emitting the previous key code, we need to
  371. // at least emit its newlines here so that the line numbers match up
  372. // in the long run.
  373. this.tokens.appendCode(keyCode.replace(/[^\n]/g, ""));
  374. }
  375. // key
  376. this.tokens.removeToken();
  377. // =
  378. this.tokens.removeToken();
  379. const snapshot = this.tokens.snapshot();
  380. this.processPropValue();
  381. keyCode = this.tokens.dangerouslyGetAndRemoveCodeSinceSnapshot(snapshot);
  382. // Don't add a comma
  383. continue;
  384. } else {
  385. this.processPropName(propName);
  386. this.tokens.replaceToken(": ");
  387. this.processPropValue();
  388. }
  389. } else if (this.tokens.matches1(tt.jsxName)) {
  390. // This is a shorthand prop like <input disabled />.
  391. const propName = this.tokens.identifierName();
  392. this.processPropName(propName);
  393. this.tokens.appendCode(": true");
  394. } else if (this.tokens.matches1(tt.braceL)) {
  395. // This is prop spread, like <div {...getProps()}>, which we can pass
  396. // through fairly directly as an object spread.
  397. this.tokens.replaceToken("");
  398. this.rootTransformer.processBalancedCode();
  399. this.tokens.replaceToken("");
  400. } else {
  401. break;
  402. }
  403. this.tokens.appendCode(",");
  404. }
  405. return keyCode;
  406. }
  407. processPropName(propName) {
  408. if (propName.includes("-")) {
  409. this.tokens.replaceToken(`'${propName}'`);
  410. } else {
  411. this.tokens.copyToken();
  412. }
  413. }
  414. processPropValue() {
  415. if (this.tokens.matches1(tt.braceL)) {
  416. this.tokens.replaceToken("");
  417. this.rootTransformer.processBalancedCode();
  418. this.tokens.replaceToken("");
  419. } else if (this.tokens.matches1(tt.jsxTagStart)) {
  420. this.processJSXTag();
  421. } else {
  422. this.processStringPropValue();
  423. }
  424. }
  425. processStringPropValue() {
  426. const token = this.tokens.currentToken();
  427. const valueCode = this.tokens.code.slice(token.start + 1, token.end - 1);
  428. const replacementCode = formatJSXTextReplacement(valueCode);
  429. const literalCode = formatJSXStringValueLiteral(valueCode);
  430. this.tokens.replaceToken(literalCode + replacementCode);
  431. }
  432. /**
  433. * Starting in the middle of the props object literal, produce an additional
  434. * prop for the children and close the object literal.
  435. */
  436. processAutomaticChildrenAndEndProps(jsxRole) {
  437. if (jsxRole === JSXRole.StaticChildren) {
  438. this.tokens.appendCode(" children: [");
  439. this.processChildren(false);
  440. this.tokens.appendCode("]}");
  441. } else {
  442. // The parser information tells us whether we will see a real child or if
  443. // all remaining children (if any) will resolve to empty. If there are no
  444. // non-empty children, don't emit a children prop at all, but still
  445. // process children so that we properly transform the code into nothing.
  446. if (jsxRole === JSXRole.OneChild) {
  447. this.tokens.appendCode(" children: ");
  448. }
  449. this.processChildren(false);
  450. this.tokens.appendCode("}");
  451. }
  452. }
  453. /**
  454. * Transform children into a comma-separated list, which will be either
  455. * arguments to createElement or array elements of a children prop.
  456. */
  457. processChildren(needsInitialComma) {
  458. let needsComma = needsInitialComma;
  459. while (true) {
  460. if (this.tokens.matches2(tt.jsxTagStart, tt.slash)) {
  461. // Closing tag, so no more children.
  462. return;
  463. }
  464. let didEmitElement = false;
  465. if (this.tokens.matches1(tt.braceL)) {
  466. if (this.tokens.matches2(tt.braceL, tt.braceR)) {
  467. // Empty interpolations and comment-only interpolations are allowed
  468. // and don't create an extra child arg.
  469. this.tokens.replaceToken("");
  470. this.tokens.replaceToken("");
  471. } else {
  472. // Interpolated expression.
  473. this.tokens.replaceToken(needsComma ? ", " : "");
  474. this.rootTransformer.processBalancedCode();
  475. this.tokens.replaceToken("");
  476. didEmitElement = true;
  477. }
  478. } else if (this.tokens.matches1(tt.jsxTagStart)) {
  479. // Child JSX element
  480. this.tokens.appendCode(needsComma ? ", " : "");
  481. this.processJSXTag();
  482. didEmitElement = true;
  483. } else if (this.tokens.matches1(tt.jsxText) || this.tokens.matches1(tt.jsxEmptyText)) {
  484. didEmitElement = this.processChildTextElement(needsComma);
  485. } else {
  486. throw new Error("Unexpected token when processing JSX children.");
  487. }
  488. if (didEmitElement) {
  489. needsComma = true;
  490. }
  491. }
  492. }
  493. /**
  494. * Turn a JSX text element into a string literal, or nothing at all if the JSX
  495. * text resolves to the empty string.
  496. *
  497. * Returns true if a string literal is emitted, false otherwise.
  498. */
  499. processChildTextElement(needsComma) {
  500. const token = this.tokens.currentToken();
  501. const valueCode = this.tokens.code.slice(token.start, token.end);
  502. const replacementCode = formatJSXTextReplacement(valueCode);
  503. const literalCode = formatJSXTextLiteral(valueCode);
  504. if (literalCode === '""') {
  505. this.tokens.replaceToken(replacementCode);
  506. return false;
  507. } else {
  508. this.tokens.replaceToken(`${needsComma ? ", " : ""}${literalCode}${replacementCode}`);
  509. return true;
  510. }
  511. }
  512. getDevSource(elementLocationCode) {
  513. return `{fileName: ${this.getFilenameVarName()}, ${elementLocationCode}}`;
  514. }
  515. getFilenameVarName() {
  516. if (!this.filenameVarName) {
  517. this.filenameVarName = this.nameManager.claimFreeName("_jsxFileName");
  518. }
  519. return this.filenameVarName;
  520. }
  521. }
  522. /**
  523. * Spec for identifiers: https://tc39.github.io/ecma262/#prod-IdentifierStart.
  524. *
  525. * Really only treat anything starting with a-z as tag names. `_`, `$`, `é`
  526. * should be treated as component names
  527. */
  528. export function startsWithLowerCase(s) {
  529. const firstChar = s.charCodeAt(0);
  530. return firstChar >= charCodes.lowercaseA && firstChar <= charCodes.lowercaseZ;
  531. }
  532. /**
  533. * Turn the given jsxText string into a JS string literal. Leading and trailing
  534. * whitespace on lines is removed, except immediately after the open-tag and
  535. * before the close-tag. Empty lines are completely removed, and spaces are
  536. * added between lines after that.
  537. *
  538. * We use JSON.stringify to introduce escape characters as necessary, and trim
  539. * the start and end of each line and remove blank lines.
  540. */
  541. function formatJSXTextLiteral(text) {
  542. let result = "";
  543. let whitespace = "";
  544. let isInInitialLineWhitespace = false;
  545. let seenNonWhitespace = false;
  546. for (let i = 0; i < text.length; i++) {
  547. const c = text[i];
  548. if (c === " " || c === "\t" || c === "\r") {
  549. if (!isInInitialLineWhitespace) {
  550. whitespace += c;
  551. }
  552. } else if (c === "\n") {
  553. whitespace = "";
  554. isInInitialLineWhitespace = true;
  555. } else {
  556. if (seenNonWhitespace && isInInitialLineWhitespace) {
  557. result += " ";
  558. }
  559. result += whitespace;
  560. whitespace = "";
  561. if (c === "&") {
  562. const {entity, newI} = processEntity(text, i + 1);
  563. i = newI - 1;
  564. result += entity;
  565. } else {
  566. result += c;
  567. }
  568. seenNonWhitespace = true;
  569. isInInitialLineWhitespace = false;
  570. }
  571. }
  572. if (!isInInitialLineWhitespace) {
  573. result += whitespace;
  574. }
  575. return JSON.stringify(result);
  576. }
  577. /**
  578. * Produce the code that should be printed after the JSX text string literal,
  579. * with most content removed, but all newlines preserved and all spacing at the
  580. * end preserved.
  581. */
  582. function formatJSXTextReplacement(text) {
  583. let numNewlines = 0;
  584. let numSpaces = 0;
  585. for (const c of text) {
  586. if (c === "\n") {
  587. numNewlines++;
  588. numSpaces = 0;
  589. } else if (c === " ") {
  590. numSpaces++;
  591. }
  592. }
  593. return "\n".repeat(numNewlines) + " ".repeat(numSpaces);
  594. }
  595. /**
  596. * Format a string in the value position of a JSX prop.
  597. *
  598. * Use the same implementation as convertAttribute from
  599. * babel-helper-builder-react-jsx.
  600. */
  601. function formatJSXStringValueLiteral(text) {
  602. let result = "";
  603. for (let i = 0; i < text.length; i++) {
  604. const c = text[i];
  605. if (c === "\n") {
  606. if (/\s/.test(text[i + 1])) {
  607. result += " ";
  608. while (i < text.length && /\s/.test(text[i + 1])) {
  609. i++;
  610. }
  611. } else {
  612. result += "\n";
  613. }
  614. } else if (c === "&") {
  615. const {entity, newI} = processEntity(text, i + 1);
  616. result += entity;
  617. i = newI - 1;
  618. } else {
  619. result += c;
  620. }
  621. }
  622. return JSON.stringify(result);
  623. }
  624. /**
  625. * Starting at a &, see if there's an HTML entity (specified by name, decimal
  626. * char code, or hex char code) and return it if so.
  627. *
  628. * Modified from jsxReadString in babel-parser.
  629. */
  630. function processEntity(text, indexAfterAmpersand) {
  631. let str = "";
  632. let count = 0;
  633. let entity;
  634. let i = indexAfterAmpersand;
  635. if (text[i] === "#") {
  636. let radix = 10;
  637. i++;
  638. let numStart;
  639. if (text[i] === "x") {
  640. radix = 16;
  641. i++;
  642. numStart = i;
  643. while (i < text.length && isHexDigit(text.charCodeAt(i))) {
  644. i++;
  645. }
  646. } else {
  647. numStart = i;
  648. while (i < text.length && isDecimalDigit(text.charCodeAt(i))) {
  649. i++;
  650. }
  651. }
  652. if (text[i] === ";") {
  653. const numStr = text.slice(numStart, i);
  654. if (numStr) {
  655. i++;
  656. entity = String.fromCodePoint(parseInt(numStr, radix));
  657. }
  658. }
  659. } else {
  660. while (i < text.length && count++ < 10) {
  661. const ch = text[i];
  662. i++;
  663. if (ch === ";") {
  664. entity = XHTMLEntities.get(str);
  665. break;
  666. }
  667. str += ch;
  668. }
  669. }
  670. if (!entity) {
  671. return {entity: "&", newI: indexAfterAmpersand};
  672. }
  673. return {entity, newI: i};
  674. }
  675. function isDecimalDigit(code) {
  676. return code >= charCodes.digit0 && code <= charCodes.digit9;
  677. }
  678. function isHexDigit(code) {
  679. return (
  680. (code >= charCodes.digit0 && code <= charCodes.digit9) ||
  681. (code >= charCodes.lowercaseA && code <= charCodes.lowercaseF) ||
  682. (code >= charCodes.uppercaseA && code <= charCodes.uppercaseF)
  683. );
  684. }