blockless
Rich Harris 2 years ago
commit 850dcbe417

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve namespace inference when having `{@render}` and `{@html}` tags

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

@ -97,6 +97,7 @@
"gentle-sheep-hug", "gentle-sheep-hug",
"gentle-spies-happen", "gentle-spies-happen",
"giant-moons-own", "giant-moons-own",
"giant-planets-shake",
"giant-roses-press", "giant-roses-press",
"good-buses-reply", "good-buses-reply",
"good-cars-visit", "good-cars-visit",
@ -108,6 +109,7 @@
"green-eggs-approve", "green-eggs-approve",
"green-hounds-play", "green-hounds-play",
"green-tigers-judge", "green-tigers-judge",
"happy-beds-scream",
"happy-suits-film", "happy-suits-film",
"healthy-planes-vanish", "healthy-planes-vanish",
"heavy-comics-move", "heavy-comics-move",

@ -1,5 +1,13 @@
# svelte # svelte
## 5.0.0-next.71
### Patch Changes
- fix: improve namespace inference when having `{@render}` and `{@html}` tags ([#10631](https://github.com/sveltejs/svelte/pull/10631))
- fix: don't collapse whitespace within text nodes ([#10691](https://github.com/sveltejs/svelte/pull/10691))
## 5.0.0-next.70 ## 5.0.0-next.70
### Patch Changes ### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.0.0-next.70", "version": "5.0.0-next.71",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -5,6 +5,7 @@ import {
regex_whitespaces_strict regex_whitespaces_strict
} from '../patterns.js'; } from '../patterns.js';
import * as b from '../../utils/builders.js'; import * as b from '../../utils/builders.js';
import { walk } from 'zimmerframe';
/** /**
* @param {string} s * @param {string} s
@ -162,24 +163,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);
} }
} }
@ -237,52 +250,50 @@ export function infer_namespace(namespace, parent, nodes, path) {
* @param {import('#compiler').Namespace | 'keep' | 'maybe_html'} namespace * @param {import('#compiler').Namespace | 'keep' | 'maybe_html'} namespace
*/ */
function check_nodes_for_namespace(nodes, namespace) { function check_nodes_for_namespace(nodes, namespace) {
for (const node of nodes) { /**
if (node.type === 'RegularElement' || node.type === 'SvelteElement') { * @param {import('#compiler').SvelteElement | import('#compiler').RegularElement} node}
* @param {{stop: () => void}} context
*/
const RegularElement = (node, { stop }) => {
if (!node.metadata.svg) { if (!node.metadata.svg) {
namespace = 'html'; namespace = 'html';
break; stop();
} else if (namespace === 'keep') { } else if (namespace === 'keep') {
namespace = 'svg'; namespace = 'svg';
} }
} else if ( };
(node.type === 'Text' && node.data.trim() !== '') ||
node.type === 'HtmlTag' || for (const node of nodes) {
node.type === 'RenderTag' walk(
node,
{},
{
_(node, { next }) {
if (
node.type === 'EachBlock' ||
node.type === 'IfBlock' ||
node.type === 'AwaitBlock' ||
node.type === 'Fragment' ||
node.type === 'KeyBlock' ||
node.type === 'RegularElement' ||
node.type === 'SvelteElement' ||
node.type === 'Text'
) { ) {
next();
}
},
SvelteElement: RegularElement,
RegularElement,
Text(node) {
if (node.data.trim() !== '') {
namespace = 'maybe_html'; namespace = 'maybe_html';
} else if (node.type === 'EachBlock') {
namespace = check_nodes_for_namespace(node.body.nodes, namespace);
if (namespace === 'html') break;
if (node.fallback) {
namespace = check_nodes_for_namespace(node.fallback.nodes, namespace);
if (namespace === 'html') break;
}
} else if (node.type === 'IfBlock') {
namespace = check_nodes_for_namespace(node.consequent.nodes, namespace);
if (namespace === 'html') break;
if (node.alternate) {
namespace = check_nodes_for_namespace(node.alternate.nodes, namespace);
if (namespace === 'html') break;
}
} else if (node.type === 'AwaitBlock') {
if (node.pending) {
namespace = check_nodes_for_namespace(node.pending.nodes, namespace);
if (namespace === 'html') break;
}
if (node.then) {
namespace = check_nodes_for_namespace(node.then.nodes, namespace);
if (namespace === 'html') break;
}
if (node.catch) {
namespace = check_nodes_for_namespace(node.catch.nodes, namespace);
if (namespace === 'html') break;
}
} else if (node.type === 'KeyBlock') {
namespace = check_nodes_for_namespace(node.fragment.nodes, namespace);
if (namespace === 'html') break;
} }
} }
}
);
if (namespace === 'html') return namespace;
}
return namespace; return namespace;
} }

@ -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;` */

@ -123,7 +123,8 @@ export function each_keyed(anchor_node, collection, flags, key_fn, render_fn, fa
// Get the <!--ssr:..--> tag of the next item in the list // Get the <!--ssr:..--> tag of the next item in the list
// The fragment array can be empty if each block has no content // The fragment array can be empty if each block has no content
hydrating_node = /** @type {import('#client').TemplateNode} */ ( hydrating_node = /** @type {import('#client').TemplateNode} */ (
/** @type {Node} */ ((fragment[fragment.length - 1] || hydrating_node).nextSibling).nextSibling /** @type {Node} */ ((fragment[fragment.length - 1] || hydrating_node).nextSibling)
.nextSibling
); );
} }

@ -26,8 +26,8 @@ import {
BRANCH_EFFECT BRANCH_EFFECT
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js'; import { flush_tasks } from './dom/task.js';
import { mutate } from './reactivity/sources.js';
import { add_owner } from './dev/ownership.js'; import { add_owner } from './dev/ownership.js';
import { mutate } from './reactivity/sources.js';
const IS_EFFECT = EFFECT | PRE_EFFECT | RENDER_EFFECT; const IS_EFFECT = EFFECT | PRE_EFFECT | RENDER_EFFECT;

@ -28,11 +28,11 @@ export { snippet_effect } from './client/dom/blocks/render-tag.js';
export { component } from './client/dom/blocks/svelte-component.js'; export { component } from './client/dom/blocks/svelte-component.js';
export { element } from './client/dom/blocks/svelte-element.js'; export { element } from './client/dom/blocks/svelte-element.js';
export * from './client/dom/blocks/each.js'; export * from './client/dom/blocks/each.js';
export * from './client/reactivity/effects.js';
export * from './client/reactivity/deriveds.js'; export * from './client/reactivity/deriveds.js';
export * from './client/reactivity/effects.js';
export * from './client/reactivity/sources.js';
export * from './client/reactivity/equality.js'; export * from './client/reactivity/equality.js';
export * from './client/reactivity/props.js'; export * from './client/reactivity/props.js';
export * from './client/reactivity/sources.js';
export * from './client/reactivity/store.js'; export * from './client/reactivity/store.js';
export * from './client/render.js'; export * from './client/render.js';
export * from './client/validate.js'; export * from './client/validate.js';

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version * https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string} * @type {string}
*/ */
export const VERSION = '5.0.0-next.70'; export const VERSION = '5.0.0-next.71';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -1,23 +0,0 @@
import { test } from '../../assert';
const tick = () => Promise.resolve();
export default test({
dev: true,
skip: true, // TODO: needs dev time warning
async test({ assert, target }) {
/** @type {string[]} */
const warnings = [];
const warn = console.warn;
console.warn = (warning) => {
warnings.push(warning);
};
target.innerHTML = '<my-app foo=yes />';
await tick();
assert.deepEqual(warnings, ["<my-app> was created without expected prop 'bar'"]);
console.warn = warn;
}
});

@ -1,9 +0,0 @@
<svelte:options customElement="my-app" />
<script>
export let foo;
export let bar;
</script>
<p>foo: {foo}</p>
<p>bar: {bar}</p>

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

@ -2,12 +2,18 @@
{#if true} {#if true}
<text x="0" y="26">true</text> <text x="0" y="26">true</text>
{:else}
<text x="0" y="26">false</text>
{/if} {/if}
{#each Array(3).fill(0) as item, idx} {#each Array(2).fill(0) as item, idx}
<text x={idx * 10} y={42}>{idx}</text> <text x={idx * 10} y={42}>{idx}</text>
{/each} {/each}
<!-- comment should not set infer html namespace --> {@html '<text x="0" y="40">html</text>'}
{@render test("snippet")}
{#snippet test(text)}
<text x={20} y={42}>{text}</text>
{/snippet}
<!-- comment should not infer html namespace -->

@ -7,7 +7,8 @@ export default test({
<text x="0" y="26">true</text> <text x="0" y="26">true</text>
<text x="0" y="42">0</text> <text x="0" y="42">0</text>
<text x="10" y="42">1</text> <text x="10" y="42">1</text>
<text x="20" y="42">2</text> <text x="0" y="40">html</text>
<text x="20" y="42">snippet</text>
</svg> </svg>
`, `,
test({ assert, target }) { test({ assert, target }) {
@ -18,7 +19,7 @@ export default test({
const text_elements = target.querySelectorAll('text'); const text_elements = target.querySelectorAll('text');
assert.equal(text_elements.length, 5); assert.equal(text_elements.length, 6);
for (const { namespaceURI } of text_elements) for (const { namespaceURI } of text_elements)
assert.equal(namespaceURI, 'http://www.w3.org/2000/svg'); assert.equal(namespaceURI, 'http://www.w3.org/2000/svg');

Loading…
Cancel
Save