source: frontend/node_modules/sucrase/dist/esm/parser/plugins/jsx/index.js

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

Fix frontend appearance

  • Property mode set to 100644
File size: 10.3 KB
Line 
1import {
2 eat,
3 finishToken,
4 getTokenFromCode,
5 IdentifierRole,
6 JSXRole,
7 match,
8 next,
9 skipSpace,
10 Token,
11} from "../../tokenizer/index";
12import {TokenType as tt} from "../../tokenizer/types";
13import {input, isTypeScriptEnabled, state} from "../../traverser/base";
14import {parseExpression, parseMaybeAssign} from "../../traverser/expression";
15import {expect, unexpected} from "../../traverser/util";
16import {charCodes} from "../../util/charcodes";
17import {IS_IDENTIFIER_CHAR, IS_IDENTIFIER_START} from "../../util/identifier";
18import {tsTryParseJSXTypeArgument} from "../typescript";
19
20/**
21 * Read token with JSX contents.
22 *
23 * In addition to detecting jsxTagStart and also regular tokens that might be
24 * part of an expression, this code detects the start and end of text ranges
25 * within JSX children. In order to properly count the number of children, we
26 * distinguish jsxText from jsxEmptyText, which is a text range that simplifies
27 * to the empty string after JSX whitespace trimming.
28 *
29 * It turns out that a JSX text range will simplify to the empty string if and
30 * only if both of these conditions hold:
31 * - The range consists entirely of whitespace characters (only counting space,
32 * tab, \r, and \n).
33 * - The range has at least one newline.
34 * This can be proven by analyzing any implementation of whitespace trimming,
35 * e.g. formatJSXTextLiteral in Sucrase or cleanJSXElementLiteralChild in Babel.
36 */
37function jsxReadToken() {
38 let sawNewline = false;
39 let sawNonWhitespace = false;
40 while (true) {
41 if (state.pos >= input.length) {
42 unexpected("Unterminated JSX contents");
43 return;
44 }
45
46 const ch = input.charCodeAt(state.pos);
47 if (ch === charCodes.lessThan || ch === charCodes.leftCurlyBrace) {
48 if (state.pos === state.start) {
49 if (ch === charCodes.lessThan) {
50 state.pos++;
51 finishToken(tt.jsxTagStart);
52 return;
53 }
54 getTokenFromCode(ch);
55 return;
56 }
57 if (sawNewline && !sawNonWhitespace) {
58 finishToken(tt.jsxEmptyText);
59 } else {
60 finishToken(tt.jsxText);
61 }
62 return;
63 }
64
65 // This is part of JSX text.
66 if (ch === charCodes.lineFeed) {
67 sawNewline = true;
68 } else if (ch !== charCodes.space && ch !== charCodes.carriageReturn && ch !== charCodes.tab) {
69 sawNonWhitespace = true;
70 }
71 state.pos++;
72 }
73}
74
75function jsxReadString(quote) {
76 state.pos++;
77 for (;;) {
78 if (state.pos >= input.length) {
79 unexpected("Unterminated string constant");
80 return;
81 }
82
83 const ch = input.charCodeAt(state.pos);
84 if (ch === quote) {
85 state.pos++;
86 break;
87 }
88 state.pos++;
89 }
90 finishToken(tt.string);
91}
92
93// Read a JSX identifier (valid tag or attribute name).
94//
95// Optimized version since JSX identifiers can't contain
96// escape characters and so can be read as single slice.
97// Also assumes that first character was already checked
98// by isIdentifierStart in readToken.
99
100function jsxReadWord() {
101 let ch;
102 do {
103 if (state.pos > input.length) {
104 unexpected("Unexpectedly reached the end of input.");
105 return;
106 }
107 ch = input.charCodeAt(++state.pos);
108 } while (IS_IDENTIFIER_CHAR[ch] || ch === charCodes.dash);
109 finishToken(tt.jsxName);
110}
111
112// Parse next token as JSX identifier
113function jsxParseIdentifier() {
114 nextJSXTagToken();
115}
116
117// Parse namespaced identifier.
118function jsxParseNamespacedName(identifierRole) {
119 jsxParseIdentifier();
120 if (!eat(tt.colon)) {
121 // Plain identifier, so this is an access.
122 state.tokens[state.tokens.length - 1].identifierRole = identifierRole;
123 return;
124 }
125 // Process the second half of the namespaced name.
126 jsxParseIdentifier();
127}
128
129// Parses element name in any form - namespaced, member
130// or single identifier.
131function jsxParseElementName() {
132 const firstTokenIndex = state.tokens.length;
133 jsxParseNamespacedName(IdentifierRole.Access);
134 let hadDot = false;
135 while (match(tt.dot)) {
136 hadDot = true;
137 nextJSXTagToken();
138 jsxParseIdentifier();
139 }
140 // For tags like <div> with a lowercase letter and no dots, the name is
141 // actually *not* an identifier access, since it's referring to a built-in
142 // tag name. Remove the identifier role in this case so that it's not
143 // accidentally transformed by the imports transform when preserving JSX.
144 if (!hadDot) {
145 const firstToken = state.tokens[firstTokenIndex];
146 const firstChar = input.charCodeAt(firstToken.start);
147 if (firstChar >= charCodes.lowercaseA && firstChar <= charCodes.lowercaseZ) {
148 firstToken.identifierRole = null;
149 }
150 }
151}
152
153// Parses any type of JSX attribute value.
154function jsxParseAttributeValue() {
155 switch (state.type) {
156 case tt.braceL:
157 next();
158 parseExpression();
159 nextJSXTagToken();
160 return;
161
162 case tt.jsxTagStart:
163 jsxParseElement();
164 nextJSXTagToken();
165 return;
166
167 case tt.string:
168 nextJSXTagToken();
169 return;
170
171 default:
172 unexpected("JSX value should be either an expression or a quoted JSX text");
173 }
174}
175
176// Parse JSX spread child, after already processing the {
177// Does not parse the closing }
178function jsxParseSpreadChild() {
179 expect(tt.ellipsis);
180 parseExpression();
181}
182
183// Parses JSX opening tag starting after "<".
184// Returns true if the tag was self-closing.
185// Does not parse the last token.
186function jsxParseOpeningElement(initialTokenIndex) {
187 if (match(tt.jsxTagEnd)) {
188 // This is an open-fragment.
189 return false;
190 }
191 jsxParseElementName();
192 if (isTypeScriptEnabled) {
193 tsTryParseJSXTypeArgument();
194 }
195 let hasSeenPropSpread = false;
196 while (!match(tt.slash) && !match(tt.jsxTagEnd) && !state.error) {
197 if (eat(tt.braceL)) {
198 hasSeenPropSpread = true;
199 expect(tt.ellipsis);
200 parseMaybeAssign();
201 // }
202 nextJSXTagToken();
203 continue;
204 }
205 if (
206 hasSeenPropSpread &&
207 state.end - state.start === 3 &&
208 input.charCodeAt(state.start) === charCodes.lowercaseK &&
209 input.charCodeAt(state.start + 1) === charCodes.lowercaseE &&
210 input.charCodeAt(state.start + 2) === charCodes.lowercaseY
211 ) {
212 state.tokens[initialTokenIndex].jsxRole = JSXRole.KeyAfterPropSpread;
213 }
214 jsxParseNamespacedName(IdentifierRole.ObjectKey);
215 if (match(tt.eq)) {
216 nextJSXTagToken();
217 jsxParseAttributeValue();
218 }
219 }
220 const isSelfClosing = match(tt.slash);
221 if (isSelfClosing) {
222 // /
223 nextJSXTagToken();
224 }
225 return isSelfClosing;
226}
227
228// Parses JSX closing tag starting after "</".
229// Does not parse the last token.
230function jsxParseClosingElement() {
231 if (match(tt.jsxTagEnd)) {
232 // Fragment syntax, so we immediately have a tag end.
233 return;
234 }
235 jsxParseElementName();
236}
237
238// Parses entire JSX element, including its opening tag
239// (starting after "<"), attributes, contents and closing tag.
240// Does not parse the last token.
241function jsxParseElementAt() {
242 const initialTokenIndex = state.tokens.length - 1;
243 state.tokens[initialTokenIndex].jsxRole = JSXRole.NoChildren;
244 let numExplicitChildren = 0;
245 const isSelfClosing = jsxParseOpeningElement(initialTokenIndex);
246 if (!isSelfClosing) {
247 nextJSXExprToken();
248 while (true) {
249 switch (state.type) {
250 case tt.jsxTagStart:
251 nextJSXTagToken();
252 if (match(tt.slash)) {
253 nextJSXTagToken();
254 jsxParseClosingElement();
255 // Key after prop spread takes precedence over number of children,
256 // since it means we switch to createElement, which doesn't care
257 // about number of children.
258 if (state.tokens[initialTokenIndex].jsxRole !== JSXRole.KeyAfterPropSpread) {
259 if (numExplicitChildren === 1) {
260 state.tokens[initialTokenIndex].jsxRole = JSXRole.OneChild;
261 } else if (numExplicitChildren > 1) {
262 state.tokens[initialTokenIndex].jsxRole = JSXRole.StaticChildren;
263 }
264 }
265 return;
266 }
267 numExplicitChildren++;
268 jsxParseElementAt();
269 nextJSXExprToken();
270 break;
271
272 case tt.jsxText:
273 numExplicitChildren++;
274 nextJSXExprToken();
275 break;
276
277 case tt.jsxEmptyText:
278 nextJSXExprToken();
279 break;
280
281 case tt.braceL:
282 next();
283 if (match(tt.ellipsis)) {
284 jsxParseSpreadChild();
285 nextJSXExprToken();
286 // Spread children are a mechanism to explicitly mark children as
287 // static, so count it as 2 children to satisfy the "more than one
288 // child" condition.
289 numExplicitChildren += 2;
290 } else {
291 // If we see {}, this is an empty pseudo-expression that doesn't
292 // count as a child.
293 if (!match(tt.braceR)) {
294 numExplicitChildren++;
295 parseExpression();
296 }
297 nextJSXExprToken();
298 }
299
300 break;
301
302 // istanbul ignore next - should never happen
303 default:
304 unexpected();
305 return;
306 }
307 }
308 }
309}
310
311// Parses entire JSX element from current position.
312// Does not parse the last token.
313export function jsxParseElement() {
314 nextJSXTagToken();
315 jsxParseElementAt();
316}
317
318// ==================================
319// Overrides
320// ==================================
321
322export function nextJSXTagToken() {
323 state.tokens.push(new Token());
324 skipSpace();
325 state.start = state.pos;
326 const code = input.charCodeAt(state.pos);
327
328 if (IS_IDENTIFIER_START[code]) {
329 jsxReadWord();
330 } else if (code === charCodes.quotationMark || code === charCodes.apostrophe) {
331 jsxReadString(code);
332 } else {
333 // The following tokens are just one character each.
334 ++state.pos;
335 switch (code) {
336 case charCodes.greaterThan:
337 finishToken(tt.jsxTagEnd);
338 break;
339 case charCodes.lessThan:
340 finishToken(tt.jsxTagStart);
341 break;
342 case charCodes.slash:
343 finishToken(tt.slash);
344 break;
345 case charCodes.equalsTo:
346 finishToken(tt.eq);
347 break;
348 case charCodes.leftCurlyBrace:
349 finishToken(tt.braceL);
350 break;
351 case charCodes.dot:
352 finishToken(tt.dot);
353 break;
354 case charCodes.colon:
355 finishToken(tt.colon);
356 break;
357 default:
358 unexpected();
359 }
360 }
361}
362
363function nextJSXExprToken() {
364 state.tokens.push(new Token());
365 state.start = state.pos;
366 jsxReadToken();
367}
Note: See TracBrowser for help on using the repository browser.