UNPKG

sucrase

Version:

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

399 lines (370 loc) 13.2 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var _xhtml = require('../parser/plugins/jsx/xhtml'); var _xhtml2 = _interopRequireDefault(_xhtml); var _types = require('../parser/tokenizer/types'); var _charcodes = require('../parser/util/charcodes'); var _getJSXPragmaInfo = require('../util/getJSXPragmaInfo'); var _getJSXPragmaInfo2 = _interopRequireDefault(_getJSXPragmaInfo); var _Transformer = require('./Transformer'); var _Transformer2 = _interopRequireDefault(_Transformer); const HEX_NUMBER = /^[\da-fA-F]+$/; const DECIMAL_NUMBER = /^\d+$/; class JSXTransformer extends _Transformer2.default { __init() {this.lastLineNumber = 1} __init2() {this.lastIndex = 0} __init3() {this.filenameVarName = null} constructor( rootTransformer, tokens, importProcessor, nameManager, options, ) { super();this.rootTransformer = rootTransformer;this.tokens = tokens;this.importProcessor = importProcessor;this.nameManager = nameManager;this.options = options;JSXTransformer.prototype.__init.call(this);JSXTransformer.prototype.__init2.call(this);JSXTransformer.prototype.__init3.call(this);; this.jsxPragmaInfo = _getJSXPragmaInfo2.default.call(void 0, options); } process() { if (this.tokens.matches1(_types.TokenType.jsxTagStart)) { this.processJSXTag(); return true; } return false; } getPrefixCode() { if (this.filenameVarName) { return `const ${this.filenameVarName} = ${JSON.stringify(this.options.filePath || "")};`; } else { return ""; } } /** * Lazily calculate line numbers to avoid unneeded work. We assume this is always called in * increasing order by index. */ getLineNumberForIndex(index) { const code = this.tokens.code; while (this.lastIndex < index && this.lastIndex < code.length) { if (code[this.lastIndex] === "\n") { this.lastLineNumber++; } this.lastIndex++; } return this.lastLineNumber; } getFilenameVarName() { if (!this.filenameVarName) { this.filenameVarName = this.nameManager.claimFreeName("_jsxFileName"); } return this.filenameVarName; } processProps(firstTokenStart) { const lineNumber = this.getLineNumberForIndex(firstTokenStart); const devProps = this.options.production ? "" : `__self: this, __source: {fileName: ${this.getFilenameVarName()}, lineNumber: ${lineNumber}}`; if (!this.tokens.matches1(_types.TokenType.jsxName) && !this.tokens.matches1(_types.TokenType.braceL)) { if (devProps) { this.tokens.appendCode(`, {${devProps}}`); } else { this.tokens.appendCode(`, null`); } return; } this.tokens.appendCode(`, {`); while (true) { if (this.tokens.matches2(_types.TokenType.jsxName, _types.TokenType.eq)) { this.processPropKeyName(); this.tokens.replaceToken(": "); if (this.tokens.matches1(_types.TokenType.braceL)) { this.tokens.replaceToken(""); this.rootTransformer.processBalancedCode(); this.tokens.replaceToken(""); } else if (this.tokens.matches1(_types.TokenType.jsxTagStart)) { this.processJSXTag(); } else { this.processStringPropValue(); } } else if (this.tokens.matches1(_types.TokenType.jsxName)) { this.processPropKeyName(); this.tokens.appendCode(": true"); } else if (this.tokens.matches1(_types.TokenType.braceL)) { this.tokens.replaceToken(""); this.rootTransformer.processBalancedCode(); this.tokens.replaceToken(""); } else { break; } this.tokens.appendCode(","); } if (devProps) { this.tokens.appendCode(` ${devProps}}`); } else { this.tokens.appendCode("}"); } } processPropKeyName() { const keyName = this.tokens.identifierName(); if (keyName.includes("-")) { this.tokens.replaceToken(`'${keyName}'`); } else { this.tokens.copyToken(); } } processStringPropValue() { const token = this.tokens.currentToken(); const valueCode = this.tokens.code.slice(token.start + 1, token.end - 1); const replacementCode = formatJSXTextReplacement(valueCode); const literalCode = formatJSXStringValueLiteral(valueCode); this.tokens.replaceToken(literalCode + replacementCode); } /** * Process the first part of a tag, before any props. */ processTagIntro() { // Walk forward until we see one of these patterns: // jsxName to start the first prop, preceded by another jsxName to end the tag name. // jsxName to start the first prop, preceded by greaterThan to end the type argument. // [open brace] to start the first prop. // [jsxTagEnd] to end the open-tag. // [slash, jsxTagEnd] to end the self-closing tag. let introEnd = this.tokens.currentIndex() + 1; while ( this.tokens.tokens[introEnd].isType || (!this.tokens.matches2AtIndex(introEnd - 1, _types.TokenType.jsxName, _types.TokenType.jsxName) && !this.tokens.matches2AtIndex(introEnd - 1, _types.TokenType.greaterThan, _types.TokenType.jsxName) && !this.tokens.matches1AtIndex(introEnd, _types.TokenType.braceL) && !this.tokens.matches1AtIndex(introEnd, _types.TokenType.jsxTagEnd) && !this.tokens.matches2AtIndex(introEnd, _types.TokenType.slash, _types.TokenType.jsxTagEnd)) ) { introEnd++; } if (introEnd === this.tokens.currentIndex() + 1) { const tagName = this.tokens.identifierName(); if (startsWithLowerCase(tagName)) { this.tokens.replaceToken(`'${tagName}'`); } } while (this.tokens.currentIndex() < introEnd) { this.rootTransformer.processToken(); } } processChildren() { while (true) { if (this.tokens.matches2(_types.TokenType.jsxTagStart, _types.TokenType.slash)) { // Closing tag, so no more children. return; } if (this.tokens.matches1(_types.TokenType.braceL)) { if (this.tokens.matches2(_types.TokenType.braceL, _types.TokenType.braceR)) { // Empty interpolations and comment-only interpolations are allowed // and don't create an extra child arg. this.tokens.replaceToken(""); this.tokens.replaceToken(""); } else { // Interpolated expression. this.tokens.replaceToken(", "); this.rootTransformer.processBalancedCode(); this.tokens.replaceToken(""); } } else if (this.tokens.matches1(_types.TokenType.jsxTagStart)) { // Child JSX element this.tokens.appendCode(", "); this.processJSXTag(); } else if (this.tokens.matches1(_types.TokenType.jsxText)) { this.processChildTextElement(); } else { throw new Error("Unexpected token when processing JSX children."); } } } processChildTextElement() { const token = this.tokens.currentToken(); const valueCode = this.tokens.code.slice(token.start, token.end); const replacementCode = formatJSXTextReplacement(valueCode); const literalCode = formatJSXTextLiteral(valueCode); if (literalCode === '""') { this.tokens.replaceToken(replacementCode); } else { this.tokens.replaceToken(`, ${literalCode}${replacementCode}`); } } processJSXTag() { const {jsxPragmaInfo} = this; const resolvedPragmaBaseName = this.importProcessor ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.base) || jsxPragmaInfo.base : jsxPragmaInfo.base; const firstTokenStart = this.tokens.currentToken().start; // First tag is always jsxTagStart. this.tokens.replaceToken(`${resolvedPragmaBaseName}${jsxPragmaInfo.suffix}(`); if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) { // Fragment syntax. const resolvedFragmentPragmaBaseName = this.importProcessor ? this.importProcessor.getIdentifierReplacement(jsxPragmaInfo.fragmentBase) || jsxPragmaInfo.fragmentBase : jsxPragmaInfo.fragmentBase; this.tokens.replaceToken( `${resolvedFragmentPragmaBaseName}${jsxPragmaInfo.fragmentSuffix}, null`, ); // Tag with children. this.processChildren(); while (!this.tokens.matches1(_types.TokenType.jsxTagEnd)) { this.tokens.replaceToken(""); } this.tokens.replaceToken(")"); } else { // Normal open tag or self-closing tag. this.processTagIntro(); this.processProps(firstTokenStart); if (this.tokens.matches2(_types.TokenType.slash, _types.TokenType.jsxTagEnd)) { // Self-closing tag. this.tokens.replaceToken(""); this.tokens.replaceToken(")"); } else if (this.tokens.matches1(_types.TokenType.jsxTagEnd)) { this.tokens.replaceToken(""); // Tag with children. this.processChildren(); while (!this.tokens.matches1(_types.TokenType.jsxTagEnd)) { this.tokens.replaceToken(""); } this.tokens.replaceToken(")"); } else { throw new Error("Expected either /> or > at the end of the tag."); } } } } exports.default = JSXTransformer; /** * Spec for identifiers: https://tc39.github.io/ecma262/#prod-IdentifierStart. * * Really only treat anything starting with a-z as tag names. `_`, `$`, `é` * should be treated as copmonent names */ function startsWithLowerCase(s) { const firstChar = s.charCodeAt(0); return firstChar >= _charcodes.charCodes.lowercaseA && firstChar <= _charcodes.charCodes.lowercaseZ; } exports.startsWithLowerCase = startsWithLowerCase; /** * Turn the given jsxText string into a JS string literal. Leading and trailing * whitespace on lines is removed, except immediately after the open-tag and * before the close-tag. Empty lines are completely removed, and spaces are * added between lines after that. * * We use JSON.stringify to introduce escape characters as necessary, and trim * the start and end of each line and remove blank lines. */ function formatJSXTextLiteral(text) { let result = ""; let whitespace = ""; let isInInitialLineWhitespace = false; let seenNonWhitespace = false; for (let i = 0; i < text.length; i++) { const c = text[i]; if (c === " " || c === "\t" || c === "\r") { if (!isInInitialLineWhitespace) { whitespace += c; } } else if (c === "\n") { whitespace = ""; isInInitialLineWhitespace = true; } else { if (seenNonWhitespace && isInInitialLineWhitespace) { result += " "; } result += whitespace; whitespace = ""; if (c === "&") { const {entity, newI} = processEntity(text, i + 1); i = newI - 1; result += entity; } else { result += c; } seenNonWhitespace = true; isInInitialLineWhitespace = false; } } if (!isInInitialLineWhitespace) { result += whitespace; } return JSON.stringify(result); } /** * Produce the code that should be printed after the JSX text string literal, * with most content removed, but all newlines preserved and all spacing at the * end preserved. */ function formatJSXTextReplacement(text) { let numNewlines = 0; let numSpaces = 0; for (const c of text) { if (c === "\n") { numNewlines++; numSpaces = 0; } else if (c === " ") { numSpaces++; } } return "\n".repeat(numNewlines) + " ".repeat(numSpaces); } /** * Format a string in the value position of a JSX prop. * * Use the same implementation as convertAttribute from * babel-helper-builder-react-jsx. */ function formatJSXStringValueLiteral(text) { let result = ""; for (let i = 0; i < text.length; i++) { const c = text[i]; if (c === "\n") { if (/\s/.test(text[i + 1])) { result += " "; while (i < text.length && /\s/.test(text[i + 1])) { i++; } } else { result += "\n"; } } else if (c === "&") { const {entity, newI} = processEntity(text, i + 1); result += entity; i = newI - 1; } else { result += c; } } return JSON.stringify(result); } /** * Modified from jsxReadString in Babylon. */ function processEntity(text, indexAfterAmpersand) { let str = ""; let count = 0; let entity; let i = indexAfterAmpersand; while (i < text.length && count++ < 10) { const ch = text[i]; i++; 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 = _xhtml2.default[str]; } break; } str += ch; } if (!entity) { return {entity: "&", newI: indexAfterAmpersand}; } return {entity, newI: i}; }