fix: don't collapse whitespace within text nodes (#10691)

fixes #9892
pull/10693/head
Simon H 2 years ago committed by GitHub
parent 0a9ba9340b
commit 5d3385c56f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: don't collapse whitespace within text nodes

@ -162,24 +162,36 @@ export function clean_nodes(
/** @type {import('#compiler').SvelteNode[]} */ /** @type {import('#compiler').SvelteNode[]} */
const trimmed = []; const trimmed = [];
/** @type {import('#compiler').Text | null} */ // Replace any whitespace between a text and non-text node with a single spaceand keep whitespace
let last_text = null; // 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') { if (node.type === 'Text') {
node.data = node.data.replace(regex_whitespaces_strict, ' '); if (prev?.type !== 'ExpressionTag') {
node.raw = node.raw.replace(regex_whitespaces_strict, ' '); const prev_is_text_ending_with_whitespace =
if ( prev?.type === 'Text' && regex_ends_with_whitespaces.test(prev.data);
(last_text === null && !can_remove_entirely) || node.data = node.data.replace(
node.data !== ' ' || regex_starts_with_whitespaces,
node.data.charCodeAt(0) === 160 // non-breaking space 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); trimmed.push(node);
} }
last_text = node;
} else { } else {
last_text = null;
trimmed.push(node); trimmed.push(node);
} }
} }

@ -2,9 +2,9 @@ export const regex_whitespace = /\s/;
export const regex_whitespaces = /\s+/; export const regex_whitespaces = /\s+/;
export const regex_starts_with_newline = /^\r?\n/; export const regex_starts_with_newline = /^\r?\n/;
export const regex_starts_with_whitespace = /^\s/; 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_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 `&nbsp;` */ /** Not \S because that also removes explicit whitespace defined through things like `&nbsp;` */
export const regex_not_whitespace = /[^ \t\r\n]/; export const regex_not_whitespace = /[^ \t\r\n]/;
/** Not \s+ because that also includes explicit whitespace defined through things like `&nbsp;` */ /** Not \s+ because that also includes explicit whitespace defined through things like `&nbsp;` */

@ -20,7 +20,10 @@ function get_html(ssr) {
</span> </span>
E E
F F
</pre> <div id="div">A B <span>C D</span> E F</div> <div id="div-with-pre"><pre> A </pre> <div id="div">A
B <span>C
D</span> E
F</div> <div id="div-with-pre"><pre> A
B B
<span> <span>
C C

@ -66,7 +66,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
runtime_error?: string; runtime_error?: string;
warnings?: string[]; warnings?: string[];
expect_unhandled_rejections?: boolean; expect_unhandled_rejections?: boolean;
withoutNormalizeHtml?: boolean; withoutNormalizeHtml?: boolean | 'only-strip-comments';
recover?: boolean; recover?: boolean;
} }
@ -213,13 +213,15 @@ async function run_test_variant(
if (variant === 'ssr') { if (variant === 'ssr') {
if (config.ssrHtml) { if (config.ssrHtml) {
assert_html_equal_with_options(target.innerHTML, config.ssrHtml, { assert_html_equal_with_options(target.innerHTML, config.ssrHtml, {
preserveComments: config.compileOptions?.preserveComments, preserveComments:
withoutNormalizeHtml: config.withoutNormalizeHtml config.withoutNormalizeHtml === 'only-strip-comments' ? false : undefined,
withoutNormalizeHtml: !!config.withoutNormalizeHtml
}); });
} else if (config.html) { } else if (config.html) {
assert_html_equal_with_options(target.innerHTML, config.html, { assert_html_equal_with_options(target.innerHTML, config.html, {
preserveComments: config.compileOptions?.preserveComments, preserveComments:
withoutNormalizeHtml: config.withoutNormalizeHtml config.withoutNormalizeHtml === 'only-strip-comments' ? false : undefined,
withoutNormalizeHtml: !!config.withoutNormalizeHtml
}); });
} }
@ -283,7 +285,9 @@ async function run_test_variant(
if (config.html) { if (config.html) {
$.flushSync(); $.flushSync();
assert_html_equal_with_options(target.innerHTML, config.html, { assert_html_equal_with_options(target.innerHTML, config.html, {
withoutNormalizeHtml: config.withoutNormalizeHtml preserveComments:
config.withoutNormalizeHtml === 'only-strip-comments' ? false : undefined,
withoutNormalizeHtml: !!config.withoutNormalizeHtml
}); });
} }

@ -4,5 +4,11 @@ export default test({
compileOptions: { compileOptions: {
dev: true // Render in dev mode to check that the validation error is not thrown dev: true // Render in dev mode to check that the validation error is not thrown
}, },
html: `A\nB\nC\nD` withoutNormalizeHtml: 'only-strip-comments',
html: `A B C D <pre>Testing
123 ;
456</pre>`,
ssrHtml: `A B C D <pre>Testing
123 ;
456</pre>`
}); });

@ -1,5 +1,15 @@
<script>
import Pre from "./pre.svelte";
</script>
A A
{#snippet snip()}C{/snippet} {#snippet snip()}C{/snippet}
B B
{@render snip()} {@render snip()}
D D
<Pre>
Testing
123 ;
456
</Pre>

@ -0,0 +1,5 @@
<script>
let { children } = $props();
</script>
<pre>{@render children()}</pre>
Loading…
Cancel
Save