diff --git a/src/compiler/core.ts b/src/compiler/core.ts index a464bd86682..15a8ce87eab 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -172,25 +172,101 @@ namespace ts { } /** - * Maps an array. If the mapped value is an array, it is spread into the result. + * Flattens an array containing a mix of array or non-array elements. + * + * @param array The array to flatten. */ - export function flatMap(array: T[], f: (x: T, i: number) => U | U[]): U[] { + export function flatten(array: (T | T[])[]): T[] { + let result: T[]; + if (array) { + result = []; + for (const v of array) { + if (v) { + if (isArray(v)) { + addRange(result, v); + } + else { + result.push(v); + } + } + } + } + + return result; + } + + /** + * Maps an array. If the mapped value is an array, it is spread into the result. + * + * @param array The array to map. + * @param mapfn The callback used to map the result into one or more values. + */ + export function flatMap(array: T[], mapfn: (x: T, i: number) => U | U[]): U[] { let result: U[]; if (array) { result = []; for (let i = 0; i < array.length; i++) { - const v = array[i]; - const ar = f(v, i); - if (ar) { - // We cast to here to leverage the behavior of Array#concat - // which will append a single value here. - result = result.concat(ar); + const v = mapfn(array[i], i); + if (v) { + if (isArray(v)) { + addRange(result, v); + } + else { + result.push(v); + } } } } return result; } + /** + * Maps contiguous spans of values with the same key. + * + * @param array The array to map. + * @param keyfn A callback used to select the key for an element. + * @param mapfn A callback used to map a contiguous chunk of values to a single value. + */ + export function spanMap(array: T[], keyfn: (x: T, i: number) => K, mapfn: (chunk: T[], key: K) => U): U[] { + let result: U[]; + if (array) { + result = []; + const len = array.length; + let previousKey: K; + let key: K; + let start = 0; + let pos = 0; + while (start < len) { + while (pos < len) { + const value = array[pos]; + key = keyfn(value, pos); + if (pos === 0) { + previousKey = key; + } + else if (key !== previousKey) { + break; + } + + pos++; + } + + if (start < pos) { + const v = mapfn(array.slice(start, pos), previousKey); + if (v) { + result.push(v); + } + + start = pos; + } + + previousKey = key; + pos++; + } + } + + return result; + } + export function concatenate(array1: T[], array2: T[]): T[] { if (!array2 || !array2.length) return array1; if (!array1 || !array1.length) return array2; diff --git a/src/compiler/transformers/jsx.ts b/src/compiler/transformers/jsx.ts index cb1aef32a97..924cfbfdb90 100644 --- a/src/compiler/transformers/jsx.ts +++ b/src/compiler/transformers/jsx.ts @@ -9,6 +9,11 @@ namespace ts { const compilerOptions = context.getCompilerOptions(); return transformSourceFile; + /** + * Transform JSX-specific syntax in a SourceFile. + * + * @param node A SourceFile node. + */ function transformSourceFile(node: SourceFile) { return visitEachChild(node, visitor, context); } @@ -64,7 +69,6 @@ namespace ts { } 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) { @@ -75,41 +79,35 @@ namespace ts { // 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)); - } - else { - if (!properties) { - properties = []; - } - - addNode(properties, transformJsxAttributeToObjectLiteralElement(attr)); - } - } - - if (properties) { - segments.push(createObjectLiteral(properties)); - } - - objectProperties = createJsxSpread(compilerOptions.reactNamespace, segments); + if (!forEach(attrs, isJsxSpreadAttribute)) { + objectProperties = createObjectLiteral(map(node.attributes, transformJsxAttributeToObjectLiteralElement)); } else { - const properties = map(node.attributes, transformJsxAttributeToObjectLiteralElement); - objectProperties = createObjectLiteral(properties); + objectProperties = createJsxSpread(compilerOptions.reactNamespace, + concatenate( + // We must always emit at least one object literal before a spread + // argument. + isJsxSpreadAttribute(attrs[0]) ? [createObjectLiteral()] : undefined, + + // Map spans of JsxAttribute nodes into object literals and spans + // of JsxSpreadAttribute nodes into expressions. + flatten( + spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) => isSpread + ? map(attrs, transformJsxSpreadAttributeToExpression) + : createObjectLiteral(map(attrs, transformJsxAttributeToObjectLiteralElement)) + ) + ) + ) + ); } } - const childExpressions = filter(map(children, transformJsxChildToExpression), isDefined); - return createJsxCreateElement(compilerOptions.reactNamespace, tagName, objectProperties, childExpressions); + return createJsxCreateElement( + compilerOptions.reactNamespace, + tagName, + objectProperties, + filter(map(children, transformJsxChildToExpression), isDefined) + ); } function transformJsxSpreadAttributeToExpression(node: JsxSpreadAttribute) { @@ -125,39 +123,29 @@ namespace ts { } function visitJsxText(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 parts: Expression[]; 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 + // 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)) { const part = text.substr(firstNonWhitespace, lastNonWhitespace - firstNonWhitespace + 1); - result = (result ? result + "\" + ' ' + \"" : "") + part; + if (!parts) { + parts = []; + } + + // We do not escape the string here as that is handled by the printer + // when it emits the literal. We do, however, need to decode JSX entities. + parts.push(createLiteral(decodeEntities(part))); } + firstNonWhitespace = -1; } else if (!isWhiteSpace(c)) { @@ -170,22 +158,41 @@ namespace ts { if (firstNonWhitespace !== -1) { const part = text.substr(firstNonWhitespace); - result = (result ? result + "\" + ' ' + \"" : "") + part; + if (!parts) { + parts = []; + } + + // We do not escape the string here as that is handled by the printer + // when it emits the literal. We do, however, need to decode JSX entities. + parts.push(createLiteral(decodeEntities(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; - } - }); + if (parts) { + return reduceLeft(parts, aggregateJsxTextParts); } - return result; + return undefined; + } + + /** + * Aggregates two expressions by interpolating them with a whitespace literal. + */ + function aggregateJsxTextParts(left: Expression, right: Expression) { + return createAdd(createAdd(left, createLiteral(" ")), right); + } + + /** + * Decodes JSX entities. + */ + function decodeEntities(text: string) { + return text.replace(/&(\w+);/g, function(s: any, m: string) { + if (entities[m] !== undefined) { + return String.fromCharCode(entities[m]); + } + else { + return s; + } + }); } function getTagName(node: JsxElement | JsxOpeningLikeElement): Expression { @@ -210,7 +217,7 @@ namespace ts { */ function getAttributeName(node: JsxAttribute): StringLiteral | Identifier { const name = node.name; - if (/[A-Za-z_]+[\w*]/.test(name.text)) { + if (/^[A-Za-z_]\w*$/.test(name.text)) { return createLiteral(name.text); } else { @@ -422,62 +429,62 @@ namespace ts { "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 - }; + "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 421737553d4..80cd6c7f8ad 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3268,6 +3268,10 @@ namespace ts { return node.kind === SyntaxKind.JsxSpreadAttribute; } + export function isJsxAttribute(node: Node): node is JsxAttribute { + return node.kind === SyntaxKind.JsxAttribute; + } + // Clauses export function isCaseOrDefaultClause(node: Node): node is CaseOrDefaultClause {