UNPKG

7.29 kBJavaScriptView Raw
1import {
2 eat,
3 finishToken,
4 getTokenFromCode,
5 IdentifierRole,
6 lookaheadType,
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// Reads inline JSX contents token.
21function jsxReadToken() {
22 for (;;) {
23 if (state.pos >= input.length) {
24 unexpected("Unterminated JSX contents");
25 return;
26 }
27
28 const ch = input.charCodeAt(state.pos);
29
30 switch (ch) {
31 case charCodes.lessThan:
32 case charCodes.leftCurlyBrace:
33 if (state.pos === state.start) {
34 if (ch === charCodes.lessThan) {
35 state.pos++;
36 finishToken(tt.jsxTagStart);
37 return;
38 }
39 getTokenFromCode(ch);
40 return;
41 }
42 finishToken(tt.jsxText);
43 return;
44
45 default:
46 state.pos++;
47 }
48 }
49}
50
51function jsxReadString(quote) {
52 state.pos++;
53 for (;;) {
54 if (state.pos >= input.length) {
55 unexpected("Unterminated string constant");
56 return;
57 }
58
59 const ch = input.charCodeAt(state.pos);
60 if (ch === quote) {
61 state.pos++;
62 break;
63 }
64 state.pos++;
65 }
66 finishToken(tt.string);
67}
68
69// Read a JSX identifier (valid tag or attribute name).
70//
71// Optimized version since JSX identifiers can't contain
72// escape characters and so can be read as single slice.
73// Also assumes that first character was already checked
74// by isIdentifierStart in readToken.
75
76function jsxReadWord() {
77 let ch;
78 do {
79 if (state.pos > input.length) {
80 unexpected("Unexpectedly reached the end of input.");
81 return;
82 }
83 ch = input.charCodeAt(++state.pos);
84 } while (IS_IDENTIFIER_CHAR[ch] || ch === charCodes.dash);
85 finishToken(tt.jsxName);
86}
87
88// Parse next token as JSX identifier
89function jsxParseIdentifier() {
90 nextJSXTagToken();
91}
92
93// Parse namespaced identifier.
94function jsxParseNamespacedName(identifierRole) {
95 jsxParseIdentifier();
96 if (!eat(tt.colon)) {
97 // Plain identifier, so this is an access.
98 state.tokens[state.tokens.length - 1].identifierRole = identifierRole;
99 return;
100 }
101 // Process the second half of the namespaced name.
102 jsxParseIdentifier();
103}
104
105// Parses element name in any form - namespaced, member
106// or single identifier.
107function jsxParseElementName() {
108 jsxParseNamespacedName(IdentifierRole.Access);
109 while (match(tt.dot)) {
110 nextJSXTagToken();
111 jsxParseIdentifier();
112 }
113}
114
115// Parses any type of JSX attribute value.
116function jsxParseAttributeValue() {
117 switch (state.type) {
118 case tt.braceL:
119 jsxParseExpressionContainer();
120 nextJSXTagToken();
121 return;
122
123 case tt.jsxTagStart:
124 jsxParseElement();
125 nextJSXTagToken();
126 return;
127
128 case tt.string:
129 nextJSXTagToken();
130 return;
131
132 default:
133 unexpected("JSX value should be either an expression or a quoted JSX text");
134 }
135}
136
137function jsxParseEmptyExpression() {
138 // Do nothing.
139}
140
141// Parse JSX spread child
142function jsxParseSpreadChild() {
143 expect(tt.braceL);
144 expect(tt.ellipsis);
145 parseExpression();
146 expect(tt.braceR);
147}
148
149// Parses JSX expression enclosed into curly brackets.
150// Does not parse the last token.
151function jsxParseExpressionContainer() {
152 next();
153 if (match(tt.braceR)) {
154 jsxParseEmptyExpression();
155 } else {
156 parseExpression();
157 }
158}
159
160// Parses following JSX attribute name-value pair.
161function jsxParseAttribute() {
162 if (eat(tt.braceL)) {
163 expect(tt.ellipsis);
164 parseMaybeAssign();
165 // }
166 nextJSXTagToken();
167 return;
168 }
169 jsxParseNamespacedName(IdentifierRole.ObjectKey);
170 if (match(tt.eq)) {
171 nextJSXTagToken();
172 jsxParseAttributeValue();
173 }
174}
175
176// Parses JSX opening tag starting after "<".
177// Returns true if the tag was self-closing.
178// Does not parse the last token.
179function jsxParseOpeningElement() {
180 if (match(tt.jsxTagEnd)) {
181 // This is an open-fragment.
182 return false;
183 }
184 jsxParseElementName();
185 if (isTypeScriptEnabled) {
186 tsTryParseJSXTypeArgument();
187 }
188 while (!match(tt.slash) && !match(tt.jsxTagEnd) && !state.error) {
189 jsxParseAttribute();
190 }
191 const isSelfClosing = match(tt.slash);
192 if (isSelfClosing) {
193 // /
194 nextJSXTagToken();
195 }
196 return isSelfClosing;
197}
198
199// Parses JSX closing tag starting after "</".
200// Does not parse the last token.
201function jsxParseClosingElement() {
202 if (match(tt.jsxTagEnd)) {
203 // Fragment syntax, so we immediately have a tag end.
204 return;
205 }
206 jsxParseElementName();
207}
208
209// Parses entire JSX element, including its opening tag
210// (starting after "<"), attributes, contents and closing tag.
211// Does not parse the last token.
212function jsxParseElementAt() {
213 const isSelfClosing = jsxParseOpeningElement();
214 if (!isSelfClosing) {
215 nextJSXExprToken();
216 while (true) {
217 switch (state.type) {
218 case tt.jsxTagStart:
219 nextJSXTagToken();
220 if (match(tt.slash)) {
221 nextJSXTagToken();
222 jsxParseClosingElement();
223 return;
224 }
225 jsxParseElementAt();
226 nextJSXExprToken();
227 break;
228
229 case tt.jsxText:
230 nextJSXExprToken();
231 break;
232
233 case tt.braceL:
234 if (lookaheadType() === tt.ellipsis) {
235 jsxParseSpreadChild();
236 } else {
237 jsxParseExpressionContainer();
238 nextJSXExprToken();
239 }
240
241 break;
242
243 // istanbul ignore next - should never happen
244 default:
245 unexpected();
246 return;
247 }
248 }
249 }
250}
251
252// Parses entire JSX element from current position.
253// Does not parse the last token.
254export function jsxParseElement() {
255 nextJSXTagToken();
256 jsxParseElementAt();
257}
258
259// ==================================
260// Overrides
261// ==================================
262
263export function nextJSXTagToken() {
264 state.tokens.push(new Token());
265 skipSpace();
266 state.start = state.pos;
267 const code = input.charCodeAt(state.pos);
268
269 if (IS_IDENTIFIER_START[code]) {
270 jsxReadWord();
271 } else if (code === charCodes.quotationMark || code === charCodes.apostrophe) {
272 jsxReadString(code);
273 } else {
274 // The following tokens are just one character each.
275 ++state.pos;
276 switch (code) {
277 case charCodes.greaterThan:
278 finishToken(tt.jsxTagEnd);
279 break;
280 case charCodes.lessThan:
281 finishToken(tt.jsxTagStart);
282 break;
283 case charCodes.slash:
284 finishToken(tt.slash);
285 break;
286 case charCodes.equalsTo:
287 finishToken(tt.eq);
288 break;
289 case charCodes.leftCurlyBrace:
290 finishToken(tt.braceL);
291 break;
292 case charCodes.dot:
293 finishToken(tt.dot);
294 break;
295 case charCodes.colon:
296 finishToken(tt.colon);
297 break;
298 default:
299 unexpected();
300 }
301 }
302}
303
304function nextJSXExprToken() {
305 state.tokens.push(new Token());
306 state.start = state.pos;
307 jsxReadToken();
308}