--- /dev/null
+'use strict';
+
+const XHTMLEntities = require('./xhtml');
+
+const hexNumber = /^[\da-fA-F]+$/;
+const decimalNumber = /^\d+$/;
+
+// The map to `acorn-jsx` tokens from `acorn` namespace objects.
+const acornJsxMap = new WeakMap();
+
+// Get the original tokens for the given `acorn` namespace object.
+function getJsxTokens(acorn) {
+ acorn = acorn.Parser.acorn || acorn;
+ let acornJsx = acornJsxMap.get(acorn);
+ if (!acornJsx) {
+ const tt = acorn.tokTypes;
+ const TokContext = acorn.TokContext;
+ const TokenType = acorn.TokenType;
+ const tc_oTag = new TokContext('<tag', false);
+ const tc_cTag = new TokContext('</tag', false);
+ const tc_expr = new TokContext('<tag>...</tag>', true, true);
+ const tokContexts = {
+ tc_oTag: tc_oTag,
+ tc_cTag: tc_cTag,
+ tc_expr: tc_expr
+ };
+ const tokTypes = {
+ jsxName: new TokenType('jsxName'),
+ jsxText: new TokenType('jsxText', {beforeExpr: true}),
+ jsxTagStart: new TokenType('jsxTagStart'),
+ jsxTagEnd: new TokenType('jsxTagEnd')
+ };
+
+ tokTypes.jsxTagStart.updateContext = function() {
+ this.context.push(tc_expr); // treat as beginning of JSX expression
+ this.context.push(tc_oTag); // start opening tag context
+ this.exprAllowed = false;
+ };
+ tokTypes.jsxTagEnd.updateContext = function(prevType) {
+ let out = this.context.pop();
+ if (out === tc_oTag && prevType === tt.slash || out === tc_cTag) {
+ this.context.pop();
+ this.exprAllowed = this.curContext() === tc_expr;
+ } else {
+ this.exprAllowed = true;
+ }
+ };
+
+ acornJsx = { tokContexts: tokContexts, tokTypes: tokTypes };
+ acornJsxMap.set(acorn, acornJsx);
+ }
+
+ return acornJsx;
+}
+
+// Transforms JSX element name to string.
+
+function getQualifiedJSXName(object) {
+ if (!object)
+ return object;
+
+ if (object.type === 'JSXIdentifier')
+ return object.name;
+
+ if (object.type === 'JSXNamespacedName')
+ return object.namespace.name + ':' + object.name.name;
+
+ if (object.type === 'JSXMemberExpression')
+ return getQualifiedJSXName(object.object) + '.' +
+ getQualifiedJSXName(object.property);
+}
+
+module.exports = function(options) {
+ options = options || {};
+ return function(Parser) {
+ return plugin({
+ allowNamespaces: options.allowNamespaces !== false,
+ allowNamespacedObjects: !!options.allowNamespacedObjects
+ }, Parser);
+ };
+};
+
+// This is `tokTypes` of the peer dep.
+// This can be different instances from the actual `tokTypes` this plugin uses.
+Object.defineProperty(module.exports, "tokTypes", {
+ get: function get_tokTypes() {
+ return getJsxTokens(require("acorn")).tokTypes;
+ },
+ configurable: true,
+ enumerable: true
+});
+
+function plugin(options, Parser) {
+ const acorn = Parser.acorn || require("acorn");
+ const acornJsx = getJsxTokens(acorn);
+ const tt = acorn.tokTypes;
+ const tok = acornJsx.tokTypes;
+ const tokContexts = acorn.tokContexts;
+ const tc_oTag = acornJsx.tokContexts.tc_oTag;
+ const tc_cTag = acornJsx.tokContexts.tc_cTag;
+ const tc_expr = acornJsx.tokContexts.tc_expr;
+ const isNewLine = acorn.isNewLine;
+ const isIdentifierStart = acorn.isIdentifierStart;
+ const isIdentifierChar = acorn.isIdentifierChar;
+
+ return class extends Parser {
+ // Expose actual `tokTypes` and `tokContexts` to other plugins.
+ static get acornJsx() {
+ return acornJsx;
+ }
+
+ // Reads inline JSX contents token.
+ jsx_readToken() {
+ let out = '', chunkStart = this.pos;
+ for (;;) {
+ if (this.pos >= this.input.length)
+ this.raise(this.start, 'Unterminated JSX contents');
+ let ch = this.input.charCodeAt(this.pos);
+
+ switch (ch) {
+ case 60: // '<'
+ case 123: // '{'
+ if (this.pos === this.start) {
+ if (ch === 60 && this.exprAllowed) {
+ ++this.pos;
+ return this.finishToken(tok.jsxTagStart);
+ }
+ return this.getTokenFromCode(ch);
+ }
+ out += this.input.slice(chunkStart, this.pos);
+ return this.finishToken(tok.jsxText, out);
+
+ case 38: // '&'
+ out += this.input.slice(chunkStart, this.pos);
+ out += this.jsx_readEntity();
+ chunkStart = this.pos;
+ break;
+
+ case 62: // '>'
+ case 125: // '}'
+ this.raise(
+ this.pos,
+ "Unexpected token `" + this.input[this.pos] + "`. Did you mean `" +
+ (ch === 62 ? ">" : "}") + "` or " + "`{\"" + this.input[this.pos] + "\"}" + "`?"
+ );
+
+ default:
+ if (isNewLine(ch)) {
+ out += this.input.slice(chunkStart, this.pos);
+ out += this.jsx_readNewLine(true);
+ chunkStart = this.pos;
+ } else {
+ ++this.pos;
+ }
+ }
+ }
+ }
+
+ jsx_readNewLine(normalizeCRLF) {
+ let ch = this.input.charCodeAt(this.pos);
+ let out;
+ ++this.pos;
+ if (ch === 13 && this.input.charCodeAt(this.pos) === 10) {
+ ++this.pos;
+ out = normalizeCRLF ? '\n' : '\r\n';
+ } else {
+ out = String.fromCharCode(ch);
+ }
+ if (this.options.locations) {
+ ++this.curLine;
+ this.lineStart = this.pos;
+ }
+
+ return out;
+ }
+
+ jsx_readString(quote) {
+ let out = '', chunkStart = ++this.pos;
+ for (;;) {
+ if (this.pos >= this.input.length)
+ this.raise(this.start, 'Unterminated string constant');
+ let ch = this.input.charCodeAt(this.pos);
+ if (ch === quote) break;
+ if (ch === 38) { // '&'
+ out += this.input.slice(chunkStart, this.pos);
+ out += this.jsx_readEntity();
+ chunkStart = this.pos;
+ } else if (isNewLine(ch)) {
+ out += this.input.slice(chunkStart, this.pos);
+ out += this.jsx_readNewLine(false);
+ chunkStart = this.pos;
+ } else {
+ ++this.pos;
+ }
+ }
+ out += this.input.slice(chunkStart, this.pos++);
+ return this.finishToken(tt.string, out);
+ }
+
+ jsx_readEntity() {
+ let str = '', count = 0, entity;
+ let ch = this.input[this.pos];
+ if (ch !== '&')
+ this.raise(this.pos, 'Entity must start with an ampersand');
+ let startPos = ++this.pos;
+ while (this.pos < this.input.length && count++ < 10) {
+ ch = this.input[this.pos++];
+ if (ch === ';') {
+ if (str[0] === '#') {
+ if (str[1] === 'x') {
+ str = str.substr(2);
+ if (hexNumber.test(str))
+ entity = String.fromCharCode(parseInt(str, 16));
+ } else {
+ str = str.substr(1);
+ if (decimalNumber.test(str))
+ entity = String.fromCharCode(parseInt(str, 10));
+ }
+ } else {
+ entity = XHTMLEntities[str];
+ }
+ break;
+ }
+ str += ch;
+ }
+ if (!entity) {
+ this.pos = startPos;
+ return '&';
+ }
+ return entity;
+ }
+
+ // Read a JSX identifier (valid tag or attribute name).
+ //
+ // Optimized version since JSX identifiers can't contain
+ // escape characters and so can be read as single slice.
+ // Also assumes that first character was already checked
+ // by isIdentifierStart in readToken.
+
+ jsx_readWord() {
+ let ch, start = this.pos;
+ do {
+ ch = this.input.charCodeAt(++this.pos);
+ } while (isIdentifierChar(ch) || ch === 45); // '-'
+ return this.finishToken(tok.jsxName, this.input.slice(start, this.pos));
+ }
+
+ // Parse next token as JSX identifier
+
+ jsx_parseIdentifier() {
+ let node = this.startNode();
+ if (this.type === tok.jsxName)
+ node.name = this.value;
+ else if (this.type.keyword)
+ node.name = this.type.keyword;
+ else
+ this.unexpected();
+ this.next();
+ return this.finishNode(node, 'JSXIdentifier');
+ }
+
+ // Parse namespaced identifier.
+
+ jsx_parseNamespacedName() {
+ let startPos = this.start, startLoc = this.startLoc;
+ let name = this.jsx_parseIdentifier();
+ if (!options.allowNamespaces || !this.eat(tt.colon)) return name;
+ var node = this.startNodeAt(startPos, startLoc);
+ node.namespace = name;
+ node.name = this.jsx_parseIdentifier();
+ return this.finishNode(node, 'JSXNamespacedName');
+ }
+
+ // Parses element name in any form - namespaced, member
+ // or single identifier.
+
+ jsx_parseElementName() {
+ if (this.type === tok.jsxTagEnd) return '';
+ let startPos = this.start, startLoc = this.startLoc;
+ let node = this.jsx_parseNamespacedName();
+ if (this.type === tt.dot && node.type === 'JSXNamespacedName' && !options.allowNamespacedObjects) {
+ this.unexpected();
+ }
+ while (this.eat(tt.dot)) {
+ let newNode = this.startNodeAt(startPos, startLoc);
+ newNode.object = node;
+ newNode.property = this.jsx_parseIdentifier();
+ node = this.finishNode(newNode, 'JSXMemberExpression');
+ }
+ return node;
+ }
+
+ // Parses any type of JSX attribute value.
+
+ jsx_parseAttributeValue() {
+ switch (this.type) {
+ case tt.braceL:
+ let node = this.jsx_parseExpressionContainer();
+ if (node.expression.type === 'JSXEmptyExpression')
+ this.raise(node.start, 'JSX attributes must only be assigned a non-empty expression');
+ return node;
+
+ case tok.jsxTagStart:
+ case tt.string:
+ return this.parseExprAtom();
+
+ default:
+ this.raise(this.start, 'JSX value should be either an expression or a quoted JSX text');
+ }
+ }
+
+ // JSXEmptyExpression is unique type since it doesn't actually parse anything,
+ // and so it should start at the end of last read token (left brace) and finish
+ // at the beginning of the next one (right brace).
+
+ jsx_parseEmptyExpression() {
+ let node = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc);
+ return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
+ }
+
+ // Parses JSX expression enclosed into curly brackets.
+
+ jsx_parseExpressionContainer() {
+ let node = this.startNode();
+ this.next();
+ node.expression = this.type === tt.braceR
+ ? this.jsx_parseEmptyExpression()
+ : this.parseExpression();
+ this.expect(tt.braceR);
+ return this.finishNode(node, 'JSXExpressionContainer');
+ }
+
+ // Parses following JSX attribute name-value pair.
+
+ jsx_parseAttribute() {
+ let node = this.startNode();
+ if (this.eat(tt.braceL)) {
+ this.expect(tt.ellipsis);
+ node.argument = this.parseMaybeAssign();
+ this.expect(tt.braceR);
+ return this.finishNode(node, 'JSXSpreadAttribute');
+ }
+ node.name = this.jsx_parseNamespacedName();
+ node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null;
+ return this.finishNode(node, 'JSXAttribute');
+ }
+
+ // Parses JSX opening tag starting after '<'.
+
+ jsx_parseOpeningElementAt(startPos, startLoc) {
+ let node = this.startNodeAt(startPos, startLoc);
+ node.attributes = [];
+ let nodeName = this.jsx_parseElementName();
+ if (nodeName) node.name = nodeName;
+ while (this.type !== tt.slash && this.type !== tok.jsxTagEnd)
+ node.attributes.push(this.jsx_parseAttribute());
+ node.selfClosing = this.eat(tt.slash);
+ this.expect(tok.jsxTagEnd);
+ return this.finishNode(node, nodeName ? 'JSXOpeningElement' : 'JSXOpeningFragment');
+ }
+
+ // Parses JSX closing tag starting after '</'.
+
+ jsx_parseClosingElementAt(startPos, startLoc) {
+ let node = this.startNodeAt(startPos, startLoc);
+ let nodeName = this.jsx_parseElementName();
+ if (nodeName) node.name = nodeName;
+ this.expect(tok.jsxTagEnd);
+ return this.finishNode(node, nodeName ? 'JSXClosingElement' : 'JSXClosingFragment');
+ }
+
+ // Parses entire JSX element, including it's opening tag
+ // (starting after '<'), attributes, contents and closing tag.
+
+ jsx_parseElementAt(startPos, startLoc) {
+ let node = this.startNodeAt(startPos, startLoc);
+ let children = [];
+ let openingElement = this.jsx_parseOpeningElementAt(startPos, startLoc);
+ let closingElement = null;
+
+ if (!openingElement.selfClosing) {
+ contents: for (;;) {
+ switch (this.type) {
+ case tok.jsxTagStart:
+ startPos = this.start; startLoc = this.startLoc;
+ this.next();
+ if (this.eat(tt.slash)) {
+ closingElement = this.jsx_parseClosingElementAt(startPos, startLoc);
+ break contents;
+ }
+ children.push(this.jsx_parseElementAt(startPos, startLoc));
+ break;
+
+ case tok.jsxText:
+ children.push(this.parseExprAtom());
+ break;
+
+ case tt.braceL:
+ children.push(this.jsx_parseExpressionContainer());
+ break;
+
+ default:
+ this.unexpected();
+ }
+ }
+ if (getQualifiedJSXName(closingElement.name) !== getQualifiedJSXName(openingElement.name)) {
+ this.raise(
+ closingElement.start,
+ 'Expected corresponding JSX closing tag for <' + getQualifiedJSXName(openingElement.name) + '>');
+ }
+ }
+ let fragmentOrElement = openingElement.name ? 'Element' : 'Fragment';
+
+ node['opening' + fragmentOrElement] = openingElement;
+ node['closing' + fragmentOrElement] = closingElement;
+ node.children = children;
+ if (this.type === tt.relational && this.value === "<") {
+ this.raise(this.start, "Adjacent JSX elements must be wrapped in an enclosing tag");
+ }
+ return this.finishNode(node, 'JSX' + fragmentOrElement);
+ }
+
+ // Parse JSX text
+
+ jsx_parseText() {
+ let node = this.parseLiteral(this.value);
+ node.type = "JSXText";
+ return node;
+ }
+
+ // Parses entire JSX element from current position.
+
+ jsx_parseElement() {
+ let startPos = this.start, startLoc = this.startLoc;
+ this.next();
+ return this.jsx_parseElementAt(startPos, startLoc);
+ }
+
+ parseExprAtom(refShortHandDefaultPos) {
+ if (this.type === tok.jsxText)
+ return this.jsx_parseText();
+ else if (this.type === tok.jsxTagStart)
+ return this.jsx_parseElement();
+ else
+ return super.parseExprAtom(refShortHandDefaultPos);
+ }
+
+ readToken(code) {
+ let context = this.curContext();
+
+ if (context === tc_expr) return this.jsx_readToken();
+
+ if (context === tc_oTag || context === tc_cTag) {
+ if (isIdentifierStart(code)) return this.jsx_readWord();
+
+ if (code == 62) {
+ ++this.pos;
+ return this.finishToken(tok.jsxTagEnd);
+ }
+
+ if ((code === 34 || code === 39) && context == tc_oTag)
+ return this.jsx_readString(code);
+ }
+
+ if (code === 60 && this.exprAllowed && this.input.charCodeAt(this.pos + 1) !== 33) {
+ ++this.pos;
+ return this.finishToken(tok.jsxTagStart);
+ }
+ return super.readToken(code);
+ }
+
+ updateContext(prevType) {
+ if (this.type == tt.braceL) {
+ var curContext = this.curContext();
+ if (curContext == tc_oTag) this.context.push(tokContexts.b_expr);
+ else if (curContext == tc_expr) this.context.push(tokContexts.b_tmpl);
+ else super.updateContext(prevType);
+ this.exprAllowed = true;
+ } else if (this.type === tt.slash && prevType === tok.jsxTagStart) {
+ this.context.length -= 2; // do not consider JSX expr -> JSX open tag -> ... anymore
+ this.context.push(tc_cTag); // reconsider as closing tag context
+ this.exprAllowed = false;
+ } else {
+ return super.updateContext(prevType);
+ }
+ }
+ };
+}