Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 44 additions & 38 deletions src/compiler/transformers/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,63 +151,69 @@ namespace ts {
}
}

function visitJsxText(node: JsxText) {
const text = getTextOfNode(node, /*includeTrivia*/ true);
let parts: Expression[];
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;
}
}
}

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;
}

/**
Expand Down
22 changes: 19 additions & 3 deletions tests/baselines/reference/tsxReactEmitWhitespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ var p = 0;
<div>
</div>;

// Emit "foo" + ' ' + "bar"
// Emit "foo bar"
<div>

foo
Expand All @@ -50,6 +50,18 @@ var p = 0;

</div>;

// Emit "hello\\ world"
<div>

hello\

world
</div>;

// Emit " a b c d "
<div> a
b c
d </div>;


//// [file.js]
Expand All @@ -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 ");
19 changes: 18 additions & 1 deletion tests/baselines/reference/tsxReactEmitWhitespace.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ var p = 0;
</div>;
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))

// Emit "foo" + ' ' + "bar"
// Emit "foo bar"
<div>
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))

Expand All @@ -90,4 +90,21 @@ var p = 0;
</div>;
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))

// Emit "hello\\ world"
<div>
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))

hello\

world
</div>;
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))

// Emit " a b c d "
<div> a
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))

b c
d </div>;
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))

21 changes: 20 additions & 1 deletion tests/baselines/reference/tsxReactEmitWhitespace.types
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ var p = 0;
</div>;
>div : any

// Emit "foo" + ' ' + "bar"
// Emit "foo bar"
<div>
><div> foo bar </div> : JSX.Element
>div : any
Expand All @@ -100,4 +100,23 @@ var p = 0;
</div>;
>div : any

// Emit "hello\\ world"
<div>
><div> hello\world</div> : JSX.Element
>div : any

hello\

world
</div>;
>div : any

// Emit " a b c d "
<div> a
><div> a b c d </div> : JSX.Element
>div : any

b c
d </div>;
>div : any

14 changes: 13 additions & 1 deletion tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ var p = 0;
<div>
</div>;

// Emit "foo" + ' ' + "bar"
// Emit "foo bar"
<div>

foo
Expand All @@ -51,3 +51,15 @@ var p = 0;

</div>;

// Emit "hello\\ world"
<div>

hello\

world
</div>;

// Emit " a b c d "
<div> a
b c
d </div>;