sucrase
Version:
Super-fast alternative to Babel for when you can target modern JS runtimes
233 lines (232 loc) • 8.32 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Transformer_1 = require("./Transformer");
class JSXTransformer extends Transformer_1.default {
constructor(rootTransformer, tokens, importProcessor) {
super();
this.rootTransformer = rootTransformer;
this.tokens = tokens;
this.importProcessor = importProcessor;
}
process() {
if (this.tokens.matches(["jsxTagStart"])) {
this.processJSXTag();
return true;
}
return false;
}
/**
* 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.importProcessor.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, " "));
}