JSXTransformer.js 26 KB

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