Skip to content

Commit 5d3385c

Browse files
authored
fix: don't collapse whitespace within text nodes (#10691)
fixes #9892
1 parent 0a9ba93 commit 5d3385c

File tree

8 files changed

+68
-23
lines changed

8 files changed

+68
-23
lines changed

.changeset/happy-beds-scream.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: don't collapse whitespace within text nodes

packages/svelte/src/compiler/phases/3-transform/utils.js

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -162,24 +162,36 @@ export function clean_nodes(
162162
/** @type {import('#compiler').SvelteNode[]} */
163163
const trimmed = [];
164164

165-
/** @type {import('#compiler').Text | null} */
166-
let last_text = null;
165+
// Replace any whitespace between a text and non-text node with a single spaceand keep whitespace
166+
// as-is within text nodes, or between text nodes and expression tags (because in the end they count
167+
// as one text). This way whitespace is mostly preserved when using CSS with `white-space: pre-line`
168+
// and default slot content going into a pre tag (which we can't see).
169+
for (let i = 0; i < regular.length; i++) {
170+
const prev = regular[i - 1];
171+
const node = regular[i];
172+
const next = regular[i + 1];
167173

168-
// Replace any inbetween whitespace with a single space
169-
for (const node of regular) {
170174
if (node.type === 'Text') {
171-
node.data = node.data.replace(regex_whitespaces_strict, ' ');
172-
node.raw = node.raw.replace(regex_whitespaces_strict, ' ');
173-
if (
174-
(last_text === null && !can_remove_entirely) ||
175-
node.data !== ' ' ||
176-
node.data.charCodeAt(0) === 160 // non-breaking space
177-
) {
175+
if (prev?.type !== 'ExpressionTag') {
176+
const prev_is_text_ending_with_whitespace =
177+
prev?.type === 'Text' && regex_ends_with_whitespaces.test(prev.data);
178+
node.data = node.data.replace(
179+
regex_starts_with_whitespaces,
180+
prev_is_text_ending_with_whitespace ? '' : ' '
181+
);
182+
node.raw = node.raw.replace(
183+
regex_starts_with_whitespaces,
184+
prev_is_text_ending_with_whitespace ? '' : ' '
185+
);
186+
}
187+
if (next?.type !== 'ExpressionTag') {
188+
node.data = node.data.replace(regex_ends_with_whitespaces, ' ');
189+
node.raw = node.raw.replace(regex_ends_with_whitespaces, ' ');
190+
}
191+
if (node.data && (node.data !== ' ' || !can_remove_entirely)) {
178192
trimmed.push(node);
179193
}
180-
last_text = node;
181194
} else {
182-
last_text = null;
183195
trimmed.push(node);
184196
}
185197
}

packages/svelte/src/compiler/phases/patterns.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ export const regex_whitespace = /\s/;
22
export const regex_whitespaces = /\s+/;
33
export const regex_starts_with_newline = /^\r?\n/;
44
export const regex_starts_with_whitespace = /^\s/;
5-
export const regex_starts_with_whitespaces = /^[ \t\r\n]*/;
5+
export const regex_starts_with_whitespaces = /^[ \t\r\n]+/;
66
export const regex_ends_with_whitespace = /\s$/;
7-
export const regex_ends_with_whitespaces = /[ \t\r\n]*$/;
7+
export const regex_ends_with_whitespaces = /[ \t\r\n]+$/;
88
/** Not \S because that also removes explicit whitespace defined through things like `&nbsp;` */
99
export const regex_not_whitespace = /[^ \t\r\n]/;
1010
/** Not \s+ because that also includes explicit whitespace defined through things like `&nbsp;` */

packages/svelte/tests/runtime-legacy/samples/pre-tag/_config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ function get_html(ssr) {
2020
</span>
2121
E
2222
F
23-
</pre> <div id="div">A B <span>C D</span> E F</div> <div id="div-with-pre"><pre> A
23+
</pre> <div id="div">A
24+
B <span>C
25+
D</span> E
26+
F</div> <div id="div-with-pre"><pre> A
2427
B
2528
<span>
2629
C

packages/svelte/tests/runtime-legacy/shared.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
6666
runtime_error?: string;
6767
warnings?: string[];
6868
expect_unhandled_rejections?: boolean;
69-
withoutNormalizeHtml?: boolean;
69+
withoutNormalizeHtml?: boolean | 'only-strip-comments';
7070
recover?: boolean;
7171
}
7272

@@ -213,13 +213,15 @@ async function run_test_variant(
213213
if (variant === 'ssr') {
214214
if (config.ssrHtml) {
215215
assert_html_equal_with_options(target.innerHTML, config.ssrHtml, {
216-
preserveComments: config.compileOptions?.preserveComments,
217-
withoutNormalizeHtml: config.withoutNormalizeHtml
216+
preserveComments:
217+
config.withoutNormalizeHtml === 'only-strip-comments' ? false : undefined,
218+
withoutNormalizeHtml: !!config.withoutNormalizeHtml
218219
});
219220
} else if (config.html) {
220221
assert_html_equal_with_options(target.innerHTML, config.html, {
221-
preserveComments: config.compileOptions?.preserveComments,
222-
withoutNormalizeHtml: config.withoutNormalizeHtml
222+
preserveComments:
223+
config.withoutNormalizeHtml === 'only-strip-comments' ? false : undefined,
224+
withoutNormalizeHtml: !!config.withoutNormalizeHtml
223225
});
224226
}
225227

@@ -283,7 +285,9 @@ async function run_test_variant(
283285
if (config.html) {
284286
$.flushSync();
285287
assert_html_equal_with_options(target.innerHTML, config.html, {
286-
withoutNormalizeHtml: config.withoutNormalizeHtml
288+
preserveComments:
289+
config.withoutNormalizeHtml === 'only-strip-comments' ? false : undefined,
290+
withoutNormalizeHtml: !!config.withoutNormalizeHtml
287291
});
288292
}
289293

packages/svelte/tests/runtime-runes/samples/snippet-whitespace/_config.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@ export default test({
44
compileOptions: {
55
dev: true // Render in dev mode to check that the validation error is not thrown
66
},
7-
html: `A\nB\nC\nD`
7+
withoutNormalizeHtml: 'only-strip-comments',
8+
html: `A B C D <pre>Testing
9+
123 ;
10+
456</pre>`,
11+
ssrHtml: `A B C D <pre>Testing
12+
123 ;
13+
456</pre>`
814
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1+
<script>
2+
import Pre from "./pre.svelte";
3+
4+
</script>
15
A
26
{#snippet snip()}C{/snippet}
37
B
48
{@render snip()}
59
D
10+
11+
<Pre>
12+
Testing
13+
123 ;
14+
456
15+
</Pre>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
let { children } = $props();
3+
</script>
4+
5+
<pre>{@render children()}</pre>

0 commit comments

Comments
 (0)