UNPKG

sucrase

Version:

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

240 lines (239 loc) 8.38 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); class JSXTransformer { constructor(rootTransformer, tokens, identifierReplacer) { this.rootTransformer = rootTransformer; this.tokens = tokens; this.identifierReplacer = identifierReplacer; } preprocess() { // Do nothing. } process() { if (this.tokens.matches(['jsxTagStart'])) { this.processJSXTag(); return true; } return false; } getPrefixCode() { return ''; } getSuffixCode() { return ''; } /** * Produce the props arg to createElement, starting at the first token of the * props, if any. */ processProps() { if (!this.tokens.matches(['jsxName']) && !this.tokens.matches(['{'])) { this.tokens.appendCode(', null'); return; } this.tokens.appendCode(', {'); while (true) { if (this.tokens.matches(['jsxName', '='])) { if (this.tokens.currentToken().value.includes('-')) { this.tokens.replaceToken(`'${this.tokens.currentToken().value}'`); } else { this.tokens.copyToken(); } this.tokens.replaceToken(': '); if (this.tokens.matches(['{'])) { this.tokens.replaceToken(''); this.rootTransformer.processBalancedCode(); this.tokens.replaceToken(''); } else { this.processStringPropValue(); } } else if (this.tokens.matches(['jsxName'])) { this.tokens.copyToken(); this.tokens.appendCode(': true'); } else if (this.tokens.matches(['{'])) { this.tokens.replaceToken(''); this.rootTransformer.processBalancedCode(); this.tokens.replaceToken(''); } else { break; } this.tokens.appendCode(','); } this.tokens.appendCode('}'); } processStringPropValue() { const value = this.tokens.currentToken().value; const replacementCode = formatJSXTextReplacement(value); const literalCode = formatJSXStringValueLiteral(value); 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: // [jsxIdentifer, equals] to start the first prop. // [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.matchesAtIndex(introEnd, ['jsxName', '=']) && !this.tokens.matchesAtIndex(introEnd, ['{']) && !this.tokens.matchesAtIndex(introEnd, ['jsxTagEnd']) && !this.tokens.matchesAtIndex(introEnd, ['/', 'jsxTagEnd'])) { introEnd++; } if (introEnd === this.tokens.currentIndex() + 1 && startsWithLowerCase(this.tokens.currentToken().value)) { this.tokens.replaceToken(`'${this.tokens.currentToken().value}'`); } while (this.tokens.currentIndex() < introEnd) { this.rootTransformer.processToken(); } } processChildren() { while (true) { if (this.tokens.matches(['jsxTagStart', '/'])) { // Closing tag, so no more children. return; } if (this.tokens.matches(['{'])) { if (this.tokens.matches(['{', '}'])) { // 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.matches(['jsxTagStart'])) { // Child JSX element this.tokens.appendCode(', '); this.processJSXTag(); } else if (this.tokens.matches(['jsxText'])) { this.processChildTextElement(); } else { throw new Error('Unexpected token when processing JSX children.'); } } } processChildTextElement() { const value = this.tokens.currentToken().value; const replacementCode = formatJSXTextReplacement(value); const literalCode = formatJSXTextLiteral(value); if (literalCode === '""') { this.tokens.replaceToken(replacementCode); } else { this.tokens.replaceToken(', ' + literalCode + replacementCode); } } processJSXTag() { const resolvedReactName = this.identifierReplacer.getIdentifierReplacement('React') || 'React'; // First tag is always jsxTagStart. this.tokens.replaceToken(`${resolvedReactName}.createElement(`); this.processTagIntro(); this.processProps(); if (this.tokens.matches(['/', 'jsxTagEnd'])) { // Self-closing tag. this.tokens.replaceToken(''); this.tokens.replaceToken(')'); } else if (this.tokens.matches(['jsxTagEnd'])) { this.tokens.replaceToken(''); // Tag with children. this.processChildren(); while (!this.tokens.matches(['jsxTagEnd'])) { this.tokens.replaceToken(''); } this.tokens.replaceToken(')'); } else { throw new Error('Expected either /> or > at the end of the tag.'); } } } exports.default = JSXTransformer; function startsWithLowerCase(s) { return s[0] == s[0].toLowerCase(); } /** * 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 (const c of text) { if (c === ' ' || c === '\t') { if (!isInInitialLineWhitespace) { whitespace += c; } } else if (c === '\n') { whitespace = ''; isInInitialLineWhitespace = true; } else { if (seenNonWhitespace && isInInitialLineWhitespace) { result += ' '; } result += whitespace; whitespace = ''; 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) { return JSON.stringify(text.replace(/\n\s+/g, " ")); }