improve whitespace handling

pull/7426/head
tanhauhau 4 years ago
parent ae898c20a4
commit 64b2e16fbc

@ -18,6 +18,7 @@ const elements_without_text = new Set([
]); ]);
const regex_ends_with_svg = /svg$/; const regex_ends_with_svg = /svg$/;
const regex_non_whitespace_characters = /[\S\u00A0]/;
export default class Text extends Node { export default class Text extends Node {
type: 'Text'; type: 'Text';
@ -63,4 +64,11 @@ export default class Text extends Node {
return false; return false;
} }
use_space(): boolean {
if (this.component.compile_options.preserveWhitespace) return false;
if (regex_non_whitespace_characters.test(this.data)) return false;
return !this.within_pre();
}
} }

@ -1218,7 +1218,11 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapp
// Don't add the <pre>/<textarea> newline logic here because pre/textarea.innerHTML // Don't add the <pre>/<textarea> newline logic here because pre/textarea.innerHTML
// would keep the leading newline, too, only someParent.innerHTML = '..<pre/textarea>..' won't // would keep the leading newline, too, only someParent.innerHTML = '..<pre/textarea>..' won't
if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' '; if (wrapper.use_space()) {
// use space instead of the text content
state.quasi.value.raw += ' ';
return;
}
const parent = wrapper.node.parent as Element; const parent = wrapper.node.parent as Element;

@ -5,11 +5,9 @@ import Wrapper from './shared/Wrapper';
import { x } from 'code-red'; import { x } from 'code-red';
import { Identifier } from 'estree'; import { Identifier } from 'estree';
const regex_non_whitespace_characters = /[\S\u00A0]/;
export default class TextWrapper extends Wrapper { export default class TextWrapper extends Wrapper {
node: Text; node: Text;
data: string; _data: string;
skip: boolean; skip: boolean;
var: Identifier; var: Identifier;
@ -23,15 +21,22 @@ export default class TextWrapper extends Wrapper {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.skip = this.node.should_skip(); this.skip = this.node.should_skip();
this.data = data; this._data = data;
this.var = (this.skip ? null : x`t`) as unknown as Identifier; this.var = (this.skip ? null : x`t`) as unknown as Identifier;
} }
use_space() { use_space() {
if (this.renderer.component.component_options.preserveWhitespace) return false; return this.node.use_space();
if (regex_non_whitespace_characters.test(this.data)) return false; }
return !this.node.within_pre(); set data(value: string) {
// when updating `this.data` during optimisation
// propagate the changes over to the underlying node
// so that the node.use_space reflects on the latest `data` value
this.node.data = this._data = value;
}
get data() {
return this._data;
} }
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {

@ -5,7 +5,9 @@ import Element from '../../nodes/Element';
export default function(node: Text, renderer: Renderer, _options: RenderOptions) { export default function(node: Text, renderer: Renderer, _options: RenderOptions) {
let text = node.data; let text = node.data;
if ( if (node.use_space()) {
text = ' ';
} else if (
!node.parent || !node.parent ||
node.parent.type !== 'Element' || node.parent.type !== 'Element' ||
((node.parent as Element).name !== 'script' && (node.parent as Element).name !== 'style') ((node.parent as Element).name !== 'script' && (node.parent as Element).name !== 'style')

@ -4,7 +4,7 @@ import glob from 'tiny-glob/sync';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import * as colors from 'kleur'; import * as colors from 'kleur';
export const assert = (assert$1 as unknown) as typeof assert$1 & { htmlEqual: (actual, expected, message?) => void, htmlEqualWithComments: (actual, expected, message?) => void }; export const assert = (assert$1 as unknown) as typeof assert$1 & { htmlEqual: (actual, expected, message?) => void, htmlEqualWithOptions: (actual, expected, options, message?) => void };
// for coverage purposes, we need to test source files, // for coverage purposes, we need to test source files,
// but for sanity purposes, we need to test dist files // but for sanity purposes, we need to test dist files
@ -140,7 +140,7 @@ function cleanChildren(node) {
} }
} }
export function normalizeHtml(window, html, { removeDataSvelte = false, preserveComments = false }: { removeDataSvelte?: boolean, preserveComments?: boolean} = {}) { export function normalizeHtml(window, html, { removeDataSvelte = false, preserveComments = false }: { removeDataSvelte?: boolean, preserveComments?: boolean }) {
try { try {
const node = window.document.createElement('div'); const node = window.document.createElement('div');
node.innerHTML = html node.innerHTML = html
@ -155,7 +155,12 @@ export function normalizeHtml(window, html, { removeDataSvelte = false, preserve
} }
} }
export function setupHtmlEqual(options?: { removeDataSvelte?: boolean }) { export function normalizeNewline(html: string) {
// return html.trim().replace(/\r\n/g, '\n')
return html.replace(/\r\n/g, '\n');
}
export function setupHtmlEqual(options: { removeDataSvelte?: boolean } = {}) {
const window = env(); const window = env();
// eslint-disable-next-line no-import-assign // eslint-disable-next-line no-import-assign
@ -167,10 +172,14 @@ export function setupHtmlEqual(options?: { removeDataSvelte?: boolean }) {
); );
}; };
// eslint-disable-next-line no-import-assign // eslint-disable-next-line no-import-assign
assert.htmlEqualWithComments = (actual, expected, message) => { assert.htmlEqualWithOptions = (actual, expected, { preserveComments, withoutNormalizeHtml }, message) => {
assert.deepEqual( assert.deepEqual(
normalizeHtml(window, actual, { ...options, preserveComments: true }), withoutNormalizeHtml
normalizeHtml(window, expected, { ...options, preserveComments: true }), ? normalizeNewline(actual).replace(/(\sdata-svelte="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
: normalizeHtml(window, actual, { ...options, preserveComments }),
withoutNormalizeHtml
? normalizeNewline(expected).replace(/(\sdata-svelte="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
: normalizeHtml(window, expected, { ...options, preserveComments }),
message message
); );
}; };

@ -8,11 +8,8 @@ const Component = create_ssr_component(($$result, $$props, $$bindings, slots) =>
if ($$props.foo === void 0 && $$bindings.foo && foo !== void 0) $$bindings.foo(foo); if ($$props.foo === void 0 && $$bindings.foo && foo !== void 0) $$bindings.foo(foo);
return `${each(things, thing => { return `${each(things, thing => {
return `<span>${escape(thing.name)}</span> return `<span>${escape(thing.name)}</span> ${debug(null, 7, 2, { foo })}`;
${debug(null, 7, 2, { foo })}`; })} <p>foo: ${escape(foo)}</p>`;
})}
<p>foo: ${escape(foo)}</p>`;
}); });
export default Component; export default Component;

@ -2,9 +2,7 @@
import { create_ssr_component } from "svelte/internal"; import { create_ssr_component } from "svelte/internal";
const Component = create_ssr_component(($$result, $$props, $$bindings, slots) => { const Component = create_ssr_component(($$result, $$props, $$bindings, slots) => {
return `<div>content</div> return `<div>content</div> <!-- comment --> <div>more content</div>`;
<!-- comment -->
<div>more content</div>`;
}); });
export default Component; export default Component;

@ -201,7 +201,9 @@ describe('runtime', () => {
} }
if (config.html) { if (config.html) {
assert.htmlEqual(target.innerHTML, config.html); assert.htmlEqualWithOptions(target.innerHTML, config.html, {
withoutNormalizeHtml: config.withoutNormalizeHtml
});
} }
if (config.test) { if (config.test) {

@ -1,19 +1,14 @@
export default { export default {
test({ assert, target }) { withoutNormalizeHtml: true,
// Test for <pre> tag html: get_html(false),
const elementPre = target.querySelector('#pre'); ssrHtml: get_html(true)
// Test for non <pre> tag };
const elementDiv = target.querySelector('#div');
// Test for <pre> tag in non <pre> tag
const elementDivWithPre = target.querySelector('#div-with-pre');
// Test for <pre> tag with leading newline
const elementPreWithLeadingNewline = target.querySelector('#pre-with-leading-newline');
const elementPreWithoutLeadingNewline = target.querySelector('#pre-without-leading-newline');
const elementPreWithMultipleLeadingNewline = target.querySelector('#pre-with-multiple-leading-newlines');
assert.equal( function get_html(ssr) {
elementPre.innerHTML, // ssr rendered HTML has an extra newline prefixed within `<pre>` tag,
` A // if the <pre> tag starts with `\n`
// because when browser parses the SSR rendered HTML, it will ignore the 1st '\n' character
return `<pre id="pre"> A
B B
<span> <span>
C C
@ -21,20 +16,12 @@ export default {
</span> </span>
E E
F F
` </pre> <div id="div">A
);
assert.equal(
elementDiv.innerHTML,
`A
B B
<span>C <span>C
D</span> D</span>
E E
F` F</div> <div id="div-with-pre"><pre> A
);
assert.equal(
elementDivWithPre.innerHTML,
`<pre> A
B B
<span> <span>
C C
@ -42,14 +29,9 @@ export default {
</span> </span>
E E
F F
</pre>` </pre></div> <div id="pre-with-leading-newline"><pre>leading newline</pre> <pre> leading newline and spaces</pre> <pre>${ssr ? '\n' : ''}
); leading newlines</pre></div> <div id="pre-without-leading-newline"><pre>without spaces</pre> <pre> with spaces </pre> <pre>
assert.equal(elementPreWithLeadingNewline.children[0].innerHTML, 'leading newline'); newline after leading space</pre></div> <pre id="pre-with-multiple-leading-newlines">${ssr ? '\n' : ''}
assert.equal(elementPreWithLeadingNewline.children[1].innerHTML, ' leading newline and spaces');
assert.equal(elementPreWithLeadingNewline.children[2].innerHTML, '\nleading newlines'); multiple leading newlines</pre>`;
assert.equal(elementPreWithoutLeadingNewline.children[0].innerHTML, 'without spaces');
assert.equal(elementPreWithoutLeadingNewline.children[1].innerHTML, ' with spaces ');
assert.equal(elementPreWithoutLeadingNewline.children[2].innerHTML, ' \nnewline after leading space');
assert.equal(elementPreWithMultipleLeadingNewline.innerHTML, '\n\nmultiple leading newlines');
} }
};

@ -2,17 +2,8 @@ export default {
compileOptions: { compileOptions: {
preserveWhitespace: true preserveWhitespace: true
}, },
test({ assert, target }) {
// Test for <pre> tag
const elementPre = target.querySelector('#pre');
// Test for non <pre> tag
const elementDiv = target.querySelector('#div');
// Test for <pre> tag in non <pre> tag
const elementDivWithPre = target.querySelector('#div-with-pre');
assert.equal( html: `<pre id="pre"> A
elementPre.innerHTML,
` A
B B
<span> <span>
C C
@ -20,11 +11,9 @@ export default {
</span> </span>
E E
F F
` </pre>
);
assert.equal( <div id="div">
elementDiv.innerHTML,
`
A A
B B
<span> <span>
@ -33,11 +22,9 @@ export default {
</span> </span>
E E
F F
` </div>
);
assert.equal( <div id="div-with-pre">
elementDivWithPre.innerHTML,
`
<pre> A <pre> A
B B
<span> <span>
@ -47,7 +34,5 @@ export default {
E E
F F
</pre> </pre>
` </div>`
);
}
}; };

@ -1,4 +1,21 @@
export default { export default {
withoutNormalizeHtml: true,
// Unable to test `html` with `<textarea>` content
// as the textarea#value will not show within `innerHtml`
ssrHtml: `<textarea id="textarea"> A
B
</textarea> <div id="div-with-textarea"><textarea> A
B
</textarea></div> <div id="textarea-with-leading-newline"><textarea>leading newline</textarea> <textarea> leading newline and spaces</textarea> <textarea>
leading newlines</textarea></div> <div id="textarea-without-leading-newline"><textarea>without spaces</textarea> <textarea> with spaces </textarea> <textarea>
newline after leading space</textarea></div> <textarea id="textarea-with-multiple-leading-newlines">
multiple leading newlines</textarea> <div id="div-with-textarea-with-multiple-leading-newlines"><textarea>
multiple leading newlines</textarea></div>`,
test({ assert, target }) { test({ assert, target }) {
// Test for <textarea> tag // Test for <textarea> tag
const elementTextarea = target.querySelector('#textarea'); const elementTextarea = target.querySelector('#textarea');

@ -83,13 +83,7 @@ describe('ssr', () => {
if (css.code) fs.writeFileSync(`${dir}/_actual.css`, css.code); if (css.code) fs.writeFileSync(`${dir}/_actual.css`, css.code);
try { try {
if (config.withoutNormalizeHtml) { assert.htmlEqualWithOptions(html, expectedHtml, { preserveComments: compileOptions.preserveComments, withoutNormalizeHtml: config.withoutNormalizeHtml });
assert.strictEqual(html.trim().replace(/\r\n/g, '\n'), expectedHtml.trim().replace(/\r\n/g, '\n'));
} else {
(compileOptions.preserveComments
? assert.htmlEqualWithComments
: assert.htmlEqual)(html, expectedHtml);
}
} catch (error) { } catch (error) {
if (shouldUpdateExpected()) { if (shouldUpdateExpected()) {
fs.writeFileSync(`${dir}/_expected.html`, html); fs.writeFileSync(`${dir}/_expected.html`, html);
@ -214,9 +208,15 @@ describe('ssr', () => {
}); });
if (config.ssrHtml) { if (config.ssrHtml) {
assert.htmlEqual(html, config.ssrHtml); assert.htmlEqualWithOptions(html, config.ssrHtml, {
preserveComments: compileOptions.preserveComments,
withoutNormalizeHtml: config.withoutNormalizeHtml
});
} else if (config.html) { } else if (config.html) {
assert.htmlEqual(html, config.html); assert.htmlEqualWithOptions(html, config.html, {
preserveComments: compileOptions.preserveComments,
withoutNormalizeHtml: config.withoutNormalizeHtml
});
} }
if (config.test_ssr) { if (config.test_ssr) {

@ -1,2 +0,0 @@
[{main.svelte,_expected.html}]
trim_trailing_whitespace = unset

@ -1,3 +0,0 @@
export default {
withoutNormalizeHtml: true
};

@ -1,37 +0,0 @@
<pre> A
B
<span>
C
D
</span>
E
F
</pre>
<div>A
B
<span>C
D</span>
E
F</div>
<div><pre> A
B
<span>
C
D
</span>
E
F
</pre></div>
<div id="pre-with-leading-newline"><pre>leading newline</pre>
<pre> leading newline and spaces</pre>
<pre>
leading newlines</pre></div>
<div id="pre-without-leading-newline"><pre>without spaces</pre>
<pre> with spaces </pre>
<pre>
newline after leading space</pre></div>

@ -1,51 +0,0 @@
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
<div>
A
B
<span>
C
D
</span>
E
F
</div>
<div>
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
</div>
<div id="pre-with-leading-newline">
<pre>
leading newline</pre>
<pre>
leading newline and spaces</pre>
<pre>
leading newlines</pre>
</div>
<div id="pre-without-leading-newline">
<pre>without spaces</pre>
<pre> with spaces </pre>
<pre>
newline after leading space</pre>
</div>

@ -1,6 +0,0 @@
export default {
withoutNormalizeHtml: true,
compileOptions: {
preserveWhitespace: true
}
};

@ -1,32 +0,0 @@
<pre> A
B
<span>
C
D
</span>
E
F
</pre>
<div>
A
B
<span>
C
D
</span>
E
F
</div>
<div>
<pre> A
B
<span>
C
D
</span>
E
F
</pre>
</div>

@ -1,34 +0,0 @@
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
<div>
A
B
<span>
C
D
</span>
E
F
</div>
<div>
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
</div>

@ -1,2 +0,0 @@
[{main.svelte,_expected.html}]
trim_trailing_whitespace = unset

@ -1,3 +0,0 @@
export default {
withoutNormalizeHtml: true
};

@ -1,28 +0,0 @@
<textarea id="textarea"> A
B
</textarea>
<div id="div-with-textarea"><textarea> A
B
</textarea></div>
<div id="textarea-with-leading-newline"><textarea>leading newline</textarea>
<textarea> leading newline and spaces</textarea>
<textarea>
leading newlines</textarea></div>
<div id="textarea-without-leading-newline"><textarea>without spaces</textarea>
<textarea> with spaces </textarea>
<textarea>
newline after leading space</textarea></div>
<textarea id="textarea-with-multiple-leading-newlines">
multiple leading newlines</textarea>
<div id="div-with-textarea-with-multiple-leading-newlines"><textarea>
multiple leading newlines</textarea></div>

@ -1,40 +0,0 @@
<textarea id="textarea">
A
B
</textarea>
<div id="div-with-textarea">
<textarea>
A
B
</textarea>
</div>
<div id="textarea-with-leading-newline">
<textarea>
leading newline</textarea>
<textarea>
leading newline and spaces</textarea>
<textarea>
leading newlines</textarea>
</div>
<div id="textarea-without-leading-newline">
<textarea>without spaces</textarea>
<textarea> with spaces </textarea>
<textarea>
newline after leading space</textarea>
</div>
<textarea id="textarea-with-multiple-leading-newlines">
multiple leading newlines</textarea>
<div id="div-with-textarea-with-multiple-leading-newlines">
<textarea>
multiple leading newlines</textarea>
</div>
Loading…
Cancel
Save