|
|
'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); } } }; }
|