UNPKG

sucrase

Version:

Super-fast alternative to Babel for when you can target modern JS runtimes

373 lines (372 loc) 14 kB
"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); } } };