Closed Rich-Harris closed 5 months ago
I personally would like to avoid all inter-element whitespace as much as possible.
Maybe do something similar to pug and jsx (at least I think thats what it does), where whitespace is only added if the elements are on the same line (inline). Tags with newlines in between get that whitespace stripped completely.
Pug even has special syntax for inline tags: https://pugjs.org/language/interpolation.html
So for example:
<ul>
<li>
Some <strong>inline</strong> tags.
</li>
</ul>
would generate the following html:
<ul><li>Some <strong>inline</strong> tags.</li></ul>
That actually happens already – whitespace inside either end of an element is removed: https://svelte.technology/repl/?gist=f4657520185203c009a9116568ac5ba2
The problems start when you have siblings separated by newlines:
<ul>
<li>
Some
<strong>inline</strong>
<em>newline separated</em>
tags.
</li>
</ul>
In that case you can't remove whitespace before the <strong>
, or between the <strong>
and the <em>
, or after the <em>
. You could collapse the intermediate whitespace to a single space character and it would behave identically (assuming no white-space: pre
CSS or similar), but you can't remove it. You can probably remove it if the elements are block-level, but it's unsafe because it depends on display: block
.
I thought about using a whitelist of inline elements as well, but I don’t think that’s a good idea. I rather think we should do the same as react/jsx and pug do here and remove all whitespace when elements are on a newline.
If you explicitly want to have it on a newline, one could render the text as an explicit snippet <span>{{" like so, with whitespace "}}</span>
. I have seen this pattern a few times in react codebases, although I admit its ugly.
Trouble is pug and JSX are different languages, so they can get away with different semantics. Svelte templates are just HTML (plus tags, blocks and directives), so from the user's perspective if markup behaves differently then it's a bug. Maybe it could be an opt-in thing? whitespace: 'aggressive'
or something?
If we do collapse whitespace to a single character, then when we get round to implementing helpers it won't be quite so bad:
// instead of this...
var text4 = document.createTextNode( '\n\t\t\t' );
// ...this
var text4 = whitespace();
Its really a question what the user expects I guess… More often things are breaking because of unintended whitespace. And depending on the users background, they might be used to template system that have a more aggressive whitespace handling.
What I'm currently doing is sending my templates through https://github.com/kangax/html-minifier before sending them through svelte -
HTMLMinifier.minify(html, {
caseSensitive: true,
collapseWhitespace: true,
conservativeCollapse: true,
ignoreCustomFragments: [ /{{[^]*?}}/ ],
keepClosingSlash: true,
})
which seems to work well enough. conservativeCollapse
makes runs of whitespace get collapsed down to one space instead of zero. caseSensitive
and keepClosingSlash
are necessary to keep svelte happy with the template. And ignoreCustomFragments
is necessary to keep html-minifier from trying to parse tags.
I think if you look at the HTML 5 spec they have an interesting way of handling this. You can have different insertion modes where character tokens are handled differently. See "in-select" insertion mode for example: https://www.w3.org/TR/html5/syntax.html#parsing-main-inselect
Removing whitespace between tags is important for layouts using display: inline-block
.
I agree that preserving the HTML expectations is important, so I think that whitespace removal should be explicit. As @Ryuno-Ki mentioned, the Twig spaceless
tag is a great solution.
{% spaceless %}
<div>
<strong>foo</strong>
</div>
{% endspaceless %}
{# output will be <div><strong>foo</strong></div> #}
Could we do something like
export default {
keepWhitespace: true
}
then check that at compile time?
It seems we can no longer naively pass Svelte components through html-minifier
because of the new Svelte-specific tags (eg. <:Head>
).
Does anyone have a workaround for that?
My comment here could probably be extended to use ignoreCustomFragments
to also ignore <:Head>
. Another regex could be added, something like /<:Head>[^]*?<\/:Head>/
. I'm no longer using the method from that comment, however, so I haven't tried this.
First time user, obvious question: why not just copy React?
@Rich-Harris referring back to a comment you made in 2016:
Svelte templates are just HTML (plus tags, blocks and directives), so from the user's perspective if markup behaves differently then it's a bug.
I'm skeptical -- maybe because other frameworks have changed my expectations. I'm new to Svelte (and love it): I think of it as a transpiler. I don't feel I'm writing HTML: I feel I'm writing JavaScript with an abundance of <
and >
characters.
When I first used React (very recently), I had no preconceptions about whitespace. I learned to my delight that JSX is not HTML.
And now I've just learned Svelte, and I see myself writing huge statements on one line. Here's code from my second-ever Svelte component, which happens to use the dreaded <pre>
tag:
<pre ref:pre class={{wrapText ? 'wrap' : ''}}>{{#each plainAndHighlightedPairs as [ plain, highlighted ], i}}{{plain}}{{#if highlighted}}<em class={{highlightIndex === i ? 'current' : ''}}>{{highlighted}}</em>{{/if}}{{/each}}</pre>
Svelte is getting in the way. Can that line be sensible?
If Svelte followed React's whitespace rules, it would be legible:
<pre ref:pre class={{wrapText ? 'wrap' : ''}}>
{{#each plainAndHighlightedPairs as [ plain, highlighted ], i}}
{{plain}}
{{#if highlighted}}
<em class={{highlightIndex === i ? 'current' : ''}}>{{highlighted}}</em>
{{/if}}
{{/each}}
</pre>
Svelte's choice is between "pretend to be HTML" and "pretend to be easy." They're mutually exclusive. People choose Svelte because HTML is too complicated: it could be easier here. And yes, changing whitespace rules is backwards-incompatible; but hey, React did it.
This is an awfully long comment, so I'll finish with a rhetorical flourish: can any reader here describe HTML's whitespace rules from memory?
Just rediscovered this issue via #1236. @adamhooper you make a strong case; the <pre>
example is gnarly. I have a counter-example though — I see this sort of thing a lot in React codebases:
<p>
click
{' '}
<a href={`/foo/${this.props.bar}`}>here</a>
{' '}
for more information
</p>
The Svelte/HTML equivalent:
<p>
click
<a href='/foo/{{bar}}'>here</a>
for more information
</p>
Having to insert {' '}
in order to preserve spaces around the <a>
is utterly grotesque, and I'd argue that the JSX rules (whitespace is significant unless it happens to contain a newline) are ultimately no more intuitive than HTML rules.
Not suggesting that we have to comply strictly with HTML, just that we need to weigh up the consequences of deviating from it.
I agree with @adamhooper on this and would love to see Svelte's HTML output with collapsed whitespace as React does it. Things like inline-block menus, tab headers, and breadcrumbs become a big mess of code when you want the whitespace eliminated. For example:
<ul class="breadcrumbs">
{#each breadcrumbs as item, i}<li>{#if i !== breadcrumbs.length - 1}<a href="{item.url}">{item.title}</a>{:else}{item.title}{/if}</li>{/each}
</ul>
Having to insert
{' '}
[in JSX] in order to preserve spaces around the<a>
is utterly grotesque
I type it out all on one line and then let Prettier do the {' '}
for me ¯\_(ツ)_/¯
Angular has similar feature of removing whitespaces. https://angular.io/api/core/Component#preserving-whitespace
Key takeaways:
ngPreserveWhitespaces
directive that affects whole element subtree can be applied &ngsp;
entity - that's certainly better than {' '}
preserveWhitespace
. I see that's it's supported already in Svelte<pre>
tags are handled specially. Angular documentation also mentions <textarea>
and more tags (if any) could be looked up in their repo.I'd like to note that as a longtime user of Angular I never had problems stemming from this particular feature. It's enabled by default and optimises generated code a lot. Behaviour becomes natural like export let
for props and on rare occasion if something doesn't look right you notice it during development and apply appropriate override.
Having to insert
{' '}
in order to preserve spaces around the<a>
is utterly grotesque
Spaces between elements seems to be the exception rather than the rule, so you don't see too much {' '}
.
I'd argue that the JSX rules (whitespace is significant unless it happens to contain a newline) are ultimately no more intuitive than HTML rules.
They're very intuitive. I've been caught out many times by extraneous spaces between and within HTML elements. I've had no such surprises with JSX.
It looks like this one is still heavily under discussion. I can echo @tomblachut 's experience from Angular. What Angular does "just works" for almost all cases, producing efficient output by default. "Efficient by default" is an important trade-off against what @Rich-Harris mentioned earlier, behaving as much as possible like ordinary HTML by default.
Every once in a while it causes trouble - rarely enough that switching to "preserve whitespace" for a subtree or entire component fixes it easily with only a tiny impact on an overall applications compiled output size.
I saw this but I'm asking if this is the same for my problem: https://github.com/sveltejs/svelte/issues/2745
I opened an issue on VSCode Svelte plugin repo (https://github.com/UnwrittenFun/svelte-vscode/issues/50).
The problem is I need this code to stay like this:
<div>
Test (<span class="color">one</span>)
</div>
It becomes this instead:
<div>
Test (
<span class="color">one</span>
)
</div>
Is it related to Svelte compiler or an issue with ghost spaces added by vscode plugin?
What can I do?
I'm not understanding.
@frederikhors It sounds like the VSCode Svelte plugin's formatter is not following the current whitespace rules of Svelte (basically how whitespace works in HTML). I think this is the issue you encounter: https://github.com/UnwrittenFun/prettier-plugin-svelte/issues/24
This thread is about potentially changing how whitespace works in Svelte in the future.
I found a ready to use preprocessor for that - https://www.npmjs.com/package/@minna-ui/svelte-preprocess-markup
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import minify from 'rollup-plugin-babel-minify';
import sveltePreprocess from 'svelte-preprocess';
import preprocessMarkup from '@minna-ui/svelte-preprocess-markup';
export default {
input: 'src/index.js',
output: {
name: 'viewflow',
file: 'dist/viewflow-components.js',
format: 'iife',
},
plugins: [
svelte({
include: 'src/*.svelte',
preprocess: [sveltePreprocess(), {'markup': preprocessMarkup()}],
customElement: true,
}),
resolve(),
minify(),
],
};
I've managed to hack it abit to remove all the spaces between the tags with this:
const tagsRegex1 = /(>)[\s]*([<{])/g;
const tagsRegex2 = /({[/:][a-z]+})[\s]*([<{])/g;
const tagsRegex3 = /({[#:][a-z]+ .+?})[\s]*([<{])/g;
const tagsRegex4 = /([>}])[\s]+(<|{[/#:][a-z][^}]*})/g;
const tagsReplace = '$1$2';
const opts = {
preprocess: {
style: processStyle,
markup({content}) {
const code = content
.replace(tagsRegex1, tagsReplace)
.replace(tagsRegex2, tagsReplace)
.replace(tagsRegex3, tagsReplace)
.replace(tagsRegex4, tagsReplace)
;
return {code};
},
},
};
But when I run the app, it still creates those text nodes.. this is a major problem WordPress on IE11 since they use some kind of MutationObserver to replace emoji with concatenating all adjacent text nodes even if it's empty and crashes.
I've finally nailed it down to the root cause of the empty text nodes, a suggested fix is here https://github.com/sveltejs/svelte/issues/4423
Is there currently a workaround to force svelte to preserve whitespace? Server-side rendering currently correctly preserves whitespace when doing
<pre>foo
</pre>
<p>bar</p>
but strips a newline during hydration.
This is especially frustrating dealing with code-highlighting where
- script: |
npm pack
mv dom-accessibility-api-*.tgz dom-accessibility-api.tgz
displayName: "Create tarball"
becomes
<span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">|
npm pack
mv dom-accessibility-api-*.tgz dom-accessibility-api.tgz
</span> <span class="hljs-attr">displayName:</span> <span class="hljs-string">'Create tarball'</span>
The result changes between hydration. Before: After:
Just stumbled across this issue. FWIW I'm using a regex to collapse whitespace which reduces the JS bundle size (minified, uncompressed) from 40.22kB to 39.82kB. I might give svelte-preprocess-markup
a shot though.
Can we just have an option (per-component like immutable, or whole project), which prevents something like t1 = space();
from being added to the JS output of the compiler?
It feels like doing this in the preprocess step with regex is asking for a trouble. Something which works now could break later, when we will have more special syntax.
Alternatively, if that compiler option is undesirable, could we have community approved preprocessing minifier config to get rid of all unnecessary whitespace while introducing no minify-related bugs?
Here is the code to test in repl.
<script>
const log = node => console.log(node.childNodes);
</script>
<div use:log>
<div on:click={() => 'preventMeSomeInnerHTML'}>haha</div>
<div>haha</div>
</div>
Personally, I would choose stripping whitespace and entering it with {' '}
anyday.
https://github.com/firefish5000/svelte-trim Can we adopt something like this in the compiler? A customizable solution, which will avoid generating AST twice. It will improve DX a lot.
Hello,
If svelte would – in the HTML part – remove <newline><all whitespace until next non whitespace>
but not the whitespace before <newline>
, then one could write ...
<span>
1
<span>2</span>
<span>3</span>
4
</span>
... and it would output 1234
(currently 1 2 3 4
).
If he would write ...
<span>1 <span>2</span> <span>3</span> 4</span>
... or (_
means whitespace) ...
<span>
1_
<span>2</span>_
<span>3</span>_
4
</span>
... he would get 1 2 3 4
.
Looks like the simplest solution to me.
This would be in conflict how the browser interprets newlines. To the browser a newline is a whitespace, too, and people knowing this would be very confused why in your first example there would be no more whitespaces between characters.
@dummdidumm Yes that is a good point, it should be an option which has to be enabled explicitly.
I actually tried this out in the parser, and it works[1]. But there are some problems.
It needs to be disabled on pre
elements, already mentioned by @eps1lon. This can be done automatically by parsing pre tags separately, like style or script or textarea tags. With trimming disabled. I have not done that yet.
elements which get 'whitespace:pre' or other values via CSS. I guess this can not be solved automatically? So the user needs to have a local way of manually disabling any global trimming.
So, globally a standard 'strip' option (I also like the export default ...
approach by @PaulBGD – or is that the way how options are set? Docs don't tell).
Locally I would like a syntax like:
{#strip nothing} ... {/strip}
{#strip default} ... {/strip} // the current approach, this is the default.
{#strip newlines} ... {/strip} // my approach
{#strip all} ... {/strip}
[1]
After const whitespace = /[ \t\r\n]/;
, add:
const newlines = /\r?\n[ \t\r\n]*/;
const newlines_everywhere = /\r?\n[ \t\r\n]*/g;
After the allow_whitespace()
function, add:
allow_newlines() {
const match = this.match_regex(newlines);
if (match)
this.index += match.length;
}
in the text()
function:
add data = data.replace(newlines_everywhere, '');
before const node ...
In the fragment()
function:
add parser.allow_newlines();
before if (parser.match('<')) {
This should be an option in the compiler.
RegExp should be avoided, whitespace should be deleted from the AST like it is done in svelte-trim
.
Making source code ugly is not an option.
Give something like:
<svelte:options whitespace="svelte|jsx|trim" />
compilerOptions: {
'whitespace': 'svelte|jsx|trim'
}
and everybody who had problems with whitespace in svelte will be happy.
Could it at least be possible to combine reduntant whitespace?
"In my opinion, the best\n animal is the squid." => "In my opinion, the best animal is the squid."
Is there any update on this? Seems like a discouraging problem to still have 5 years in as part of a component framework as svelte as Svelte is
It took me a little while to realize why some markup I was rebuilding from JSX to Svelte appeared to have mysterious extra spaces scattered all around. It was not fun.
After reading all the issues I could find in github around this, for the first time in my "svelte crush" I was left with a bitter taste: Svelte is all about "ergonomics" but in this particular case I feel the most sensible default surrounding whitespace is clashing with personal preferences of core contributors.
There are probably good reasons why JSX, Angular, Pug and friends have chosen to treat whitespace in a slightly different way than pure HTML, in the same way Svelte {#does} a lot of its awesome:stuff. So invoking HTML purism around this does not seem like a solid reason to ignore the issue. Moreover, even though this can be worked-around with a plugin/preprocessor, that really feels against Svelte's ethos (it makes me feel dirty).
Am I missing any good reasons not to add a compiler options to address this? I think it will prevent a lot of future pain in many non-pure and non-virgin frontend devs like me, that will be soon converting to the Svelte cult.
This is the current Svelte config I'm using to wipe any involountary whitespace from the HTML, thanks to shirotech.
import preprocess from 'svelte-preprocess';
const tagsRegex1 = /(>)[\s]*([<{])/g;
const tagsRegex2 = /({[/:][a-z]+})[\s]*([<{])/g;
const tagsRegex3 = /({[#:][a-z]+ .+?})[\s]*([<{])/g;
const tagsRegex4 = /([>}])[\s]+(<|{[/#:][a-z][^}]*})/g;
const tagsReplace = '$1$2';
const config = {
preprocess: preprocess({
replace: [
[tagsRegex1, tagsReplace],
[tagsRegex2, tagsReplace],
[tagsRegex3, tagsReplace],
[tagsRegex4, tagsReplace]
]
})
};
export default config;
Any news on this? Still experiencing problems.
This is the current Svelte config I'm using to wipe any involountary whitespace from the HTML, thanks to shirotech.
import preprocess from 'svelte-preprocess'; const tagsRegex1 = /(>)[\s]*([<{])/g; const tagsRegex2 = /({[/:][a-z]+})[\s]*([<{])/g; const tagsRegex3 = /({[#:][a-z]+ .+?})[\s]*([<{])/g; const tagsRegex4 = /([>}])[\s]+(<|{[/#:][a-z][^}]*})/g; const tagsReplace = '$1$2'; const config = { preprocess: preprocess({ replace: [ [tagsRegex1, tagsReplace], [tagsRegex2, tagsReplace], [tagsRegex3, tagsReplace], [tagsRegex4, tagsReplace] ] }) }; export default config;
Thanks for using my code and putting it in a more friendly config, I've been relying on this but came at a great cost for large files, and if you have more than hundreds of svelte components it's even worse. Ran a benchmark https://www.measurethat.net/Benchmarks/Show/22894/0/replacer-string-vs-function-2 there is a way to optimise it even further if anyone is interested.
Turning replacer string
to a function
seems to be a bit faster, every ms counts when we're at this bottleneck.
const tagsReplace = (_: string, p1: string, p2: string): string => p1 + p2;
My alternative to avoid line breaks:
preprocess: sveltePreprocess({
replace: [
// https://github.com/sveltejs/svelte/issues/189
[/\s+<!--nobr-->\s+/gm, ""],
],
}),
Used like this:
({#each nn(word.origin) as character}
<!--nobr-->
{#if character !== tokenText}
<!--nobr-->
<Token text={character} wordIds={[]} bind:selection={outputSelection} />
<!--nobr-->
{:else}
<!--nobr-->
<b>{character}</b>
<!--nobr-->
{/if}
<!--nobr-->
{/each})
It's annoyingly verbose but at least its behavior is reasonably constrained and explicit.
Although like everyone here, I would love to avoid having such a hack in my codebase. IMO this issue should at least be mentioned in the FAQ.
I like how the Fresh Framework does it:
<h1> Welcome
to Fresh </h1>
<p>
<em>foo</em>
<em>bar</em>{" "}
<em>baz</em> <em>quux</em>
</p>
<pre>{
`foo
bar
baz`
}</pre>
... becomes this:
<h1> Welcome to Fresh </h1><p><em>foo</em><em>bar</em> <em>baz</em> <em>quux</em></p><pre>foo
bar
baz</pre>
The Deno formatter in VSCode additionally helps by removing / compacting whitespace and inserting / removing {" "}
where necessary / possible. For example, it changes the code at the top to:
<h1>
Welcome to Fresh
</h1>
<p>
<em>foo</em>
<em>bar</em> <em>baz</em> <em>quux</em>
</p>
<pre>{
`foo
bar
baz`
}</pre>
... which becomes:
<h1>Welcome to Fresh</h1><p><em>foo</em><em>bar</em> <em>baz</em> <em>quux</em></p><pre>foo
bar
baz</pre>
I'm experiencing this too I believe. Correct me if I'm wrong and this is a separate issue.
{#each droppedOut as out, i}
{#if out.team.links.length > 0}
<a href={out.team.links[0]?.href} target="_blank" rel="noreferrer">
{out.team.nickname}
</a>
{:else}
{out.team.nickname}
{/if}
{#if i < droppedOut.length - 1}
,
{/if}
{/each}
There's a space before the comma.
If I do
{#if out.team.links.length > 0}<a href={out.team.links[0]?.href} target="_blank" rel="noreferrer">{out.team.nickname}</a>{:else}{out.team.nickname}{/if}{#if i < droppedOut.length - 1}, {/if}
Then it goes away:
In fact, the problem is present here:
{#if out.team.links.length > 0}<a href={out.team.links[0]?.href} target="_blank" rel="noreferrer">{out.team.nickname}</a>{:else}{out.team.nickname}{/if}
{#if i < droppedOut.length - 1}, {/if}
But not here:
{#if out.team.links.length > 0}<a href={out.team.links[0]?.href} target="_blank" rel="noreferrer">{out.team.nickname}</a>{:else}{out.team.nickname}{/if}{#if i < droppedOut.length - 1}, {/if}
Svelte 5 will change its whitespace handling. It's impossible to correctly mirror what the browser does without preserving all whitespace as is, so the whitespace handling was simplified, which makes it easier both for the human and the compiler to understand: https://svelte-5-preview.vercel.app/docs/breaking-changes#whitespace-handling-changed
Can "Whitespace at the start and end of a tag is removed completely" be clarified? Or is there a reference somewhere?
It isn't clear to me if it is talking about all around the start and end of a tag, or just inside of the tag. For example:
<foo>
<bar></bar>
<bar> </bar>
</foo>
Is this equivalent to A, where all whitespace around tags is removed.
<foo><bar></bar><bar></bar></foo>
Or B where only the whitespace inside of start and end tags are removed.
<foo><bar></bar> <bar></bar></foo>
Thank for the reply @dummdidumm ! I understand the problem now. Not sure about the solution other than using prettier-ignore
and dealing with a long line or two.
In my specific use case, I'm just using a pipe |
separator anyway now 😅
But it's good knowledge to have for the future when I come across this again! Thanks again 😄
@kevincox it would be B. Whitespace between tag siblings is trimmed to one whitespace.
@dummdidumm there: It is not practical to always collapse whitespace between nodes to one white space. This will not resolve the main problem. If you do it this way, you can as well do nothing, because the browser anyway takes care of this type of whitespace folding.
The relevant question is how to handle whitespace between elements. Consider my post above, how Fresh does it:
<foo/>
{#if hi}
<bar/>
Hello
<baz/>
{/if}
Hello
<quux/>
becomes
<foo/>{#if hi}<bar/>Hello<baz/>{/if}Hello<quux/>
<foo/> {#if hi} <bar/> Hello <baz/> {/if} Hello <quux/>
becomes
<foo/> {#if hi}<bar/> Hello <baz/>{/if} Hello <quux/>
If the user wants to insert explicit whitespace, he uses {" "}
. That method is also used inside pre
s, no special handling for them.
{...}
blocks behave like element tags
White space between text and elements is handled the same way. Remove all of it when a newline is contained, otherwise collapse it to one whitespace.
To be honest, everyone makes up their own set of slightly different arbitrary rules. We picked this specific ruleset to be as much backwards-compatible as possible with Svelte 4 while making things easier to reason about. There's no correct answer here.
@dummdidumm Your solution is just bad. The user will not be able to format his code intuitively without having unwanted whitespaces.
<ul>
<li>Homepage</li>
<li>Docs</li>
<li>Blog</li>
</ul>
Will not become
<ul><li>Homepage</li><li>Docs</li><li>Blog</li></ul>
as it should. It will become
<ul><li>Homepage</li> <li>Docs</li> <li>Blog</li></ul>
And every CSS designer will curse.
The implementation of my solution is not difficult, you search for [^\S\n]*(\n)?\s*
and if you have a match for (\n)
you replace with the empty string, otherwise, if you matched anything, with one white space. (Edit: I changed the regex, so, admittedly, it is not super trivial)
While fixing #178 it occurred to me that there are some nodes, like
<datalist>
, where it doesn't make sense to have text node children. The browser agrees:Not sure what the first text node is doing there in the second case. Anyway, Svelte should be smart enough not to create text nodes inside elements where they're meaningless.
Additionally, we could collapse excess whitespace between nodes that aren't inside a
<pre>
element, since these are equivalent:(That's not strictly true, since it's dependent on CSS, so there probably needs to be a
preserveWhitespace: true
option if we did that.)