sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.19k stars 4.09k forks source link

Better whitespace handling #189

Closed Rich-Harris closed 5 months ago

Rich-Harris commented 7 years ago

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:

document.body.innerHTML = `
  <div>
    <span>a</span>
    <span>b</span>
  </div>
`;

console.log( document.body.querySelector( 'div' ).childNodes );
// [ text, span, text, span, text ]

document.body.innerHTML = `
  <datalist>
    <option value='a'/>
    <option value='b'/>
  </datalist>
`;

console.log( document.body.querySelector( 'datalist' ).childNodes );
// [ text, option, option ]

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:

<p>one</p> <p>two</p>
<p>  one  </p>

    <p>  two  </p>

(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.)

Swatinem commented 7 years 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>
Rich-Harris commented 7 years ago

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.

Swatinem commented 7 years ago

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.

Rich-Harris commented 7 years ago

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();
Swatinem commented 7 years ago

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.

Ryuno-Ki commented 7 years ago

@Rich-Harris wrote;

Not sure what the first text node is doing there in the second case.

I'm quite sure it is a line break.

Django and Twig have a spaceless environment tag.

Conduitry commented 7 years ago

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.

camwest commented 7 years ago

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

cristinecula commented 7 years ago

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> #}
PaulBGD commented 7 years ago

Could we do something like

export default {
  keepWhitespace: true
}

then check that at compile time?

mrkishi commented 6 years ago

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?

Conduitry commented 6 years ago

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.

adamhooper commented 6 years ago

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?

Rich-Harris commented 6 years ago

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.

morleytatro commented 6 years ago

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>
lydell commented 5 years ago

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 ¯\_(ツ)_/¯

tomblachut commented 5 years ago

Angular has similar feature of removing whitespaces. https://angular.io/api/core/Component#preserving-whitespace

Key takeaways:

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.

steve-taylor commented 5 years ago

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.

kylecordes commented 5 years ago

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.

frederikhors commented 5 years ago

I saw this but I'm asking if this is the same for my problem: https://github.com/sveltejs/svelte/issues/2745

frederikhors commented 5 years ago

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.

lydell commented 5 years ago

@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.

kmmbvnr commented 4 years ago

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(),
  ],
};
shirotech commented 4 years ago

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.

https://github.com/WordPress/WordPress/blob/19fd7a0da93bcbab94396fb7bc2f26e56a4c1668/wp-includes/js/wp-emoji.js#L141

shirotech commented 4 years ago

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

eps1lon commented 4 years ago

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">&#39;Create tarball&#39;</span>

The result changes between hydration. Before: Screenshot from 2020-05-16 22-07-39 After: Screenshot from 2020-05-16 22-08-26

nolanlawson commented 4 years ago

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.

non25 commented 4 years ago

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.

non25 commented 3 years ago

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.

nilslindemann commented 3 years ago

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.

dummdidumm commented 3 years ago

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.

nilslindemann commented 3 years ago

@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.

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('<')) {

non25 commented 3 years ago

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.

SvizelPritula commented 3 years ago

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."
eden-omb commented 2 years ago

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

jsilveira commented 2 years ago

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.

smastrom-dolly commented 2 years ago

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;
elron commented 1 year ago

Any news on this? Still experiencing problems.

shirotech commented 1 year ago

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;
71 commented 11 months ago

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.

nilslindemann commented 11 months ago

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>
justingolden21 commented 10 months ago

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}
                , &nbsp;
            {/if}
        {/each}

image

image

image

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}, &nbsp;{/if}

Then it goes away:

image

image

image

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}, &nbsp;{/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}, &nbsp;{/if}
dummdidumm commented 10 months ago

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

kevincox commented 10 months ago

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>
justingolden21 commented 10 months ago

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 😄

dummdidumm commented 10 months ago

@kevincox it would be B. Whitespace between tag siblings is trimmed to one whitespace.

nilslindemann commented 10 months ago

@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:

  1. If a newline is contained, then all white spaces are removed
  2. otherwise, the white spaces are collapsed to one whitespace
  3. white space after opening tag and before closing tag is all removed (which you intend to do)

Example for 1

<foo/>

{#if hi}

<bar/>

Hello

<baz/>

{/if}

Hello

<quux/>

becomes

<foo/>{#if hi}<bar/>Hello<baz/>{/if}Hello<quux/>

Example for 2

<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 pres, 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.

dummdidumm commented 9 months ago

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.

nilslindemann commented 9 months ago

@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.

nilslindemann commented 9 months ago

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)