123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- 'use strict';
-
- const XHTMLEntities = require('./xhtml');
-
- const hexNumber = /^[\da-fA-F]+$/;
- const decimalNumber = /^\d+$/;
-
- const acorn = require("acorn");
- const tt = acorn.tokTypes;
- const TokContext = acorn.TokContext;
- const tokContexts = acorn.tokContexts;
- const TokenType = acorn.TokenType;
- const isNewLine = acorn.isNewLine;
- const isIdentifierStart = acorn.isIdentifierStart;
- const isIdentifierChar = acorn.isIdentifierChar;
-
- const tc_oTag = new TokContext('<tag', false);
- const tc_cTag = new TokContext('</tag', false);
- const tc_expr = new TokContext('<tag>...</tag>', true, true);
-
- const tok = {
- jsxName: new TokenType('jsxName'),
- jsxText: new TokenType('jsxText', {beforeExpr: true}),
- jsxTagStart: new TokenType('jsxTagStart'),
- jsxTagEnd: new TokenType('jsxTagEnd')
- }
-
- tok.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;
- };
- tok.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;
- }
- };
-
- // 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);
- }
- };
- module.exports.tokTypes = tok;
-
- function plugin(options, Parser) {
- return class extends Parser {
- // 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;
-
- 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(value) {
- let node = this.parseLiteral(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(this.value);
- 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);
- }
- }
- };
- }
|