diff --git a/.changeset/happy-beds-scream.md b/.changeset/happy-beds-scream.md new file mode 100644 index 0000000000..2a82aff9ba --- /dev/null +++ b/.changeset/happy-beds-scream.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: don't collapse whitespace within text nodes diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index ceb8838c90..97fc45d7a8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -162,24 +162,36 @@ export function clean_nodes( /** @type {import('#compiler').SvelteNode[]} */ const trimmed = []; - /** @type {import('#compiler').Text | null} */ - let last_text = null; + // Replace any whitespace between a text and non-text node with a single spaceand keep whitespace + // as-is within text nodes, or between text nodes and expression tags (because in the end they count + // as one text). This way whitespace is mostly preserved when using CSS with `white-space: pre-line` + // and default slot content going into a pre tag (which we can't see). + for (let i = 0; i < regular.length; i++) { + const prev = regular[i - 1]; + const node = regular[i]; + const next = regular[i + 1]; - // Replace any inbetween whitespace with a single space - for (const node of regular) { if (node.type === 'Text') { - node.data = node.data.replace(regex_whitespaces_strict, ' '); - node.raw = node.raw.replace(regex_whitespaces_strict, ' '); - if ( - (last_text === null && !can_remove_entirely) || - node.data !== ' ' || - node.data.charCodeAt(0) === 160 // non-breaking space - ) { + if (prev?.type !== 'ExpressionTag') { + const prev_is_text_ending_with_whitespace = + prev?.type === 'Text' && regex_ends_with_whitespaces.test(prev.data); + node.data = node.data.replace( + regex_starts_with_whitespaces, + prev_is_text_ending_with_whitespace ? '' : ' ' + ); + node.raw = node.raw.replace( + regex_starts_with_whitespaces, + prev_is_text_ending_with_whitespace ? '' : ' ' + ); + } + if (next?.type !== 'ExpressionTag') { + node.data = node.data.replace(regex_ends_with_whitespaces, ' '); + node.raw = node.raw.replace(regex_ends_with_whitespaces, ' '); + } + if (node.data && (node.data !== ' ' || !can_remove_entirely)) { trimmed.push(node); } - last_text = node; } else { - last_text = null; trimmed.push(node); } } diff --git a/packages/svelte/src/compiler/phases/patterns.js b/packages/svelte/src/compiler/phases/patterns.js index 74e715da1b..4f737adb3c 100644 --- a/packages/svelte/src/compiler/phases/patterns.js +++ b/packages/svelte/src/compiler/phases/patterns.js @@ -2,9 +2,9 @@ export const regex_whitespace = /\s/; export const regex_whitespaces = /\s+/; export const regex_starts_with_newline = /^\r?\n/; export const regex_starts_with_whitespace = /^\s/; -export const regex_starts_with_whitespaces = /^[ \t\r\n]*/; +export const regex_starts_with_whitespaces = /^[ \t\r\n]+/; export const regex_ends_with_whitespace = /\s$/; -export const regex_ends_with_whitespaces = /[ \t\r\n]*$/; +export const regex_ends_with_whitespaces = /[ \t\r\n]+$/; /** Not \S because that also removes explicit whitespace defined through things like ` ` */ export const regex_not_whitespace = /[^ \t\r\n]/; /** Not \s+ because that also includes explicit whitespace defined through things like ` ` */ diff --git a/packages/svelte/tests/runtime-legacy/samples/pre-tag/_config.js b/packages/svelte/tests/runtime-legacy/samples/pre-tag/_config.js index b8119f3f2c..d5a18488e2 100644 --- a/packages/svelte/tests/runtime-legacy/samples/pre-tag/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/pre-tag/_config.js @@ -20,7 +20,10 @@ function get_html(ssr) { E F -
A B C D E F
    A
+
A + B C + D E + F
    A
     B
     
       C
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index 02168c79f9..e8074b9e5b 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -66,7 +66,7 @@ export interface RuntimeTest = RecordTesting
+123          ;
+    456
`, + ssrHtml: `A B C D
Testing
+123          ;
+    456
` }); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/main.svelte index a85a932602..35bbf4b310 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/main.svelte @@ -1,5 +1,15 @@ + A {#snippet snip()}C{/snippet} B {@render snip()} D + +
+    Testing
+123          ;
+    456
+
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/pre.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/pre.svelte new file mode 100644 index 0000000000..821b578fa9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/pre.svelte @@ -0,0 +1,5 @@ + + +
{@render children()}