434 lines
13 KiB
JavaScript
434 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
var XHTMLEntities = require('./xhtml');
|
|
|
|
var hexNumber = /^[\da-fA-F]+$/;
|
|
var decimalNumber = /^\d+$/;
|
|
|
|
module.exports = function(acorn) {
|
|
var tt = acorn.tokTypes;
|
|
var tc = acorn.tokContexts;
|
|
|
|
tc.j_oTag = new acorn.TokContext('<tag', false);
|
|
tc.j_cTag = new acorn.TokContext('</tag', false);
|
|
tc.j_expr = new acorn.TokContext('<tag>...</tag>', true, true);
|
|
|
|
tt.jsxName = new acorn.TokenType('jsxName');
|
|
tt.jsxText = new acorn.TokenType('jsxText', {beforeExpr: true});
|
|
tt.jsxTagStart = new acorn.TokenType('jsxTagStart');
|
|
tt.jsxTagEnd = new acorn.TokenType('jsxTagEnd');
|
|
|
|
tt.jsxTagStart.updateContext = function() {
|
|
this.context.push(tc.j_expr); // treat as beginning of JSX expression
|
|
this.context.push(tc.j_oTag); // start opening tag context
|
|
this.exprAllowed = false;
|
|
};
|
|
tt.jsxTagEnd.updateContext = function(prevType) {
|
|
var out = this.context.pop();
|
|
if (out === tc.j_oTag && prevType === tt.slash || out === tc.j_cTag) {
|
|
this.context.pop();
|
|
this.exprAllowed = this.curContext() === tc.j_expr;
|
|
} else {
|
|
this.exprAllowed = true;
|
|
}
|
|
};
|
|
|
|
var pp = acorn.Parser.prototype;
|
|
|
|
// Reads inline JSX contents token.
|
|
|
|
pp.jsx_readToken = function() {
|
|
var out = '', chunkStart = this.pos;
|
|
for (;;) {
|
|
if (this.pos >= this.input.length)
|
|
this.raise(this.start, 'Unterminated JSX contents');
|
|
var 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(tt.jsxTagStart);
|
|
}
|
|
return this.getTokenFromCode(ch);
|
|
}
|
|
out += this.input.slice(chunkStart, this.pos);
|
|
return this.finishToken(tt.jsxText, out);
|
|
|
|
case 38: // '&'
|
|
out += this.input.slice(chunkStart, this.pos);
|
|
out += this.jsx_readEntity();
|
|
chunkStart = this.pos;
|
|
break;
|
|
|
|
default:
|
|
if (acorn.isNewLine(ch)) {
|
|
out += this.input.slice(chunkStart, this.pos);
|
|
out += this.jsx_readNewLine(true);
|
|
chunkStart = this.pos;
|
|
} else {
|
|
++this.pos;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
pp.jsx_readNewLine = function(normalizeCRLF) {
|
|
var ch = this.input.charCodeAt(this.pos);
|
|
var 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;
|
|
};
|
|
|
|
pp.jsx_readString = function(quote) {
|
|
var out = '', chunkStart = ++this.pos;
|
|
for (;;) {
|
|
if (this.pos >= this.input.length)
|
|
this.raise(this.start, 'Unterminated string constant');
|
|
var 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 (acorn.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);
|
|
};
|
|
|
|
pp.jsx_readEntity = function() {
|
|
var str = '', count = 0, entity;
|
|
var ch = this.input[this.pos];
|
|
if (ch !== '&')
|
|
this.raise(this.pos, 'Entity must start with an ampersand');
|
|
var 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.
|
|
|
|
pp.jsx_readWord = function() {
|
|
var ch, start = this.pos;
|
|
do {
|
|
ch = this.input.charCodeAt(++this.pos);
|
|
} while (acorn.isIdentifierChar(ch) || ch === 45); // '-'
|
|
return this.finishToken(tt.jsxName, this.input.slice(start, this.pos));
|
|
};
|
|
|
|
// Transforms JSX element name to string.
|
|
|
|
function getQualifiedJSXName(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);
|
|
}
|
|
|
|
// Parse next token as JSX identifier
|
|
|
|
pp.jsx_parseIdentifier = function() {
|
|
var node = this.startNode();
|
|
if (this.type === tt.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.
|
|
|
|
pp.jsx_parseNamespacedName = function() {
|
|
var startPos = this.start, startLoc = this.startLoc;
|
|
var name = this.jsx_parseIdentifier();
|
|
if (!this.options.plugins.jsx.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.
|
|
|
|
pp.jsx_parseElementName = function() {
|
|
var startPos = this.start, startLoc = this.startLoc;
|
|
var node = this.jsx_parseNamespacedName();
|
|
if (this.type === tt.dot && node.type === 'JSXNamespacedName' && !this.options.plugins.jsx.allowNamespacedObjects) {
|
|
this.unexpected();
|
|
}
|
|
while (this.eat(tt.dot)) {
|
|
var 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.
|
|
|
|
pp.jsx_parseAttributeValue = function() {
|
|
switch (this.type) {
|
|
case tt.braceL:
|
|
var 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 tt.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).
|
|
|
|
pp.jsx_parseEmptyExpression = function() {
|
|
var node = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc);
|
|
return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
|
|
};
|
|
|
|
// Parses JSX expression enclosed into curly brackets.
|
|
|
|
|
|
pp.jsx_parseExpressionContainer = function() {
|
|
var 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.
|
|
|
|
pp.jsx_parseAttribute = function() {
|
|
var 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 '<'.
|
|
|
|
pp.jsx_parseOpeningElementAt = function(startPos, startLoc) {
|
|
var node = this.startNodeAt(startPos, startLoc);
|
|
node.attributes = [];
|
|
node.name = this.jsx_parseElementName();
|
|
while (this.type !== tt.slash && this.type !== tt.jsxTagEnd)
|
|
node.attributes.push(this.jsx_parseAttribute());
|
|
node.selfClosing = this.eat(tt.slash);
|
|
this.expect(tt.jsxTagEnd);
|
|
return this.finishNode(node, 'JSXOpeningElement');
|
|
};
|
|
|
|
// Parses JSX closing tag starting after '</'.
|
|
|
|
pp.jsx_parseClosingElementAt = function(startPos, startLoc) {
|
|
var node = this.startNodeAt(startPos, startLoc);
|
|
node.name = this.jsx_parseElementName();
|
|
this.expect(tt.jsxTagEnd);
|
|
return this.finishNode(node, 'JSXClosingElement');
|
|
};
|
|
|
|
// Parses entire JSX element, including it's opening tag
|
|
// (starting after '<'), attributes, contents and closing tag.
|
|
|
|
pp.jsx_parseElementAt = function(startPos, startLoc) {
|
|
var node = this.startNodeAt(startPos, startLoc);
|
|
var children = [];
|
|
var openingElement = this.jsx_parseOpeningElementAt(startPos, startLoc);
|
|
var closingElement = null;
|
|
|
|
if (!openingElement.selfClosing) {
|
|
contents: for (;;) {
|
|
switch (this.type) {
|
|
case tt.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 tt.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) + '>');
|
|
}
|
|
}
|
|
|
|
node.openingElement = openingElement;
|
|
node.closingElement = 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, 'JSXElement');
|
|
};
|
|
|
|
// Parses entire JSX element from current position.
|
|
|
|
pp.jsx_parseElement = function() {
|
|
var startPos = this.start, startLoc = this.startLoc;
|
|
this.next();
|
|
return this.jsx_parseElementAt(startPos, startLoc);
|
|
};
|
|
|
|
acorn.plugins.jsx = function(instance, opts) {
|
|
if (!opts) {
|
|
return;
|
|
}
|
|
|
|
if (typeof opts !== 'object') {
|
|
opts = {};
|
|
}
|
|
|
|
instance.options.plugins.jsx = {
|
|
allowNamespaces: opts.allowNamespaces !== false,
|
|
allowNamespacedObjects: !!opts.allowNamespacedObjects
|
|
};
|
|
|
|
instance.extend('parseExprAtom', function(inner) {
|
|
return function(refShortHandDefaultPos) {
|
|
if (this.type === tt.jsxText)
|
|
return this.parseLiteral(this.value);
|
|
else if (this.type === tt.jsxTagStart)
|
|
return this.jsx_parseElement();
|
|
else
|
|
return inner.call(this, refShortHandDefaultPos);
|
|
};
|
|
});
|
|
|
|
instance.extend('readToken', function(inner) {
|
|
return function(code) {
|
|
var context = this.curContext();
|
|
|
|
if (context === tc.j_expr) return this.jsx_readToken();
|
|
|
|
if (context === tc.j_oTag || context === tc.j_cTag) {
|
|
if (acorn.isIdentifierStart(code)) return this.jsx_readWord();
|
|
|
|
if (code == 62) {
|
|
++this.pos;
|
|
return this.finishToken(tt.jsxTagEnd);
|
|
}
|
|
|
|
if ((code === 34 || code === 39) && context == tc.j_oTag)
|
|
return this.jsx_readString(code);
|
|
}
|
|
|
|
if (code === 60 && this.exprAllowed) {
|
|
++this.pos;
|
|
return this.finishToken(tt.jsxTagStart);
|
|
}
|
|
return inner.call(this, code);
|
|
};
|
|
});
|
|
|
|
instance.extend('updateContext', function(inner) {
|
|
return function(prevType) {
|
|
if (this.type == tt.braceL) {
|
|
var curContext = this.curContext();
|
|
if (curContext == tc.j_oTag) this.context.push(tc.b_expr);
|
|
else if (curContext == tc.j_expr) this.context.push(tc.b_tmpl);
|
|
else inner.call(this, prevType);
|
|
this.exprAllowed = true;
|
|
} else if (this.type === tt.slash && prevType === tt.jsxTagStart) {
|
|
this.context.length -= 2; // do not consider JSX expr -> JSX open tag -> ... anymore
|
|
this.context.push(tc.j_cTag); // reconsider as closing tag context
|
|
this.exprAllowed = false;
|
|
} else {
|
|
return inner.call(this, prevType);
|
|
}
|
|
};
|
|
});
|
|
};
|
|
|
|
return acorn;
|
|
};
|