Skip to content

Commit 91af4ae

Browse files
author
Andy
authored
Merge pull request #10782 from Microsoft/react_whitespace
For JSX text, construct a single literal node `"foo bar"` instead of `"foo" + " " + "bar"`
2 parents 1978c5b + 2248c98 commit 91af4ae

File tree

5 files changed

+114
-44
lines changed

5 files changed

+114
-44
lines changed

src/compiler/transformers/jsx.ts

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -160,63 +160,69 @@ namespace ts {
160160
}
161161
}
162162

163-
function visitJsxText(node: JsxText) {
164-
const text = getTextOfNode(node, /*includeTrivia*/ true);
165-
let parts: Expression[];
163+
function visitJsxText(node: JsxText): StringLiteral | undefined {
164+
const fixed = fixupWhitespaceAndDecodeEntities(getTextOfNode(node, /*includeTrivia*/ true));
165+
return fixed === undefined ? undefined : createLiteral(fixed);
166+
}
167+
168+
/**
169+
* JSX trims whitespace at the end and beginning of lines, except that the
170+
* start/end of a tag is considered a start/end of a line only if that line is
171+
* on the same line as the closing tag. See examples in
172+
* tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
173+
* 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
174+
*
175+
* An equivalent algorithm would be:
176+
* - If there is only one line, return it.
177+
* - If there is only whitespace (but multiple lines), return `undefined`.
178+
* - Split the text into lines.
179+
* - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
180+
* - Decode entities on each line (individually).
181+
* - Remove empty lines and join the rest with " ".
182+
*/
183+
function fixupWhitespaceAndDecodeEntities(text: string): string | undefined {
184+
let acc: string | undefined;
185+
// First non-whitespace character on this line.
166186
let firstNonWhitespace = 0;
187+
// Last non-whitespace character on this line.
167188
let lastNonWhitespace = -1;
189+
// These initial values are special because the first line is:
190+
// firstNonWhitespace = 0 to indicate that we want leading whitsepace,
191+
// but lastNonWhitespace = -1 as a special flag to indicate that we *don't* include the line if it's all whitespace.
168192

169-
// JSX trims whitespace at the end and beginning of lines, except that the
170-
// start/end of a tag is considered a start/end of a line only if that line is
171-
// on the same line as the closing tag. See examples in
172-
// tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
173193
for (let i = 0; i < text.length; i++) {
174194
const c = text.charCodeAt(i);
175195
if (isLineBreak(c)) {
176-
if (firstNonWhitespace !== -1 && (lastNonWhitespace - firstNonWhitespace + 1 > 0)) {
177-
const part = text.substr(firstNonWhitespace, lastNonWhitespace - firstNonWhitespace + 1);
178-
if (!parts) {
179-
parts = [];
180-
}
181-
182-
// We do not escape the string here as that is handled by the printer
183-
// when it emits the literal. We do, however, need to decode JSX entities.
184-
parts.push(createLiteral(decodeEntities(part)));
196+
// If we've seen any non-whitespace characters on this line, add the 'trim' of the line.
197+
// (lastNonWhitespace === -1 is a special flag to detect whether the first line is all whitespace.)
198+
if (firstNonWhitespace !== -1 && lastNonWhitespace !== -1) {
199+
acc = addLineOfJsxText(acc, text.substr(firstNonWhitespace, lastNonWhitespace - firstNonWhitespace + 1));
185200
}
186201

202+
// Reset firstNonWhitespace for the next line.
203+
// Don't bother to reset lastNonWhitespace because we ignore it if firstNonWhitespace = -1.
187204
firstNonWhitespace = -1;
188205
}
189-
else if (!isWhiteSpace(c)) {
206+
else if (!isWhiteSpaceSingleLine(c)) {
190207
lastNonWhitespace = i;
191208
if (firstNonWhitespace === -1) {
192209
firstNonWhitespace = i;
193210
}
194211
}
195212
}
196213

197-
if (firstNonWhitespace !== -1) {
198-
const part = text.substr(firstNonWhitespace);
199-
if (!parts) {
200-
parts = [];
201-
}
202-
203-
// We do not escape the string here as that is handled by the printer
204-
// when it emits the literal. We do, however, need to decode JSX entities.
205-
parts.push(createLiteral(decodeEntities(part)));
206-
}
207-
208-
if (parts) {
209-
return reduceLeft(parts, aggregateJsxTextParts);
210-
}
211-
212-
return undefined;
214+
return firstNonWhitespace !== -1
215+
// Last line had a non-whitespace character. Emit the 'trimLeft', meaning keep trailing whitespace.
216+
? addLineOfJsxText(acc, text.substr(firstNonWhitespace))
217+
// Last line was all whitespace, so ignore it
218+
: acc;
213219
}
214220

215-
/**
216-
* Aggregates two expressions by interpolating them with a whitespace literal.
217-
*/
218-
function aggregateJsxTextParts(left: Expression, right: Expression) {
219-
return createAdd(createAdd(left, createLiteral(" ")), right);
221+
function addLineOfJsxText(acc: string | undefined, trimmedLine: string): string {
222+
// We do not escape the string here as that is handled by the printer
223+
// when it emits the literal. We do, however, need to decode JSX entities.
224+
const decoded = decodeEntities(trimmedLine);
225+
return acc === undefined ? decoded : acc + " " + decoded;
220226
}
221227

222228
/**

tests/baselines/reference/tsxReactEmitWhitespace.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ var p = 0;
4141
<div>
4242
</div>;
4343

44-
// Emit "foo" + ' ' + "bar"
44+
// Emit "foo bar"
4545
<div>
4646

4747
foo
@@ -50,6 +50,18 @@ var p = 0;
5050

5151
</div>;
5252

53+
// Emit "hello\\ world"
54+
<div>
55+
56+
hello\
57+
58+
world
59+
</div>;
60+
61+
// Emit " a b c d "
62+
<div> a
63+
b c
64+
d </div>;
5365

5466

5567
//// [file.js]
@@ -75,5 +87,9 @@ React.createElement("div", null, " 3 ");
7587
React.createElement("div", null, "3");
7688
// Emit no args
7789
React.createElement("div", null);
78-
// Emit "foo" + ' ' + "bar"
79-
React.createElement("div", null, "foo" + " " + "bar");
90+
// Emit "foo bar"
91+
React.createElement("div", null, "foo bar");
92+
// Emit "hello\\ world"
93+
React.createElement("div", null, "hello\\ world");
94+
// Emit " a b c d "
95+
React.createElement("div", null, " a b c d ");

tests/baselines/reference/tsxReactEmitWhitespace.symbols

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ var p = 0;
7979
</div>;
8080
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))
8181

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

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

93+
// Emit "hello\\ world"
94+
<div>
95+
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))
96+
97+
hello\
98+
99+
world
100+
</div>;
101+
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))
102+
103+
// Emit " a b c d "
104+
<div> a
105+
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))
106+
107+
b c
108+
d </div>;
109+
>div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22))
93110

tests/baselines/reference/tsxReactEmitWhitespace.types

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ var p = 0;
8888
</div>;
8989
>div : any
9090

91-
// Emit "foo" + ' ' + "bar"
91+
// Emit "foo bar"
9292
<div>
9393
><div> foo bar </div> : JSX.Element
9494
>div : any
@@ -100,4 +100,23 @@ var p = 0;
100100
</div>;
101101
>div : any
102102

103+
// Emit "hello\\ world"
104+
<div>
105+
><div> hello\world</div> : JSX.Element
106+
>div : any
107+
108+
hello\
109+
110+
world
111+
</div>;
112+
>div : any
113+
114+
// Emit " a b c d "
115+
<div> a
116+
><div> a b c d </div> : JSX.Element
117+
>div : any
118+
119+
b c
120+
d </div>;
121+
>div : any
103122

tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ var p = 0;
4242
<div>
4343
</div>;
4444

45-
// Emit "foo" + ' ' + "bar"
45+
// Emit "foo bar"
4646
<div>
4747

4848
foo
@@ -51,3 +51,15 @@ var p = 0;
5151

5252
</div>;
5353

54+
// Emit "hello\\ world"
55+
<div>
56+
57+
hello\
58+
59+
world
60+
</div>;
61+
62+
// Emit " a b c d "
63+
<div> a
64+
b c
65+
d </div>;

0 commit comments

Comments
 (0)