From 6c0551f86788e3c3df305dbed99b2991c59a5802 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 16 Feb 2016 18:07:39 -0800 Subject: [PATCH] Adds initial jsx transformer --- src/compiler/factory.ts | 43 +++ src/compiler/transformers/jsx.ts | 462 ++++++++++++++++++++++++++++++- src/compiler/utilities.ts | 8 + 3 files changed, 511 insertions(+), 2 deletions(-) diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 6e195314253..0b0505bb50f 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -514,6 +514,16 @@ namespace ts { return node; } + // Property assignments + + export function createPropertyAssignment(name: PropertyName, initializer: Expression) { + const node = createNode(SyntaxKind.PropertyAssignment); + node.name = name; + node.questionToken = undefined; + node.initializer = initializer; + return node; + } + // Compound nodes export function createAssignment(left: Expression, right: Expression, location?: TextRange) { @@ -573,6 +583,39 @@ namespace ts { ); } + export function createJsxSpread(reactNamespace: string, segments: Expression[]) { + return createCall( + createPropertyAccess( + createIdentifier(reactNamespace || "React"), + "__spread" + ), + segments + ); + } + + export function createJsxCreateElement(reactNamespace: string, tagName: Expression, props: Expression, children: Expression[]): LeftHandSideExpression { + const argumentsList = [tagName]; + if (props) { + argumentsList.push(props) + } + + if (children && children.length > 0) { + if (!props) { + argumentsList.push(createNull()); + } + + addRange(argumentsList, children); + } + + return createCall( + createPropertyAccess( + createIdentifier(reactNamespace || "React"), + "createElement" + ), + argumentsList + ); + } + // Helpers export function createParamHelper(expression: Expression, parameterOffset: number) { diff --git a/src/compiler/transformers/jsx.ts b/src/compiler/transformers/jsx.ts index e9470b32eea..8ad85f153c9 100644 --- a/src/compiler/transformers/jsx.ts +++ b/src/compiler/transformers/jsx.ts @@ -3,8 +3,10 @@ /*@internal*/ namespace ts { - // TODO(rbuckton): JSX->React transformer + const entities: Map = createEntitiesMap(); + export function transformJsx(context: TransformationContext) { + const compilerOptions = context.getCompilerOptions(); return transformSourceFile; function transformSourceFile(node: SourceFile) { @@ -24,7 +26,463 @@ namespace ts { } function visitorWorker(node: Node): Node { - return node; + switch (node.kind) { + case SyntaxKind.JsxElement: + return visitJsxElement(node); + + case SyntaxKind.JsxSelfClosingElement: + return visitJsxSelfClosingElement(node); + } + + Debug.fail("Unexpected node kind."); + } + + function transformJsxChildToExpression(node: JsxChild): Expression { + switch (node.kind) { + case SyntaxKind.JsxText: + return visitNonEmptyJsxText(node); + + case SyntaxKind.JsxExpression: + return visitJsxExpression(node); + + case SyntaxKind.JsxElement: + return visitJsxElement(node); + + case SyntaxKind.JsxSelfClosingElement: + return visitJsxSelfClosingElement(node); + } + + Debug.fail("Unexpected node kind."); + } + + function visitJsxElement(node: JsxElement) { + return visitJsxOpeningLikeElement(node.openingElement, node.children); + } + + function visitJsxSelfClosingElement(node: JsxSelfClosingElement) { + return visitJsxOpeningLikeElement(node, /*children*/ undefined); + } + + function visitJsxOpeningLikeElement(node: JsxOpeningLikeElement, children: JsxChild[]) { + // We must the node onto the node stack if it is not already at the top. + const tagName = getTagName(node); + let objectProperties: Expression; + if (node.attributes.length === 0) { + // When there are no attributes, React wants "null" + objectProperties = createLiteral(null); + } + else { + // Either emit one big object literal (no spread attribs), or + // a call to React.__spread + const attrs = node.attributes; + if (forEach(attrs, isJsxSpreadAttribute)) { + const segments: Expression[] = []; + let properties: ObjectLiteralElement[] = []; + for (const attr of attrs) { + if (isJsxSpreadAttribute(attr)) { + if (properties) { + segments.push(createObjectLiteral(properties)); + properties = undefined; + } + + addNode(segments, transformJsxSpreadAttributeToExpression(attr), isExpression); + } + else { + if (!properties) { + properties = []; + } + + addNode(properties, transformJsxAttributeToObjectLiteralElement(attr), isObjectLiteralElement); + } + } + + if (properties) { + segments.push(createObjectLiteral(properties)); + } + + objectProperties = createJsxSpread(compilerOptions.reactNamespace, segments); + } + else { + const properties = map(node.attributes, transformJsxAttributeToObjectLiteralElement); + objectProperties = createObjectLiteral(properties); + } + } + + const childExpressions = filter(map(children, transformJsxChildToExpression), isDefined); + return createJsxCreateElement(compilerOptions.reactNamespace, tagName, objectProperties, childExpressions); + } + + function transformJsxSpreadAttributeToExpression(node: JsxSpreadAttribute) { + return visitNode(node.expression, visitor, isExpression); + } + + function transformJsxAttributeToObjectLiteralElement(node: JsxAttribute) { + const name = getAttributeName(node); + const expression = node.initializer + ? visitNode(node.initializer, visitor, isExpression) + : createLiteral(true); + return createPropertyAssignment(name, expression); + } + + function visitNonEmptyJsxText(node: JsxText) { + const text = getTextToEmit(node); + if (text !== undefined) { + return createLiteral(text); + } + return undefined; + } + + function getTextToEmit(node: JsxText) { + const text = trimReactWhitespaceAndApplyEntities(node); + if (text === undefined || text.length === 0) { + return undefined; + } + else { + return text; + } + } + + function trimReactWhitespaceAndApplyEntities(node: JsxText): string { + const text = getTextOfNode(node, /*includeTrivia*/ true); + let result: string = undefined; + let firstNonWhitespace = 0; + let lastNonWhitespace = -1; + + // JSX trims whitespace at the end and beginning of lines, except that the + // start/end of a tag is considered a start/end of a line only if that line is + // on the same line as the closing tag. See examples in tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx + for (let i = 0; i < text.length; i++) { + const c = text.charCodeAt(i); + if (isLineBreak(c)) { + if (firstNonWhitespace !== -1 && (lastNonWhitespace - firstNonWhitespace + 1 > 0)) { + let part = text.substr(firstNonWhitespace, lastNonWhitespace - firstNonWhitespace + 1); + result = (result ? result + "\" + ' ' + \"" : "") + part; + } + firstNonWhitespace = -1; + } + else if (!isWhiteSpace(c)) { + lastNonWhitespace = i; + if (firstNonWhitespace === -1) { + firstNonWhitespace = i; + } + } + } + + if (firstNonWhitespace !== -1) { + const part = text.substr(firstNonWhitespace); + result = (result ? result + "\" + ' ' + \"" : "") + part; + } + + if (result) { + // Replace entities like   + result = result.replace(/&(\w+);/g, function(s: any, m: string) { + if (entities[m] !== undefined) { + return String.fromCharCode(entities[m]); + } + else { + return s; + } + }); + } + + return result; + } + + function getTagName(node: JsxElement | JsxOpeningLikeElement): Expression { + if (node.kind === SyntaxKind.JsxElement) { + return getTagName((node).openingElement); + } + else { + const name = (node).tagName; + if (isIdentifier(name) && isIntrinsicJsxName(name.text)) { + return createLiteral(name.text); + } + else { + return createExpressionFromEntityName(name); + } + } + } + + /** + * Emit an attribute name, which is quoted if it needs to be quoted. Because + * these emit into an object literal property name, we don't need to be worried + * about keywords, just non-identifier characters + */ + function getAttributeName(node: JsxAttribute): StringLiteral | Identifier { + const name = node.name; + if (/[A-Za-z_]+[\w*]/.test(name.text)) { + return createLiteral(name.text); + } + else { + return name; + } + } + + function visitJsxText(node: JsxText) { + const text = trimReactWhitespaceAndApplyEntities(node); + return createLiteral(text || ""); + } + + function visitJsxExpression(node: JsxExpression) { + return visitNode(node.expression, visitor, isExpression); } } + + function createEntitiesMap(): Map { + return { + "quot": 0x0022, + "amp": 0x0026, + "apos": 0x0027, + "lt": 0x003C, + "gt": 0x003E, + "nbsp": 0x00A0, + "iexcl": 0x00A1, + "cent": 0x00A2, + "pound": 0x00A3, + "curren": 0x00A4, + "yen": 0x00A5, + "brvbar": 0x00A6, + "sect": 0x00A7, + "uml": 0x00A8, + "copy": 0x00A9, + "ordf": 0x00AA, + "laquo": 0x00AB, + "not": 0x00AC, + "shy": 0x00AD, + "reg": 0x00AE, + "macr": 0x00AF, + "deg": 0x00B0, + "plusmn": 0x00B1, + "sup2": 0x00B2, + "sup3": 0x00B3, + "acute": 0x00B4, + "micro": 0x00B5, + "para": 0x00B6, + "middot": 0x00B7, + "cedil": 0x00B8, + "sup1": 0x00B9, + "ordm": 0x00BA, + "raquo": 0x00BB, + "frac14": 0x00BC, + "frac12": 0x00BD, + "frac34": 0x00BE, + "iquest": 0x00BF, + "Agrave": 0x00C0, + "Aacute": 0x00C1, + "Acirc": 0x00C2, + "Atilde": 0x00C3, + "Auml": 0x00C4, + "Aring": 0x00C5, + "AElig": 0x00C6, + "Ccedil": 0x00C7, + "Egrave": 0x00C8, + "Eacute": 0x00C9, + "Ecirc": 0x00CA, + "Euml": 0x00CB, + "Igrave": 0x00CC, + "Iacute": 0x00CD, + "Icirc": 0x00CE, + "Iuml": 0x00CF, + "ETH": 0x00D0, + "Ntilde": 0x00D1, + "Ograve": 0x00D2, + "Oacute": 0x00D3, + "Ocirc": 0x00D4, + "Otilde": 0x00D5, + "Ouml": 0x00D6, + "times": 0x00D7, + "Oslash": 0x00D8, + "Ugrave": 0x00D9, + "Uacute": 0x00DA, + "Ucirc": 0x00DB, + "Uuml": 0x00DC, + "Yacute": 0x00DD, + "THORN": 0x00DE, + "szlig": 0x00DF, + "agrave": 0x00E0, + "aacute": 0x00E1, + "acirc": 0x00E2, + "atilde": 0x00E3, + "auml": 0x00E4, + "aring": 0x00E5, + "aelig": 0x00E6, + "ccedil": 0x00E7, + "egrave": 0x00E8, + "eacute": 0x00E9, + "ecirc": 0x00EA, + "euml": 0x00EB, + "igrave": 0x00EC, + "iacute": 0x00ED, + "icirc": 0x00EE, + "iuml": 0x00EF, + "eth": 0x00F0, + "ntilde": 0x00F1, + "ograve": 0x00F2, + "oacute": 0x00F3, + "ocirc": 0x00F4, + "otilde": 0x00F5, + "ouml": 0x00F6, + "divide": 0x00F7, + "oslash": 0x00F8, + "ugrave": 0x00F9, + "uacute": 0x00FA, + "ucirc": 0x00FB, + "uuml": 0x00FC, + "yacute": 0x00FD, + "thorn": 0x00FE, + "yuml": 0x00FF, + "OElig": 0x0152, + "oelig": 0x0153, + "Scaron": 0x0160, + "scaron": 0x0161, + "Yuml": 0x0178, + "fnof": 0x0192, + "circ": 0x02C6, + "tilde": 0x02DC, + "Alpha": 0x0391, + "Beta": 0x0392, + "Gamma": 0x0393, + "Delta": 0x0394, + "Epsilon": 0x0395, + "Zeta": 0x0396, + "Eta": 0x0397, + "Theta": 0x0398, + "Iota": 0x0399, + "Kappa": 0x039A, + "Lambda": 0x039B, + "Mu": 0x039C, + "Nu": 0x039D, + "Xi": 0x039E, + "Omicron": 0x039F, + "Pi": 0x03A0, + "Rho": 0x03A1, + "Sigma": 0x03A3, + "Tau": 0x03A4, + "Upsilon": 0x03A5, + "Phi": 0x03A6, + "Chi": 0x03A7, + "Psi": 0x03A8, + "Omega": 0x03A9, + "alpha": 0x03B1, + "beta": 0x03B2, + "gamma": 0x03B3, + "delta": 0x03B4, + "epsilon": 0x03B5, + "zeta": 0x03B6, + "eta": 0x03B7, + "theta": 0x03B8, + "iota": 0x03B9, + "kappa": 0x03BA, + "lambda": 0x03BB, + "mu": 0x03BC, + "nu": 0x03BD, + "xi": 0x03BE, + "omicron": 0x03BF, + "pi": 0x03C0, + "rho": 0x03C1, + "sigmaf": 0x03C2, + "sigma": 0x03C3, + "tau": 0x03C4, + "upsilon": 0x03C5, + "phi": 0x03C6, + "chi": 0x03C7, + "psi": 0x03C8, + "omega": 0x03C9, + "thetasym": 0x03D1, + "upsih": 0x03D2, + "piv": 0x03D6, + "ensp": 0x2002, + "emsp": 0x2003, + "thinsp": 0x2009, + "zwnj": 0x200C, + "zwj": 0x200D, + "lrm": 0x200E, + "rlm": 0x200F, + "ndash": 0x2013, + "mdash": 0x2014, + "lsquo": 0x2018, + "rsquo": 0x2019, + "sbquo": 0x201A, + "ldquo": 0x201C, + "rdquo": 0x201D, + "bdquo": 0x201E, + "dagger": 0x2020, + "Dagger": 0x2021, + "bull": 0x2022, + "hellip": 0x2026, + "permil": 0x2030, + "prime": 0x2032, + "Prime": 0x2033, + "lsaquo": 0x2039, + "rsaquo": 0x203A, + "oline": 0x203E, + "frasl": 0x2044, + "euro": 0x20AC, + "image": 0x2111, + "weierp": 0x2118, + "real": 0x211C, + "trade": 0x2122, + "alefsym": 0x2135, + "larr": 0x2190, + "uarr": 0x2191, + "rarr": 0x2192, + "darr": 0x2193, + "harr": 0x2194, + "crarr": 0x21B5, + "lArr": 0x21D0, + "uArr": 0x21D1, + "rArr": 0x21D2, + "dArr": 0x21D3, + "hArr": 0x21D4, + "forall": 0x2200, + "part": 0x2202, + "exist": 0x2203, + "empty": 0x2205, + "nabla": 0x2207, + "isin": 0x2208, + "notin": 0x2209, + "ni": 0x220B, + "prod": 0x220F, + "sum": 0x2211, + "minus": 0x2212, + "lowast": 0x2217, + "radic": 0x221A, + "prop": 0x221D, + "infin": 0x221E, + "ang": 0x2220, + "and": 0x2227, + "or": 0x2228, + "cap": 0x2229, + "cup": 0x222A, + "int": 0x222B, + "there4": 0x2234, + "sim": 0x223C, + "cong": 0x2245, + "asymp": 0x2248, + "ne": 0x2260, + "equiv": 0x2261, + "le": 0x2264, + "ge": 0x2265, + "sub": 0x2282, + "sup": 0x2283, + "nsub": 0x2284, + "sube": 0x2286, + "supe": 0x2287, + "oplus": 0x2295, + "otimes": 0x2297, + "perp": 0x22A5, + "sdot": 0x22C5, + "lceil": 0x2308, + "rceil": 0x2309, + "lfloor": 0x230A, + "rfloor": 0x230B, + "lang": 0x2329, + "rang": 0x232A, + "loz": 0x25CA, + "spades": 0x2660, + "clubs": 0x2663, + "hearts": 0x2665, + "diams": 0x2666 + }; + } } \ No newline at end of file diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index a5a0a05a0be..b84feea4153 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -180,6 +180,10 @@ namespace ts { return node.pos; } + export function isDefined(value: any): boolean { + return value !== undefined; + } + // Returns true if this node is missing from the actual source code. A 'missing' node is different // from 'undefined/defined'. When a node is undefined (which can happen for optional nodes // in the tree), it is definitely missing. However, a node may be defined, but still be @@ -3237,6 +3241,10 @@ namespace ts { || kind === SyntaxKind.JsxSpreadAttribute; } + export function isJsxSpreadAttribute(node: Node): node is JsxSpreadAttribute { + return node.kind === SyntaxKind.JsxSpreadAttribute; + } + // Clauses export function isCaseOrDefaultClause(node: Node): node is CaseOrDefaultClause {