sucrase
Version:
Super-fast alternative to Babel for when you can target modern JS runtimes
373 lines (372 loc) • 14 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tokenizer_1 = require("../../tokenizer");
const context_1 = require("../../tokenizer/context");
const types_1 = require("../../tokenizer/types");
const charCodes = require("../../util/charcodes");
const identifier_1 = require("../../util/identifier");
const whitespace_1 = require("../../util/whitespace");
const xhtml_1 = require("./xhtml");
const HEX_NUMBER = /^[\da-fA-F]+$/;
const DECIMAL_NUMBER = /^\d+$/;
context_1.types.j_oTag = new context_1.TokContext("<tag", false);
context_1.types.j_cTag = new context_1.TokContext("</tag", false);
context_1.types.j_expr = new context_1.TokContext("<tag>...</tag>", true, true);
types_1.types.jsxName = new types_1.TokenType("jsxName");
types_1.types.jsxText = new types_1.TokenType("jsxText", { beforeExpr: true });
types_1.types.jsxTagStart = new types_1.TokenType("jsxTagStart", { startsExpr: true });
types_1.types.jsxTagEnd = new types_1.TokenType("jsxTagEnd");
types_1.types.jsxTagStart.updateContext = function () {
this.state.context.push(context_1.types.j_expr); // treat as beginning of JSX expression
this.state.context.push(context_1.types.j_oTag); // start opening tag context
this.state.exprAllowed = false;
};
types_1.types.jsxTagEnd.updateContext = function (prevType) {
const out = this.state.context.pop();
if ((out === context_1.types.j_oTag && prevType === types_1.types.slash) || out === context_1.types.j_cTag) {
this.state.context.pop();
this.state.exprAllowed = this.curContext() === context_1.types.j_expr;
}
else {
this.state.exprAllowed = true;
}
};
exports.default = (superClass) => class extends superClass {
// Reads inline JSX contents token.
jsxReadToken() {
let out = "";
let chunkStart = this.state.pos;
for (;;) {
if (this.state.pos >= this.input.length) {
this.raise(this.state.start, "Unterminated JSX contents");
}
const ch = this.input.charCodeAt(this.state.pos);
switch (ch) {
case charCodes.lessThan:
case charCodes.leftCurlyBrace:
if (this.state.pos === this.state.start) {
if (ch === charCodes.lessThan && this.state.exprAllowed) {
++this.state.pos;
this.finishToken(types_1.types.jsxTagStart);
return;
}
this.getTokenFromCode(ch);
return;
}
out += this.input.slice(chunkStart, this.state.pos);
this.finishToken(types_1.types.jsxText, out);
return;
case charCodes.ampersand:
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadEntity();
chunkStart = this.state.pos;
break;
default:
if (whitespace_1.isNewLine(ch)) {
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadNewLine(true);
chunkStart = this.state.pos;
}
else {
++this.state.pos;
}
}
}
}
jsxReadNewLine(normalizeCRLF) {
const ch = this.input.charCodeAt(this.state.pos);
let out;
++this.state.pos;
if (ch === charCodes.carriageReturn &&
this.input.charCodeAt(this.state.pos) === charCodes.lineFeed) {
++this.state.pos;
out = normalizeCRLF ? "\n" : "\r\n";
}
else {
out = String.fromCharCode(ch);
}
return out;
}
jsxReadString(quote) {
let out = "";
let chunkStart = ++this.state.pos;
for (;;) {
if (this.state.pos >= this.input.length) {
this.raise(this.state.start, "Unterminated string constant");
}
const ch = this.input.charCodeAt(this.state.pos);
if (ch === quote)
break;
if (ch === charCodes.ampersand) {
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadEntity();
chunkStart = this.state.pos;
}
else if (whitespace_1.isNewLine(ch)) {
out += this.input.slice(chunkStart, this.state.pos);
out += this.jsxReadNewLine(false);
chunkStart = this.state.pos;
}
else {
++this.state.pos;
}
}
out += this.input.slice(chunkStart, this.state.pos++);
this.finishToken(types_1.types.string, out);
}
jsxReadEntity() {
let str = "";
let count = 0;
let entity;
let ch = this.input[this.state.pos];
const startPos = ++this.state.pos;
while (this.state.pos < this.input.length && count++ < 10) {
ch = this.input[this.state.pos++];
if (ch === ";") {
if (str[0] === "#") {
if (str[1] === "x") {
str = str.substr(2);
if (HEX_NUMBER.test(str)) {
entity = String.fromCodePoint(parseInt(str, 16));
}
}
else {
str = str.substr(1);
if (DECIMAL_NUMBER.test(str)) {
entity = String.fromCodePoint(parseInt(str, 10));
}
}
}
else {
entity = xhtml_1.default[str];
}
break;
}
str += ch;
}
if (!entity) {
this.state.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.
jsxReadWord() {
let ch;
const start = this.state.pos;
do {
ch = this.input.charCodeAt(++this.state.pos);
} while (identifier_1.isIdentifierChar(ch) || ch === charCodes.dash);
this.finishToken(types_1.types.jsxName, this.input.slice(start, this.state.pos));
}
// Parse next token as JSX identifier
jsxParseIdentifier() {
this.next();
}
// Parse namespaced identifier.
jsxParseNamespacedName() {
this.jsxParseIdentifier();
if (!this.eat(types_1.types.colon)) {
// Plain identifier, so this is an access.
this.state.tokens[this.state.tokens.length - 1].identifierRole = tokenizer_1.IdentifierRole.Access;
return;
}
// Process the second half of the namespaced name.
this.jsxParseIdentifier();
}
// Parses element name in any form - namespaced, member
// or single identifier.
jsxParseElementName() {
this.jsxParseNamespacedName();
while (this.eat(types_1.types.dot)) {
this.jsxParseIdentifier();
}
}
// Parses any type of JSX attribute value.
jsxParseAttributeValue() {
switch (this.state.type) {
case types_1.types.braceL:
this.jsxParseExpressionContainer();
return;
case types_1.types.jsxTagStart:
case types_1.types.string:
this.parseExprAtom();
return;
default:
throw this.raise(this.state.start, "JSX value should be either an expression or a quoted JSX text");
}
}
jsxParseEmptyExpression() {
// Do nothing.
}
// Parse JSX spread child
jsxParseSpreadChild() {
this.expect(types_1.types.braceL);
this.expect(types_1.types.ellipsis);
this.parseExpression();
this.expect(types_1.types.braceR);
}
// Parses JSX expression enclosed into curly brackets.
jsxParseExpressionContainer() {
this.next();
if (this.match(types_1.types.braceR)) {
this.jsxParseEmptyExpression();
}
else {
this.parseExpression();
}
this.expect(types_1.types.braceR);
}
// Parses following JSX attribute name-value pair.
jsxParseAttribute() {
if (this.eat(types_1.types.braceL)) {
this.expect(types_1.types.ellipsis);
this.parseMaybeAssign();
this.expect(types_1.types.braceR);
return;
}
this.jsxParseNamespacedName();
if (this.eat(types_1.types.eq)) {
this.jsxParseAttributeValue();
}
}
// Parses JSX opening tag starting after "<".
// Returns true if the tag was self-closing.
jsxParseOpeningElement() {
if (this.eat(types_1.types.jsxTagEnd)) {
// This is an open-fragment.
return false;
}
this.jsxParseElementName();
while (!this.match(types_1.types.slash) && !this.match(types_1.types.jsxTagEnd)) {
this.jsxParseAttribute();
}
const isSelfClosing = this.eat(types_1.types.slash);
this.expect(types_1.types.jsxTagEnd);
return isSelfClosing;
}
// Parses JSX closing tag starting after "</".
jsxParseClosingElement() {
if (this.eat(types_1.types.jsxTagEnd)) {
return;
}
this.jsxParseElementName();
this.expect(types_1.types.jsxTagEnd);
}
// Parses entire JSX element, including its opening tag
// (starting after "<"), attributes, contents and closing tag.
jsxParseElementAt() {
const isSelfClosing = this.jsxParseOpeningElement();
if (!isSelfClosing) {
contents: while (true) {
switch (this.state.type) {
case types_1.types.jsxTagStart:
this.next();
if (this.eat(types_1.types.slash)) {
this.jsxParseClosingElement();
break contents;
}
this.jsxParseElementAt();
break;
case types_1.types.jsxText:
this.parseExprAtom();
break;
case types_1.types.braceL:
if (this.lookaheadType() === types_1.types.ellipsis) {
this.jsxParseSpreadChild();
}
else {
this.jsxParseExpressionContainer();
}
break;
// istanbul ignore next - should never happen
default:
throw this.unexpected();
}
}
}
}
// Parses entire JSX element from current position.
jsxParseElement() {
this.next();
this.jsxParseElementAt();
}
// ==================================
// Overrides
// ==================================
// Returns true if this was an arrow function.
parseExprAtom() {
if (this.match(types_1.types.jsxText)) {
this.parseLiteral();
return false;
}
else if (this.match(types_1.types.jsxTagStart)) {
this.jsxParseElement();
return false;
}
else {
return super.parseExprAtom();
}
}
readToken(code) {
if (this.state.inPropertyName) {
super.readToken(code);
return;
}
const context = this.curContext();
if (context === context_1.types.j_expr) {
this.jsxReadToken();
return;
}
if (context === context_1.types.j_oTag || context === context_1.types.j_cTag) {
if (identifier_1.isIdentifierStart(code)) {
this.jsxReadWord();
return;
}
if (code === charCodes.greaterThan) {
++this.state.pos;
this.finishToken(types_1.types.jsxTagEnd);
return;
}
if ((code === charCodes.quotationMark || code === charCodes.apostrophe) &&
context === context_1.types.j_oTag) {
this.jsxReadString(code);
return;
}
}
if (code === charCodes.lessThan && this.state.exprAllowed) {
++this.state.pos;
this.finishToken(types_1.types.jsxTagStart);
return;
}
super.readToken(code);
}
updateContext(prevType) {
if (this.match(types_1.types.braceL)) {
const curContext = this.curContext();
if (curContext === context_1.types.j_oTag) {
this.state.context.push(context_1.types.braceExpression);
}
else if (curContext === context_1.types.j_expr) {
this.state.context.push(context_1.types.templateQuasi);
}
else {
super.updateContext(prevType);
}
this.state.exprAllowed = true;
}
else if (this.match(types_1.types.slash) && prevType === types_1.types.jsxTagStart) {
this.state.context.length -= 2; // do not consider JSX expr -> JSX open tag -> ... anymore
this.state.context.push(context_1.types.j_cTag); // reconsider as closing tag context
this.state.exprAllowed = false;
}
else {
super.updateContext(prevType);
}
}
};