fix: better whitespace handling inside printer (#18638)

- preserve whitespace around elements if it leads to a line break
- preserve whitespace for whitespace-sensitive elements (pre, textarea,
title)

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/18628/head
Simon H 1 month ago committed by GitHub
parent a1d5035d17
commit ee6dff80ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: better whitespace handling inside printer

@ -19,6 +19,7 @@ const LINE_BREAK_THRESHOLD = 50;
*/ */
export function print(ast, options = undefined) { export function print(ast, options = undefined) {
const comments = (ast.type === 'Root' && ast.comments) || []; const comments = (ast.type === 'Root' && ast.comments) || [];
const state = { preserve_whitespace: 0 };
const css_comments = const css_comments =
(ast.type === 'Root' ? ast.css?.comments : ast.type === 'StyleSheet' ? ast.comments : null) || (ast.type === 'Root' ? ast.css?.comments : ast.type === 'StyleSheet' ? ast.comments : null) ||
[]; [];
@ -31,7 +32,7 @@ export function print(ast, options = undefined) {
getLeadingComments: options?.getLeadingComments, getLeadingComments: options?.getLeadingComments,
getTrailingComments: options?.getTrailingComments getTrailingComments: options?.getTrailingComments
}), }),
...svelte_visitors(comments), ...svelte_visitors(comments, state),
...css_visitors(css_comments, comments) ...css_visitors(css_comments, comments)
}), }),
{ {
@ -43,9 +44,15 @@ export function print(ast, options = undefined) {
/** /**
* @param {Context} context * @param {Context} context
* @param {AST.SvelteNode} node * @param {AST.SvelteNode} node
* @param {boolean} preserve_whitespace
* @param {boolean} allow_inline * @param {boolean} allow_inline
*/ */
function block(context, node, allow_inline = false) { function block(context, node, preserve_whitespace = false, allow_inline = false) {
if (preserve_whitespace) {
context.visit(node);
return;
}
const child_context = context.new(); const child_context = context.new();
child_context.visit(node); child_context.visit(node);
@ -146,8 +153,9 @@ function attributes(node, attributes, context, comments) {
* @param {AST.BaseElement} node * @param {AST.BaseElement} node
* @param {Context} context * @param {Context} context
* @param {AST.JSComment[]} comments * @param {AST.JSComment[]} comments
* @param {{ preserve_whitespace: number }} state
*/ */
function base_element(node, context, comments) { function base_element(node, context, comments, state) {
const child_context = context.new(); const child_context = context.new();
child_context.write('<' + node.name); child_context.write('<' + node.name);
@ -173,13 +181,30 @@ function base_element(node, context, comments) {
child_context.write(`${multiline_attributes ? '' : ' '}/>`); child_context.write(`${multiline_attributes ? '' : ' '}/>`);
} else { } else {
child_context.write('>'); child_context.write('>');
block(child_context, node.fragment, true); block(child_context, node.fragment, state.preserve_whitespace > 0, true);
child_context.write(`</${node.name}>`); child_context.write(`</${node.name}>`);
} }
context.append(child_context); context.append(child_context);
} }
/**
* @param {AST.BaseElement} node
* @param {Context} context
* @param {AST.JSComment[]} comments
* @param {{ preserve_whitespace: number }} state
*/
function print_element(node, context, comments, state) {
const name = node.name.toLowerCase();
const preserve =
(node.type === 'RegularElement' || node.type === 'TitleElement') &&
(name === 'pre' || name === 'textarea' || name === 'title');
if (preserve) state.preserve_whitespace += 1;
base_element(node, context, comments, state);
if (preserve) state.preserve_whitespace -= 1;
}
/** /**
* @param {AST.CSS.CSSComment[]} comments * @param {AST.CSS.CSSComment[]} comments
* @param {AST.JSComment[]} js_comments * @param {AST.JSComment[]} js_comments
@ -440,9 +465,10 @@ function css_visitors(comments, js_comments) {
/** /**
* @param {AST.JSComment[]} comments * @param {AST.JSComment[]} comments
* @param {{ preserve_whitespace: number }} state
* @returns {Visitors<AST.SvelteNode>} * @returns {Visitors<AST.SvelteNode>}
*/ */
const svelte_visitors = (comments) => ({ const svelte_visitors = (comments, state) => ({
Root(node, context) { Root(node, context) {
if (node.options) { if (node.options) {
context.write('<svelte:options'); context.write('<svelte:options');
@ -474,11 +500,27 @@ const svelte_visitors = (comments) => ({
context.write('<script'); context.write('<script');
attributes(node, node.attributes, context, comments); attributes(node, node.attributes, context, comments);
context.write('>'); context.write('>');
block(context, node.content); block(context, node.content, state.preserve_whitespace > 0);
context.write('</script>'); context.write('</script>');
}, },
Fragment(node, context) { Fragment(node, context) {
if (state.preserve_whitespace > 0) {
for (const child of node.nodes) {
context.visit(child);
context.multiline ||= child.type === 'Text' && /[\r\n]/.test(child.data);
}
return;
}
const first = node.nodes[0];
const last = node.nodes.at(-1);
const has_surrounding_whitespace =
first?.type === 'Text' &&
/^\s/.test(first.data) &&
last?.type === 'Text' &&
/\s$/.test(last.data);
/** @type {AST.SvelteNode[][]} */ /** @type {AST.SvelteNode[][]} */
const items = []; const items = [];
@ -570,6 +612,10 @@ const svelte_visitors = (comments) => ({
}); });
multiline ||= width > LINE_BREAK_THRESHOLD; multiline ||= width > LINE_BREAK_THRESHOLD;
// Normally context.newline() also makes context.multiline true, but the below loop only
// does that if we have more than one child context. If there's one long text block inside
// with whitespace at the edges we wanna split that up, too.
context.multiline ||= has_surrounding_whitespace && width > LINE_BREAK_THRESHOLD * 2;
for (let i = 0; i < child_contexts.length; i += 1) { for (let i = 0; i < child_contexts.length; i += 1) {
const prev = child_contexts[i]; const prev = child_contexts[i];
@ -636,7 +682,7 @@ const svelte_visitors = (comments) => ({
if (node.pending) { if (node.pending) {
context.write('}'); context.write('}');
block(context, node.pending); block(context, node.pending, state.preserve_whitespace > 0);
context.write('{:'); context.write('{:');
} else { } else {
context.write(' '); context.write(' ');
@ -647,7 +693,7 @@ const svelte_visitors = (comments) => ({
if (node.value) context.visit(node.value); if (node.value) context.visit(node.value);
context.write('}'); context.write('}');
block(context, node.then); block(context, node.then, state.preserve_whitespace > 0);
if (node.catch) { if (node.catch) {
context.write('{:'); context.write('{:');
@ -659,7 +705,7 @@ const svelte_visitors = (comments) => ({
if (node.error) context.visit(node.error); if (node.error) context.visit(node.error);
context.write('}'); context.write('}');
block(context, node.catch); block(context, node.catch, state.preserve_whitespace > 0);
} }
context.write('{/await}'); context.write('{/await}');
@ -703,7 +749,7 @@ const svelte_visitors = (comments) => ({
}, },
Component(node, context) { Component(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
ConstTag(node, context) { ConstTag(node, context) {
@ -793,11 +839,11 @@ const svelte_visitors = (comments) => ({
context.write('}'); context.write('}');
block(context, node.body); block(context, node.body, state.preserve_whitespace > 0);
if (node.fallback) { if (node.fallback) {
context.write('{:else}'); context.write('{:else}');
block(context, node.fallback); block(context, node.fallback, state.preserve_whitespace > 0);
} }
context.write('{/each}'); context.write('{/each}');
@ -821,13 +867,13 @@ const svelte_visitors = (comments) => ({
context.visit(node.test); context.visit(node.test);
context.write('}'); context.write('}');
block(context, node.consequent); block(context, node.consequent, state.preserve_whitespace > 0);
} else { } else {
context.write('{#if '); context.write('{#if ');
context.visit(node.test); context.visit(node.test);
context.write('}'); context.write('}');
block(context, node.consequent); block(context, node.consequent, state.preserve_whitespace > 0);
} }
if (node.alternate !== null) { if (node.alternate !== null) {
@ -839,7 +885,7 @@ const svelte_visitors = (comments) => ({
) )
) { ) {
context.write('{:else}'); context.write('{:else}');
block(context, node.alternate); block(context, node.alternate, state.preserve_whitespace > 0);
} else { } else {
context.visit(node.alternate); context.visit(node.alternate);
} }
@ -854,7 +900,7 @@ const svelte_visitors = (comments) => ({
context.write('{#key '); context.write('{#key ');
context.visit(node.expression); context.visit(node.expression);
context.write('}'); context.write('}');
block(context, node.fragment); block(context, node.fragment, state.preserve_whitespace > 0);
context.write('{/key}'); context.write('{/key}');
}, },
@ -886,7 +932,7 @@ const svelte_visitors = (comments) => ({
}, },
RegularElement(node, context) { RegularElement(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
RenderTag(node, context) { RenderTag(node, context) {
@ -896,7 +942,7 @@ const svelte_visitors = (comments) => ({
}, },
SlotElement(node, context) { SlotElement(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
SnippetBlock(node, context) { SnippetBlock(node, context) {
@ -915,7 +961,7 @@ const svelte_visitors = (comments) => ({
} }
context.write(')}'); context.write(')}');
block(context, node.body); block(context, node.body, state.preserve_whitespace > 0);
context.write('{/snippet}'); context.write('{/snippet}');
}, },
@ -951,11 +997,11 @@ const svelte_visitors = (comments) => ({
}, },
SvelteBody(node, context) { SvelteBody(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
SvelteBoundary(node, context) { SvelteBoundary(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
SvelteComponent(node, context) { SvelteComponent(node, context) {
@ -967,7 +1013,7 @@ const svelte_visitors = (comments) => ({
attributes(node, node.attributes, context, comments); attributes(node, node.attributes, context, comments);
if (node.fragment && node.fragment.nodes.length > 0) { if (node.fragment && node.fragment.nodes.length > 0) {
context.write('>'); context.write('>');
block(context, node.fragment, true); block(context, node.fragment, state.preserve_whitespace > 0, true);
context.write(`</svelte:component>`); context.write(`</svelte:component>`);
} else { } else {
context.write(' />'); context.write(' />');
@ -975,7 +1021,7 @@ const svelte_visitors = (comments) => ({
}, },
SvelteDocument(node, context) { SvelteDocument(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
SvelteElement(node, context) { SvelteElement(node, context) {
@ -988,7 +1034,7 @@ const svelte_visitors = (comments) => ({
if (node.fragment && node.fragment.nodes.length > 0) { if (node.fragment && node.fragment.nodes.length > 0) {
context.write('>'); context.write('>');
block(context, node.fragment); block(context, node.fragment, state.preserve_whitespace > 0);
context.write(`</svelte:element>`); context.write(`</svelte:element>`);
} else { } else {
context.write(' />'); context.write(' />');
@ -996,19 +1042,19 @@ const svelte_visitors = (comments) => ({
}, },
SvelteFragment(node, context) { SvelteFragment(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
SvelteHead(node, context) { SvelteHead(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
SvelteSelf(node, context) { SvelteSelf(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
SvelteWindow(node, context) { SvelteWindow(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
Text(node, context) { Text(node, context) {
@ -1016,7 +1062,7 @@ const svelte_visitors = (comments) => ({
}, },
TitleElement(node, context) { TitleElement(node, context) {
base_element(node, context, comments); print_element(node, context, comments, state);
}, },
TransitionDirective(node, context) { TransitionDirective(node, context) {

@ -0,0 +1,5 @@
<button asdasd asdioqwjdoiqwjd qowdjqwoidjqowijdoiqj>
hello very long liiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine
</button>
<span>hello very long liiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine</span>

@ -0,0 +1,5 @@
<button asdasd asdioqwjdoiqwjd qowdjqwoidjqowijdoiqj>
hello very long liiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine
</button>
<span>hello very long liiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine</span>

@ -0,0 +1,19 @@
<pre>
hel
lo
.
</pre>
<pre> before
<span> nested
text </span>
{#if visible} conditional
value{/if}</pre>
<textarea> first
second </textarea>
<svelte:head><title> hello
world </title></svelte:head>

@ -0,0 +1,21 @@
<pre>
hel
lo
.
</pre>
<pre> before
<span> nested
text </span>
{#if visible} conditional
value{/if}</pre>
<textarea> first
second </textarea>
<svelte:head>
<title> hello
world </title>
</svelte:head>
Loading…
Cancel
Save