source: frontend/node_modules/sucrase/dist/transformers/JSXTransformer.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

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