@@ -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 /**
0 commit comments