UNPKG

13.2 kBJavaScriptView Raw
1"use strict"; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }Object.defineProperty(exports, "__esModule", {value: true});
2
3
4var _xhtml = require('../parser/plugins/jsx/xhtml'); var _xhtml2 = _interopRequireDefault(_xhtml);
5var _types = require('../parser/tokenizer/types');
6var _charcodes = require('../parser/util/charcodes');
7
8var _getJSXPragmaInfo = require('../util/getJSXPragmaInfo'); var _getJSXPragmaInfo2 = _interopRequireDefault(_getJSXPragmaInfo);
9
10var _Transformer = require('./Transformer'); var _Transformer2 = _interopRequireDefault(_Transformer);
11
12const HEX_NUMBER = /^[\da-fA-F]+$/;
13const DECIMAL_NUMBER = /^\d+$/;
14
15 class JSXTransformer extends _Transformer2.default {
16 __init() {this.lastLineNumber = 1}
17 __init2() {this.lastIndex = 0}
18 __init3() {this.filenameVarName = null}
19
20
21 constructor(
22 rootTransformer,
23 tokens,
24 importProcessor,
25 nameManager,
26 options,
27 ) {
28 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);;
29 this.jsxPragmaInfo = _getJSXPragmaInfo2.default.call(void 0, options);
30 }
31
32 process() {
33 if (this.tokens.matches1(_types.TokenType.jsxTagStart)) {
34 this.processJSXTag();
35 return true;
36 }
37 return false;
38 }
39
40 getPrefixCode() {
41 if (this.filenameVarName) {
42 return `const ${this.filenameVarName} = ${JSON.stringify(this.options.filePath || "")};`;
43 } else {
44 return "";
45 }
46 }
47
48 /**
49 * Lazily calculate line numbers to avoid unneeded work. We assume this is always called in
50 * increasing order by index.
51 */
52 getLineNumberForIndex(index) {
53 const code = this.tokens.code;
54 while (this.lastIndex < index && this.lastIndex < code.length) {
55 if (code[this.lastIndex] === "\n") {
56 this.lastLineNumber++;
57 }
58 this.lastIndex++;
59 }
60 return this.lastLineNumber;
61 }
62
63 getFilenameVarName() {
64 if (!this.filenameVarName) {
65 this.filenameVarName = this.nameManager.claimFreeName("_jsxFileName");
66 }
67 return this.filenameVarName;
68 }
69
70 processProps(firstTokenStart) {
71 const lineNumber = this.getLineNumberForIndex(firstTokenStart);
72 const devProps = this.options.production
73 ? ""
74 : `__self: this, __source: {fileName: ${this.getFilenameVarName()}, lineNumber: ${lineNumber}}`;
75 if (!this.tokens.matches1(_types.TokenType.jsxName) && !this.tokens.matches1(_types.TokenType.braceL)) {
76 if (devProps) {
77 this.tokens.appendCode(`, {${devProps}}`);
78 } else {
79 this.tokens.appendCode(`, null`);
80 }
81 return;
82 }
83 this.tokens.appendCode(`, {`);
84 while (true) {
85 if (this.tokens.matches2(_types.TokenType.jsxName, _types.TokenType.eq)) {
86 this.processPropKeyName();
87 this.tokens.replaceToken(": ");
88 if (this.tokens.matches1(_types.TokenType.braceL)) {
89 this.tokens.replaceToken("");
90 this.rootTransformer.processBalancedCode();
91 this.tokens.replaceToken("");
92 } else if (this.tokens.matches1(_types.TokenType.jsxTagStart)) {
93 this.processJSXTag();
94 } else {
95 this.processStringPropValue();
96 }
97 } else if (this.tokens.matches1(_types.TokenType.jsxName)) {
98 this.processPropKeyName();
99 this.tokens.appendCode(": true");
100 } else if (this.tokens.matches1(_types.TokenType.braceL)) {
101 this.tokens.replaceToken("");
102 this.rootTransformer.processBalancedCode();
103 this.tokens.replaceToken("");
104 } else {
105 break;
106 }
107 this.tokens.appendCode(",");
108 }
109 if (devProps) {
110 this.tokens.appendCode(` ${devProps}}`);
111 } else {
112 this.tokens.appendCode("}");
113 }
114 }
115
116 processPropKeyName() {
117 const keyName = this.tokens.identifierName();
118 if (keyName.includes("-")) {
119 this.tokens.replaceToken(`'${keyName}'`);
120 } else {
121 this.tokens.copyToken();
122 }
123 }
124
125 processStringPropValue() {
126 const token = this.tokens.currentToken();
127 const valueCode = this.tokens.code.slice(token.start + 1, token.end - 1);
128 const replacementCode = formatJSXTextReplacement(valueCode);
129 const literalCode = formatJSXStringValueLiteral(valueCode);
130 this.tokens.replaceToken(literalCode + replacementCode);
131 }
132
133 /**
134 * Process the first part of a tag, before any props.
135 */
136 processTagIntro() {
137 // Walk forward until we see one of these patterns:
138 // jsxName to start the first prop, preceded by another jsxName to end the tag name.
139 // jsxName to start the first prop, preceded by greaterThan to end the type argument.
140 // [open brace] to start the first prop.
141 // [jsxTagEnd] to end the open-tag.
142 // [slash, jsxTagEnd] to end the self-closing tag.
143 let introEnd = this.tokens.currentIndex() + 1;
144 while (
145 this.tokens.tokens[introEnd].isType ||
146 (!this.tokens.matches2AtIndex(introEnd - 1, _types.TokenType.jsxName, _types.TokenType.jsxName) &&
147 !this.tokens.matches2AtIndex(introEnd - 1, _types.TokenType.greaterThan, _types.TokenType.jsxName) &&
148 !this.tokens.matches1AtIndex(introEnd, _types.TokenType.braceL) &&
149 !this.tokens.matches1AtIndex(introEnd, _types.TokenType.jsxTagEnd) &&
150 !this.tokens.matches2AtIndex(introEnd, _types.TokenType.slash, _types.TokenType.jsxTagEnd))
151 ) {
152 introEnd++;
153 }
154 if (introEnd === this.tokens.currentIndex() + 1) {
155 const tagName = this.tokens.identifierName();
156 if (startsWithLowerCase(tagName)) {
157 this.tokens.replaceToken(`'${tagName}'`);
158 }
159 }
160 while (this.tokens.currentIndex() < introEnd) {
161 this.rootTransformer.processToken();
162 }
163 }
164
165 processChildren() {
166 while (true) {
167 if (this.tokens.matches2(_types.TokenType.jsxTagStart, _types.TokenType.slash)) {
168 // Closing tag, so no more children.
169 return;
170 }
171 if (this.tokens.matches1(_types.TokenType.braceL)) {
172 if (this.tokens.matches2(_types.TokenType.braceL, _types.TokenType.braceR)) {
173 // Empty interpolations and comment-only interpolations are allowed
174 // and don't create an extra child arg.
175 this.tokens.replaceToken("");
176 this.tokens.replaceToken("");
177 } else {
178 // Interpolated expression.
179 this.tokens.replaceToken(", ");
180 this.rootTransformer.processBalancedCode();
181 this.tokens.replaceToken("");
182 }
183 } else if (this.tokens.matches1(_types.TokenType.jsxTagStart)) {
184 // Child JSX element
185 this.tokens.appendCode(", ");
186 this.processJSXTag();
187 } else if (this.tokens.matches1(_types.TokenType.jsxText)) {
188 this.processChildTextElement();
189 } else {
190 throw new Error("Unexpected token when processing JSX children.");
191 }
192 }
193 }
194
195 processChildTextElement() {
196 const token = this.tokens.currentToken();
197 const valueCode = this.tokens.code.slice(token.start, token.end);
198 const replacementCode = formatJSXTextReplacement(valueCode);
199 const literalCode = formatJSXTextLiteral(valueCode);
200 if (literalCode === '""') {
201 this.tokens.replaceToken(replacementCode);
202 } else {
203 this.tokens.replaceToken(`, ${literalCode}${replacementCode}`);
204 }
205 }
206
207 processJSXTag() {
208 const {jsxPragmaInfo} = this;
209 const resolvedPragmaBaseName = this.importProcessor
210 ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.base) || jsxPragmaInfo.base
211 : jsxPragmaInfo.base;
212 const firstTokenStart = this.tokens.currentToken().start;
213 // First tag is always jsxTagStart.
214 this.tokens.replaceToken(`${resolvedPragmaBaseName}${jsxPragmaInfo.suffix}(`);
215
216 if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
217 // Fragment syntax.
218 const resolvedFragmentPragmaBaseName = this.importProcessor
219 ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.fragmentBase) ||
220 jsxPragmaInfo.fragmentBase
221 : jsxPragmaInfo.fragmentBase;
222 this.tokens.replaceToken(
223 `${resolvedFragmentPragmaBaseName}${jsxPragmaInfo.fragmentSuffix}, null`,
224 );
225 // Tag with children.
226 this.processChildren();
227 while (!this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
228 this.tokens.replaceToken("");
229 }
230 this.tokens.replaceToken(")");
231 } else {
232 // Normal open tag or self-closing tag.
233 this.processTagIntro();
234 this.processProps(firstTokenStart);
235
236 if (this.tokens.matches2(_types.TokenType.slash, _types.TokenType.jsxTagEnd)) {
237 // Self-closing tag.
238 this.tokens.replaceToken("");
239 this.tokens.replaceToken(")");
240 } else if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
241 this.tokens.replaceToken("");
242 // Tag with children.
243 this.processChildren();
244 while (!this.tokens.matches1(_types.TokenType.jsxTagEnd)) {
245 this.tokens.replaceToken("");
246 }
247 this.tokens.replaceToken(")");
248 } else {
249 throw new Error("Expected either /> or > at the end of the tag.");
250 }
251 }
252 }
253} exports.default = JSXTransformer;
254
255/**
256 * Spec for identifiers: https://tc39.github.io/ecma262/#prod-IdentifierStart.
257 *
258 * Really only treat anything starting with a-z as tag names. `_`, `$`, `é`
259 * should be treated as copmonent names
260 */
261 function startsWithLowerCase(s) {
262 const firstChar = s.charCodeAt(0);
263 return firstChar >= _charcodes.charCodes.lowercaseA && firstChar <= _charcodes.charCodes.lowercaseZ;
264} exports.startsWithLowerCase = startsWithLowerCase;
265
266/**
267 * Turn the given jsxText string into a JS string literal. Leading and trailing
268 * whitespace on lines is removed, except immediately after the open-tag and
269 * before the close-tag. Empty lines are completely removed, and spaces are
270 * added between lines after that.
271 *
272 * We use JSON.stringify to introduce escape characters as necessary, and trim
273 * the start and end of each line and remove blank lines.
274 */
275function formatJSXTextLiteral(text) {
276 let result = "";
277 let whitespace = "";
278
279 let isInInitialLineWhitespace = false;
280 let seenNonWhitespace = false;
281 for (let i = 0; i < text.length; i++) {
282 const c = text[i];
283 if (c === " " || c === "\t" || c === "\r") {
284 if (!isInInitialLineWhitespace) {
285 whitespace += c;
286 }
287 } else if (c === "\n") {
288 whitespace = "";
289 isInInitialLineWhitespace = true;
290 } else {
291 if (seenNonWhitespace && isInInitialLineWhitespace) {
292 result += " ";
293 }
294 result += whitespace;
295 whitespace = "";
296 if (c === "&") {
297 const {entity, newI} = processEntity(text, i + 1);
298 i = newI - 1;
299 result += entity;
300 } else {
301 result += c;
302 }
303 seenNonWhitespace = true;
304 isInInitialLineWhitespace = false;
305 }
306 }
307 if (!isInInitialLineWhitespace) {
308 result += whitespace;
309 }
310 return JSON.stringify(result);
311}
312
313/**
314 * Produce the code that should be printed after the JSX text string literal,
315 * with most content removed, but all newlines preserved and all spacing at the
316 * end preserved.
317 */
318function formatJSXTextReplacement(text) {
319 let numNewlines = 0;
320 let numSpaces = 0;
321 for (const c of text) {
322 if (c === "\n") {
323 numNewlines++;
324 numSpaces = 0;
325 } else if (c === " ") {
326 numSpaces++;
327 }
328 }
329 return "\n".repeat(numNewlines) + " ".repeat(numSpaces);
330}
331
332/**
333 * Format a string in the value position of a JSX prop.
334 *
335 * Use the same implementation as convertAttribute from
336 * babel-helper-builder-react-jsx.
337 */
338function formatJSXStringValueLiteral(text) {
339 let result = "";
340 for (let i = 0; i < text.length; i++) {
341 const c = text[i];
342 if (c === "\n") {
343 if (/\s/.test(text[i + 1])) {
344 result += " ";
345 while (i < text.length && /\s/.test(text[i + 1])) {
346 i++;
347 }
348 } else {
349 result += "\n";
350 }
351 } else if (c === "&") {
352 const {entity, newI} = processEntity(text, i + 1);
353 result += entity;
354 i = newI - 1;
355 } else {
356 result += c;
357 }
358 }
359 return JSON.stringify(result);
360}
361
362/**
363 * Modified from jsxReadString in Babylon.
364 */
365function processEntity(text, indexAfterAmpersand) {
366 let str = "";
367 let count = 0;
368 let entity;
369 let i = indexAfterAmpersand;
370
371 while (i < text.length && count++ < 10) {
372 const ch = text[i];
373 i++;
374 if (ch === ";") {
375 if (str[0] === "#") {
376 if (str[1] === "x") {
377 str = str.substr(2);
378 if (HEX_NUMBER.test(str)) {
379 entity = String.fromCodePoint(parseInt(str, 16));
380 }
381 } else {
382 str = str.substr(1);
383 if (DECIMAL_NUMBER.test(str)) {
384 entity = String.fromCodePoint(parseInt(str, 10));
385 }
386 }
387 } else {
388 entity = _xhtml2.default[str];
389 }
390 break;
391 }
392 str += ch;
393 }
394 if (!entity) {
395 return {entity: "&", newI: indexAfterAmpersand};
396 }
397 return {entity, newI: i};
398}
399
\No newline at end of file