UNPKG

15.8 kBJavaScriptView Raw
1'use strict';
2
3const XHTMLEntities = require('./xhtml');
4
5const hexNumber = /^[\da-fA-F]+$/;
6const decimalNumber = /^\d+$/;
7
8// The map to `acorn-jsx` tokens from `acorn` namespace objects.
9const acornJsxMap = new WeakMap();
10
11// Get the original tokens for the given `acorn` namespace object.
12function getJsxTokens(acorn) {
13 acorn = acorn.Parser.acorn || acorn;
14 let acornJsx = acornJsxMap.get(acorn);
15 if (!acornJsx) {
16 const tt = acorn.tokTypes;
17 const TokContext = acorn.TokContext;
18 const TokenType = acorn.TokenType;
19 const tc_oTag = new TokContext('<tag', false);
20 const tc_cTag = new TokContext('</tag', false);
21 const tc_expr = new TokContext('<tag>...</tag>', true, true);
22 const tokContexts = {
23 tc_oTag: tc_oTag,
24 tc_cTag: tc_cTag,
25 tc_expr: tc_expr
26 };
27 const tokTypes = {
28 jsxName: new TokenType('jsxName'),
29 jsxText: new TokenType('jsxText', {beforeExpr: true}),
30 jsxTagStart: new TokenType('jsxTagStart', {startsExpr: true}),
31 jsxTagEnd: new TokenType('jsxTagEnd')
32 };
33
34 tokTypes.jsxTagStart.updateContext = function() {
35 this.context.push(tc_expr); // treat as beginning of JSX expression
36 this.context.push(tc_oTag); // start opening tag context
37 this.exprAllowed = false;
38 };
39 tokTypes.jsxTagEnd.updateContext = function(prevType) {
40 let out = this.context.pop();
41 if (out === tc_oTag && prevType === tt.slash || out === tc_cTag) {
42 this.context.pop();
43 this.exprAllowed = this.curContext() === tc_expr;
44 } else {
45 this.exprAllowed = true;
46 }
47 };
48
49 acornJsx = { tokContexts: tokContexts, tokTypes: tokTypes };
50 acornJsxMap.set(acorn, acornJsx);
51 }
52
53 return acornJsx;
54}
55
56// Transforms JSX element name to string.
57
58function getQualifiedJSXName(object) {
59 if (!object)
60 return object;
61
62 if (object.type === 'JSXIdentifier')
63 return object.name;
64
65 if (object.type === 'JSXNamespacedName')
66 return object.namespace.name + ':' + object.name.name;
67
68 if (object.type === 'JSXMemberExpression')
69 return getQualifiedJSXName(object.object) + '.' +
70 getQualifiedJSXName(object.property);
71}
72
73module.exports = function(options) {
74 options = options || {};
75 return function(Parser) {
76 return plugin({
77 allowNamespaces: options.allowNamespaces !== false,
78 allowNamespacedObjects: !!options.allowNamespacedObjects
79 }, Parser);
80 };
81};
82
83// This is `tokTypes` of the peer dep.
84// This can be different instances from the actual `tokTypes` this plugin uses.
85Object.defineProperty(module.exports, "tokTypes", {
86 get: function get_tokTypes() {
87 return getJsxTokens(require("acorn")).tokTypes;
88 },
89 configurable: true,
90 enumerable: true
91});
92
93function plugin(options, Parser) {
94 const acorn = Parser.acorn || require("acorn");
95 const acornJsx = getJsxTokens(acorn);
96 const tt = acorn.tokTypes;
97 const tok = acornJsx.tokTypes;
98 const tokContexts = acorn.tokContexts;
99 const tc_oTag = acornJsx.tokContexts.tc_oTag;
100 const tc_cTag = acornJsx.tokContexts.tc_cTag;
101 const tc_expr = acornJsx.tokContexts.tc_expr;
102 const isNewLine = acorn.isNewLine;
103 const isIdentifierStart = acorn.isIdentifierStart;
104 const isIdentifierChar = acorn.isIdentifierChar;
105
106 return class extends Parser {
107 // Expose actual `tokTypes` and `tokContexts` to other plugins.
108 static get acornJsx() {
109 return acornJsx;
110 }
111
112 // Reads inline JSX contents token.
113 jsx_readToken() {
114 let out = '', chunkStart = this.pos;
115 for (;;) {
116 if (this.pos >= this.input.length)
117 this.raise(this.start, 'Unterminated JSX contents');
118 let ch = this.input.charCodeAt(this.pos);
119
120 switch (ch) {
121 case 60: // '<'
122 case 123: // '{'
123 if (this.pos === this.start) {
124 if (ch === 60 && this.exprAllowed) {
125 ++this.pos;
126 return this.finishToken(tok.jsxTagStart);
127 }
128 return this.getTokenFromCode(ch);
129 }
130 out += this.input.slice(chunkStart, this.pos);
131 return this.finishToken(tok.jsxText, out);
132
133 case 38: // '&'
134 out += this.input.slice(chunkStart, this.pos);
135 out += this.jsx_readEntity();
136 chunkStart = this.pos;
137 break;
138
139 case 62: // '>'
140 case 125: // '}'
141 this.raise(
142 this.pos,
143 "Unexpected token `" + this.input[this.pos] + "`. Did you mean `" +
144 (ch === 62 ? "&gt;" : "&rbrace;") + "` or " + "`{\"" + this.input[this.pos] + "\"}" + "`?"
145 );
146
147 default:
148 if (isNewLine(ch)) {
149 out += this.input.slice(chunkStart, this.pos);
150 out += this.jsx_readNewLine(true);
151 chunkStart = this.pos;
152 } else {
153 ++this.pos;
154 }
155 }
156 }
157 }
158
159 jsx_readNewLine(normalizeCRLF) {
160 let ch = this.input.charCodeAt(this.pos);
161 let out;
162 ++this.pos;
163 if (ch === 13 && this.input.charCodeAt(this.pos) === 10) {
164 ++this.pos;
165 out = normalizeCRLF ? '\n' : '\r\n';
166 } else {
167 out = String.fromCharCode(ch);
168 }
169 if (this.options.locations) {
170 ++this.curLine;
171 this.lineStart = this.pos;
172 }
173
174 return out;
175 }
176
177 jsx_readString(quote) {
178 let out = '', chunkStart = ++this.pos;
179 for (;;) {
180 if (this.pos >= this.input.length)
181 this.raise(this.start, 'Unterminated string constant');
182 let ch = this.input.charCodeAt(this.pos);
183 if (ch === quote) break;
184 if (ch === 38) { // '&'
185 out += this.input.slice(chunkStart, this.pos);
186 out += this.jsx_readEntity();
187 chunkStart = this.pos;
188 } else if (isNewLine(ch)) {
189 out += this.input.slice(chunkStart, this.pos);
190 out += this.jsx_readNewLine(false);
191 chunkStart = this.pos;
192 } else {
193 ++this.pos;
194 }
195 }
196 out += this.input.slice(chunkStart, this.pos++);
197 return this.finishToken(tt.string, out);
198 }
199
200 jsx_readEntity() {
201 let str = '', count = 0, entity;
202 let ch = this.input[this.pos];
203 if (ch !== '&')
204 this.raise(this.pos, 'Entity must start with an ampersand');
205 let startPos = ++this.pos;
206 while (this.pos < this.input.length && count++ < 10) {
207 ch = this.input[this.pos++];
208 if (ch === ';') {
209 if (str[0] === '#') {
210 if (str[1] === 'x') {
211 str = str.substr(2);
212 if (hexNumber.test(str))
213 entity = String.fromCharCode(parseInt(str, 16));
214 } else {
215 str = str.substr(1);
216 if (decimalNumber.test(str))
217 entity = String.fromCharCode(parseInt(str, 10));
218 }
219 } else {
220 entity = XHTMLEntities[str];
221 }
222 break;
223 }
224 str += ch;
225 }
226 if (!entity) {
227 this.pos = startPos;
228 return '&';
229 }
230 return entity;
231 }
232
233 // Read a JSX identifier (valid tag or attribute name).
234 //
235 // Optimized version since JSX identifiers can't contain
236 // escape characters and so can be read as single slice.
237 // Also assumes that first character was already checked
238 // by isIdentifierStart in readToken.
239
240 jsx_readWord() {
241 let ch, start = this.pos;
242 do {
243 ch = this.input.charCodeAt(++this.pos);
244 } while (isIdentifierChar(ch) || ch === 45); // '-'
245 return this.finishToken(tok.jsxName, this.input.slice(start, this.pos));
246 }
247
248 // Parse next token as JSX identifier
249
250 jsx_parseIdentifier() {
251 let node = this.startNode();
252 if (this.type === tok.jsxName)
253 node.name = this.value;
254 else if (this.type.keyword)
255 node.name = this.type.keyword;
256 else
257 this.unexpected();
258 this.next();
259 return this.finishNode(node, 'JSXIdentifier');
260 }
261
262 // Parse namespaced identifier.
263
264 jsx_parseNamespacedName() {
265 let startPos = this.start, startLoc = this.startLoc;
266 let name = this.jsx_parseIdentifier();
267 if (!options.allowNamespaces || !this.eat(tt.colon)) return name;
268 var node = this.startNodeAt(startPos, startLoc);
269 node.namespace = name;
270 node.name = this.jsx_parseIdentifier();
271 return this.finishNode(node, 'JSXNamespacedName');
272 }
273
274 // Parses element name in any form - namespaced, member
275 // or single identifier.
276
277 jsx_parseElementName() {
278 if (this.type === tok.jsxTagEnd) return '';
279 let startPos = this.start, startLoc = this.startLoc;
280 let node = this.jsx_parseNamespacedName();
281 if (this.type === tt.dot && node.type === 'JSXNamespacedName' && !options.allowNamespacedObjects) {
282 this.unexpected();
283 }
284 while (this.eat(tt.dot)) {
285 let newNode = this.startNodeAt(startPos, startLoc);
286 newNode.object = node;
287 newNode.property = this.jsx_parseIdentifier();
288 node = this.finishNode(newNode, 'JSXMemberExpression');
289 }
290 return node;
291 }
292
293 // Parses any type of JSX attribute value.
294
295 jsx_parseAttributeValue() {
296 switch (this.type) {
297 case tt.braceL:
298 let node = this.jsx_parseExpressionContainer();
299 if (node.expression.type === 'JSXEmptyExpression')
300 this.raise(node.start, 'JSX attributes must only be assigned a non-empty expression');
301 return node;
302
303 case tok.jsxTagStart:
304 case tt.string:
305 return this.parseExprAtom();
306
307 default:
308 this.raise(this.start, 'JSX value should be either an expression or a quoted JSX text');
309 }
310 }
311
312 // JSXEmptyExpression is unique type since it doesn't actually parse anything,
313 // and so it should start at the end of last read token (left brace) and finish
314 // at the beginning of the next one (right brace).
315
316 jsx_parseEmptyExpression() {
317 let node = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc);
318 return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
319 }
320
321 // Parses JSX expression enclosed into curly brackets.
322
323 jsx_parseExpressionContainer() {
324 let node = this.startNode();
325 this.next();
326 node.expression = this.type === tt.braceR
327 ? this.jsx_parseEmptyExpression()
328 : this.parseExpression();
329 this.expect(tt.braceR);
330 return this.finishNode(node, 'JSXExpressionContainer');
331 }
332
333 // Parses following JSX attribute name-value pair.
334
335 jsx_parseAttribute() {
336 let node = this.startNode();
337 if (this.eat(tt.braceL)) {
338 this.expect(tt.ellipsis);
339 node.argument = this.parseMaybeAssign();
340 this.expect(tt.braceR);
341 return this.finishNode(node, 'JSXSpreadAttribute');
342 }
343 node.name = this.jsx_parseNamespacedName();
344 node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null;
345 return this.finishNode(node, 'JSXAttribute');
346 }
347
348 // Parses JSX opening tag starting after '<'.
349
350 jsx_parseOpeningElementAt(startPos, startLoc) {
351 let node = this.startNodeAt(startPos, startLoc);
352 node.attributes = [];
353 let nodeName = this.jsx_parseElementName();
354 if (nodeName) node.name = nodeName;
355 while (this.type !== tt.slash && this.type !== tok.jsxTagEnd)
356 node.attributes.push(this.jsx_parseAttribute());
357 node.selfClosing = this.eat(tt.slash);
358 this.expect(tok.jsxTagEnd);
359 return this.finishNode(node, nodeName ? 'JSXOpeningElement' : 'JSXOpeningFragment');
360 }
361
362 // Parses JSX closing tag starting after '</'.
363
364 jsx_parseClosingElementAt(startPos, startLoc) {
365 let node = this.startNodeAt(startPos, startLoc);
366 let nodeName = this.jsx_parseElementName();
367 if (nodeName) node.name = nodeName;
368 this.expect(tok.jsxTagEnd);
369 return this.finishNode(node, nodeName ? 'JSXClosingElement' : 'JSXClosingFragment');
370 }
371
372 // Parses entire JSX element, including it's opening tag
373 // (starting after '<'), attributes, contents and closing tag.
374
375 jsx_parseElementAt(startPos, startLoc) {
376 let node = this.startNodeAt(startPos, startLoc);
377 let children = [];
378 let openingElement = this.jsx_parseOpeningElementAt(startPos, startLoc);
379 let closingElement = null;
380
381 if (!openingElement.selfClosing) {
382 contents: for (;;) {
383 switch (this.type) {
384 case tok.jsxTagStart:
385 startPos = this.start; startLoc = this.startLoc;
386 this.next();
387 if (this.eat(tt.slash)) {
388 closingElement = this.jsx_parseClosingElementAt(startPos, startLoc);
389 break contents;
390 }
391 children.push(this.jsx_parseElementAt(startPos, startLoc));
392 break;
393
394 case tok.jsxText:
395 children.push(this.parseExprAtom());
396 break;
397
398 case tt.braceL:
399 children.push(this.jsx_parseExpressionContainer());
400 break;
401
402 default:
403 this.unexpected();
404 }
405 }
406 if (getQualifiedJSXName(closingElement.name) !== getQualifiedJSXName(openingElement.name)) {
407 this.raise(
408 closingElement.start,
409 'Expected corresponding JSX closing tag for <' + getQualifiedJSXName(openingElement.name) + '>');
410 }
411 }
412 let fragmentOrElement = openingElement.name ? 'Element' : 'Fragment';
413
414 node['opening' + fragmentOrElement] = openingElement;
415 node['closing' + fragmentOrElement] = closingElement;
416 node.children = children;
417 if (this.type === tt.relational && this.value === "<") {
418 this.raise(this.start, "Adjacent JSX elements must be wrapped in an enclosing tag");
419 }
420 return this.finishNode(node, 'JSX' + fragmentOrElement);
421 }
422
423 // Parse JSX text
424
425 jsx_parseText() {
426 let node = this.parseLiteral(this.value);
427 node.type = "JSXText";
428 return node;
429 }
430
431 // Parses entire JSX element from current position.
432
433 jsx_parseElement() {
434 let startPos = this.start, startLoc = this.startLoc;
435 this.next();
436 return this.jsx_parseElementAt(startPos, startLoc);
437 }
438
439 parseExprAtom(refShortHandDefaultPos) {
440 if (this.type === tok.jsxText)
441 return this.jsx_parseText();
442 else if (this.type === tok.jsxTagStart)
443 return this.jsx_parseElement();
444 else
445 return super.parseExprAtom(refShortHandDefaultPos);
446 }
447
448 readToken(code) {
449 let context = this.curContext();
450
451 if (context === tc_expr) return this.jsx_readToken();
452
453 if (context === tc_oTag || context === tc_cTag) {
454 if (isIdentifierStart(code)) return this.jsx_readWord();
455
456 if (code == 62) {
457 ++this.pos;
458 return this.finishToken(tok.jsxTagEnd);
459 }
460
461 if ((code === 34 || code === 39) && context == tc_oTag)
462 return this.jsx_readString(code);
463 }
464
465 if (code === 60 && this.exprAllowed && this.input.charCodeAt(this.pos + 1) !== 33) {
466 ++this.pos;
467 return this.finishToken(tok.jsxTagStart);
468 }
469 return super.readToken(code);
470 }
471
472 updateContext(prevType) {
473 if (this.type == tt.braceL) {
474 var curContext = this.curContext();
475 if (curContext == tc_oTag) this.context.push(tokContexts.b_expr);
476 else if (curContext == tc_expr) this.context.push(tokContexts.b_tmpl);
477 else super.updateContext(prevType);
478 this.exprAllowed = true;
479 } else if (this.type === tt.slash && prevType === tok.jsxTagStart) {
480 this.context.length -= 2; // do not consider JSX expr -> JSX open tag -> ... anymore
481 this.context.push(tc_cTag); // reconsider as closing tag context
482 this.exprAllowed = false;
483 } else {
484 return super.updateContext(prevType);
485 }
486 }
487 };
488}
489
\No newline at end of file