3 const XHTMLEntities = require('./xhtml');
5 const hexNumber = /^[\da-fA-F]+$/;
6 const decimalNumber = /^\d+$/;
8 // The map to `acorn-jsx` tokens from `acorn` namespace objects.
9 const acornJsxMap = new WeakMap();
11 // Get the original tokens for the given `acorn` namespace object.
12 function getJsxTokens(acorn) {
13 acorn = acorn.Parser.acorn || acorn;
14 let acornJsx = acornJsxMap.get(acorn);
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);
28 jsxName: new TokenType('jsxName'),
29 jsxText: new TokenType('jsxText', {beforeExpr: true}),
30 jsxTagStart: new TokenType('jsxTagStart', {startsExpr: true}),
31 jsxTagEnd: new TokenType('jsxTagEnd')
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;
39 tokTypes.jsxTagEnd.updateContext = function(prevType) {
40 let out = this.context.pop();
41 if (out === tc_oTag && prevType === tt.slash || out === tc_cTag) {
43 this.exprAllowed = this.curContext() === tc_expr;
45 this.exprAllowed = true;
49 acornJsx = { tokContexts: tokContexts, tokTypes: tokTypes };
50 acornJsxMap.set(acorn, acornJsx);
56 // Transforms JSX element name to string.
58 function getQualifiedJSXName(object) {
62 if (object.type === 'JSXIdentifier')
65 if (object.type === 'JSXNamespacedName')
66 return object.namespace.name + ':' + object.name.name;
68 if (object.type === 'JSXMemberExpression')
69 return getQualifiedJSXName(object.object) + '.' +
70 getQualifiedJSXName(object.property);
73 module.exports = function(options) {
74 options = options || {};
75 return function(Parser) {
77 allowNamespaces: options.allowNamespaces !== false,
78 allowNamespacedObjects: !!options.allowNamespacedObjects
83 // This is `tokTypes` of the peer dep.
84 // This can be different instances from the actual `tokTypes` this plugin uses.
85 Object.defineProperty(module.exports, "tokTypes", {
86 get: function get_tokTypes() {
87 return getJsxTokens(require("acorn")).tokTypes;
93 function 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;
106 return class extends Parser {
107 // Expose actual `tokTypes` and `tokContexts` to other plugins.
108 static get acornJsx() {
112 // Reads inline JSX contents token.
114 let out = '', chunkStart = this.pos;
116 if (this.pos >= this.input.length)
117 this.raise(this.start, 'Unterminated JSX contents');
118 let ch = this.input.charCodeAt(this.pos);
123 if (this.pos === this.start) {
124 if (ch === 60 && this.exprAllowed) {
126 return this.finishToken(tok.jsxTagStart);
128 return this.getTokenFromCode(ch);
130 out += this.input.slice(chunkStart, this.pos);
131 return this.finishToken(tok.jsxText, out);
134 out += this.input.slice(chunkStart, this.pos);
135 out += this.jsx_readEntity();
136 chunkStart = this.pos;
143 "Unexpected token `" + this.input[this.pos] + "`. Did you mean `" +
144 (ch === 62 ? ">" : "}") + "` or " + "`{\"" + this.input[this.pos] + "\"}" + "`?"
149 out += this.input.slice(chunkStart, this.pos);
150 out += this.jsx_readNewLine(true);
151 chunkStart = this.pos;
159 jsx_readNewLine(normalizeCRLF) {
160 let ch = this.input.charCodeAt(this.pos);
163 if (ch === 13 && this.input.charCodeAt(this.pos) === 10) {
165 out = normalizeCRLF ? '\n' : '\r\n';
167 out = String.fromCharCode(ch);
169 if (this.options.locations) {
171 this.lineStart = this.pos;
177 jsx_readString(quote) {
178 let out = '', chunkStart = ++this.pos;
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;
196 out += this.input.slice(chunkStart, this.pos++);
197 return this.finishToken(tt.string, out);
201 let str = '', count = 0, entity;
202 let ch = this.input[this.pos];
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++];
209 if (str[0] === '#') {
210 if (str[1] === 'x') {
212 if (hexNumber.test(str))
213 entity = String.fromCharCode(parseInt(str, 16));
216 if (decimalNumber.test(str))
217 entity = String.fromCharCode(parseInt(str, 10));
220 entity = XHTMLEntities[str];
233 // Read a JSX identifier (valid tag or attribute name).
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.
241 let ch, start = this.pos;
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));
248 // Parse next token as JSX identifier
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;
259 return this.finishNode(node, 'JSXIdentifier');
262 // Parse namespaced identifier.
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');
274 // Parses element name in any form - namespaced, member
275 // or single identifier.
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) {
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');
293 // Parses any type of JSX attribute value.
295 jsx_parseAttributeValue() {
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');
303 case tok.jsxTagStart:
305 return this.parseExprAtom();
308 this.raise(this.start, 'JSX value should be either an expression or a quoted JSX text');
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).
316 jsx_parseEmptyExpression() {
317 let node = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc);
318 return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
321 // Parses JSX expression enclosed into curly brackets.
323 jsx_parseExpressionContainer() {
324 let node = this.startNode();
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');
333 // Parses following JSX attribute name-value pair.
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');
343 node.name = this.jsx_parseNamespacedName();
344 node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null;
345 return this.finishNode(node, 'JSXAttribute');
348 // Parses JSX opening tag starting after '<'.
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');
362 // Parses JSX closing tag starting after '</'.
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');
372 // Parses entire JSX element, including it's opening tag
373 // (starting after '<'), attributes, contents and closing tag.
375 jsx_parseElementAt(startPos, startLoc) {
376 let node = this.startNodeAt(startPos, startLoc);
378 let openingElement = this.jsx_parseOpeningElementAt(startPos, startLoc);
379 let closingElement = null;
381 if (!openingElement.selfClosing) {
384 case tok.jsxTagStart:
385 startPos = this.start; startLoc = this.startLoc;
387 if (this.eat(tt.slash)) {
388 closingElement = this.jsx_parseClosingElementAt(startPos, startLoc);
391 children.push(this.jsx_parseElementAt(startPos, startLoc));
395 children.push(this.parseExprAtom());
399 children.push(this.jsx_parseExpressionContainer());
406 if (getQualifiedJSXName(closingElement.name) !== getQualifiedJSXName(openingElement.name)) {
408 closingElement.start,
409 'Expected corresponding JSX closing tag for <' + getQualifiedJSXName(openingElement.name) + '>');
412 let fragmentOrElement = openingElement.name ? 'Element' : 'Fragment';
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");
420 return this.finishNode(node, 'JSX' + fragmentOrElement);
426 let node = this.parseLiteral(this.value);
427 node.type = "JSXText";
431 // Parses entire JSX element from current position.
434 let startPos = this.start, startLoc = this.startLoc;
436 return this.jsx_parseElementAt(startPos, startLoc);
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();
445 return super.parseExprAtom(refShortHandDefaultPos);
449 let context = this.curContext();
451 if (context === tc_expr) return this.jsx_readToken();
453 if (context === tc_oTag || context === tc_cTag) {
454 if (isIdentifierStart(code)) return this.jsx_readWord();
458 return this.finishToken(tok.jsxTagEnd);
461 if ((code === 34 || code === 39) && context == tc_oTag)
462 return this.jsx_readString(code);
465 if (code === 60 && this.exprAllowed && this.input.charCodeAt(this.pos + 1) !== 33) {
467 return this.finishToken(tok.jsxTagStart);
469 return super.readToken(code);
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;
484 return super.updateContext(prevType);