@@ -160,63 +160,69 @@ namespace ts {
160
160
}
161
161
}
162
162
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.
166
186
let firstNonWhitespace = 0 ;
187
+ // Last non-whitespace character on this line.
167
188
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.
168
192
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
193
for ( let i = 0 ; i < text . length ; i ++ ) {
174
194
const c = text . charCodeAt ( i ) ;
175
195
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 ) ) ;
185
200
}
186
201
202
+ // Reset firstNonWhitespace for the next line.
203
+ // Don't bother to reset lastNonWhitespace because we ignore it if firstNonWhitespace = -1.
187
204
firstNonWhitespace = - 1 ;
188
205
}
189
- else if ( ! isWhiteSpace ( c ) ) {
206
+ else if ( ! isWhiteSpaceSingleLine ( c ) ) {
190
207
lastNonWhitespace = i ;
191
208
if ( firstNonWhitespace === - 1 ) {
192
209
firstNonWhitespace = i ;
193
210
}
194
211
}
195
212
}
196
213
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 ;
213
219
}
214
220
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 ;
220
226
}
221
227
222
228
/**
0 commit comments