diff --git a/src/compiler/transformers/jsx.ts b/src/compiler/transformers/jsx.ts index c0179a10e88..d0b2761da06 100644 --- a/src/compiler/transformers/jsx.ts +++ b/src/compiler/transformers/jsx.ts @@ -160,33 +160,50 @@ namespace ts { } } - function visitJsxText(node: JsxText) { - const text = getTextOfNode(node, /*includeTrivia*/ true); - let parts: Expression[]; - let firstNonWhitespace = 0; - let lastNonWhitespace = -1; + function visitJsxText(node: JsxText): StringLiteral | undefined { + const fixed = fixupWhitespaceAndDecodeEntities(getTextOfNode(node, /*includeTrivia*/ true)); + return fixed === undefined ? undefined : createLiteral(fixed); + } + + /** + * 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 + * See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model + * + * An equivalent algorithm would be: + * - If there is only one line, return it. + * - If there is only whitespace (but multiple lines), return `undefined`. + * - Split the text into lines. + * - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines. + * - Decode entities on each line (individually). + * - Remove empty lines and join the rest with " ". + */ + function fixupWhitespaceAndDecodeEntities(text: string): string | undefined { + let acc: string | undefined; + // First non-whitespace character on this line. + let firstNonWhitespace = 0; + // Last non-whitespace character on this line. + let lastNonWhitespace = -1; + // These initial values are special because the first line is: + // firstNonWhitespace = 0 to indicate that we want leading whitsepace, + // but lastNonWhitespace = -1 as a special flag to indicate that we *don't* include the line if it's all whitespace. - // 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)) { - const part = text.substr(firstNonWhitespace, lastNonWhitespace - firstNonWhitespace + 1); - 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 we've seen any non-whitespace characters on this line, add the 'trim' of the line. + // (lastNonWhitespace === -1 is a special flag to detect whether the first line is all whitespace.) + if (firstNonWhitespace !== -1 && lastNonWhitespace !== -1) { + acc = addLineOfJsxText(acc, text.substr(firstNonWhitespace, lastNonWhitespace - firstNonWhitespace + 1)); } + // Reset firstNonWhitespace for the next line. + // Don't bother to reset lastNonWhitespace because we ignore it if firstNonWhitespace = -1. firstNonWhitespace = -1; } - else if (!isWhiteSpace(c)) { + else if (!isWhiteSpaceSingleLine(c)) { lastNonWhitespace = i; if (firstNonWhitespace === -1) { firstNonWhitespace = i; @@ -194,29 +211,18 @@ namespace ts { } } - if (firstNonWhitespace !== -1) { - const part = text.substr(firstNonWhitespace); - 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 (parts) { - return reduceLeft(parts, aggregateJsxTextParts); - } - - return undefined; + return firstNonWhitespace !== -1 + // Last line had a non-whitespace character. Emit the 'trimLeft', meaning keep trailing whitespace. + ? addLineOfJsxText(acc, text.substr(firstNonWhitespace)) + // Last line was all whitespace, so ignore it + : acc; } - /** - * Aggregates two expressions by interpolating them with a whitespace literal. - */ - function aggregateJsxTextParts(left: Expression, right: Expression) { - return createAdd(createAdd(left, createLiteral(" ")), right); + function addLineOfJsxText(acc: string | undefined, trimmedLine: string): string { + // 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. + const decoded = decodeEntities(trimmedLine); + return acc === undefined ? decoded : acc + " " + decoded; } /** diff --git a/tests/baselines/reference/tsxReactEmitWhitespace.js b/tests/baselines/reference/tsxReactEmitWhitespace.js index dd69091569f..54839412567 100644 --- a/tests/baselines/reference/tsxReactEmitWhitespace.js +++ b/tests/baselines/reference/tsxReactEmitWhitespace.js @@ -41,7 +41,7 @@ var p = 0;
; -// Emit "foo" + ' ' + "bar" +// Emit "foo bar"
foo @@ -50,6 +50,18 @@ var p = 0;
; +// Emit "hello\\ world" +
+ + hello\ + +world +
; + +// Emit " a b c d " +
a + b c + d
; //// [file.js] @@ -75,5 +87,9 @@ React.createElement("div", null, " 3 "); React.createElement("div", null, "3"); // Emit no args React.createElement("div", null); -// Emit "foo" + ' ' + "bar" -React.createElement("div", null, "foo" + " " + "bar"); +// Emit "foo bar" +React.createElement("div", null, "foo bar"); +// Emit "hello\\ world" +React.createElement("div", null, "hello\\ world"); +// Emit " a b c d " +React.createElement("div", null, " a b c d "); diff --git a/tests/baselines/reference/tsxReactEmitWhitespace.symbols b/tests/baselines/reference/tsxReactEmitWhitespace.symbols index a0d8266faa0..baac01227b6 100644 --- a/tests/baselines/reference/tsxReactEmitWhitespace.symbols +++ b/tests/baselines/reference/tsxReactEmitWhitespace.symbols @@ -79,7 +79,7 @@ var p = 0; ; >div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) -// Emit "foo" + ' ' + "bar" +// Emit "foo bar"
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) @@ -90,4 +90,21 @@ var p = 0;
; >div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) +// Emit "hello\\ world" +
+>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) + + hello\ + +world +
; +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) + +// Emit " a b c d " +
a +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) + + b c + d
; +>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) diff --git a/tests/baselines/reference/tsxReactEmitWhitespace.types b/tests/baselines/reference/tsxReactEmitWhitespace.types index f5ae431d342..0527c02904f 100644 --- a/tests/baselines/reference/tsxReactEmitWhitespace.types +++ b/tests/baselines/reference/tsxReactEmitWhitespace.types @@ -88,7 +88,7 @@ var p = 0; ; >div : any -// Emit "foo" + ' ' + "bar" +// Emit "foo bar"
>
foo bar
: JSX.Element >div : any @@ -100,4 +100,23 @@ var p = 0;
; >div : any +// Emit "hello\\ world" +
+>
hello\world
: JSX.Element +>div : any + + hello\ + +world +
; +>div : any + +// Emit " a b c d " +
a +>
a b c d
: JSX.Element +>div : any + + b c + d
; +>div : any diff --git a/tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx b/tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx index 34fd158eab1..be677b37ff8 100644 --- a/tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx +++ b/tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx @@ -42,7 +42,7 @@ var p = 0;
; -// Emit "foo" + ' ' + "bar" +// Emit "foo bar"
foo @@ -51,3 +51,15 @@ var p = 0;
; +// Emit "hello\\ world" +
+ + hello\ + +world +
; + +// Emit " a b c d " +
a + b c + d
;