Adds initial jsx transformer

This commit is contained in:
Ron Buckton 2016-02-16 18:07:39 -08:00
parent c4dc2ae46d
commit 6c0551f867
3 changed files with 511 additions and 2 deletions

View File

@ -514,6 +514,16 @@ namespace ts {
return node;
}
// Property assignments
export function createPropertyAssignment(name: PropertyName, initializer: Expression) {
const node = <PropertyAssignment>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) {

View File

@ -3,8 +3,10 @@
/*@internal*/
namespace ts {
// TODO(rbuckton): JSX->React transformer
const entities: Map<number> = 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(<JsxElement>node);
case SyntaxKind.JsxSelfClosingElement:
return visitJsxSelfClosingElement(<JsxSelfClosingElement>node);
}
Debug.fail("Unexpected node kind.");
}
function transformJsxChildToExpression(node: JsxChild): Expression {
switch (node.kind) {
case SyntaxKind.JsxText:
return visitNonEmptyJsxText(<JsxText>node);
case SyntaxKind.JsxExpression:
return visitJsxExpression(<JsxExpression>node);
case SyntaxKind.JsxElement:
return visitJsxElement(<JsxElement>node);
case SyntaxKind.JsxSelfClosingElement:
return visitJsxSelfClosingElement(<JsxSelfClosingElement>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 &nbsp;
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((<JsxElement>node).openingElement);
}
else {
const name = (<JsxOpeningLikeElement>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<number> {
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
};
}
}

View File

@ -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 {