fix: fall back to component namespace when not statically determinable

In #10006 we added more elaborate mechanisms to determine which namespace a given element is in. For `<svelte:element>` we added a "can't know at compile time" case and introduced a limited heuristic into the runtime.
This doesn't work for a few reasons:
- we're checking the parent's namespace to determine the current namespace, but the element itself could be the one that _changes_ the namespace
- as mentioned in the previous comment already, on the first render we can't do any parent analysis
- it does not take into account the static component namespace

The last point is the crucial one: In Svelte 4, we're falling back to the component namespace if we can't know statically - e.g. if someone added `<svelte:options namespace="svg">` then `<svelte:element>` should fall back to that namespace instead.
We were not doing that up until now, which introduced a regression. Fixing this also means getting rid of the (flawed) "can't know statically" heuristic.

Fixes #10858, though for a complete solution we likely need some way to tell `<svelte:element>` the namespace at runtime through a special attribute. Maybe we can use `xmlns` for that like we do in the static case
pull/11219/head
Simon Holthausen 2 years ago
parent 4b59ef3c41
commit e304445476

@ -1341,7 +1341,8 @@ const common_visitors = {
ancestor.type === 'SvelteFragment' ||
ancestor.type === 'SnippetBlock'
) {
// Inside a slot or a snippet -> this resets the namespace, so we can't determine it
// Inside a slot or a snippet -> this resets the namespace, so assume the component namespace
node.metadata.svg = context.state.options.namespace === 'svg';
return;
}
if (ancestor.type === 'SvelteElement' || ancestor.type === 'RegularElement') {

@ -7,7 +7,7 @@ import { global_visitors } from './visitors/global.js';
import { javascript_visitors } from './visitors/javascript.js';
import { javascript_visitors_runes } from './visitors/javascript-runes.js';
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
import { is_state_source, serialize_get_binding } from './utils.js';
import { serialize_get_binding } from './utils.js';
import { render_stylesheet } from '../css/index.js';
/**

@ -2096,11 +2096,7 @@ export const template_visitors = {
'$.element',
context.state.node,
get_tag,
node.metadata.svg === true
? b.true
: node.metadata.svg === false
? b.false
: b.literal(null),
node.metadata.svg ? b.true : b.false,
inner.length === 0
? /** @type {any} */ (undefined)
: b.arrow([element_id, b.id('$$anchor')], b.block(inner))

@ -316,10 +316,10 @@ export interface SvelteElement extends BaseElement {
tag: Expression;
metadata: {
/**
* `true`/`false` if this is definitely (not) an svg element.
* `null` means we can't know statically.
* `true` if this is an svg element. The boolean may not be accurate because
* the tag is dynamic, but we do our best to infer it from the template.
*/
svg: boolean | null;
svg: boolean;
scoped: boolean;
};
}

@ -39,7 +39,7 @@ function swap_block_dom(effect, from, to) {
/**
* @param {Comment} anchor
* @param {() => string} get_tag
* @param {boolean | null} is_svg `null` == not statically known
* @param {boolean} is_svg
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn
* @returns {void}
*/
@ -74,15 +74,10 @@ export function element(anchor, get_tag, is_svg, render_fn) {
var previous_each_item = current_each_item;
set_current_each_item(each_item_block);
// We try our best infering the namespace in case it's not possible to determine statically,
// but on the first render on the client (without hydration) the parent will be undefined,
// since the anchor is not attached to its parent / the dom yet.
const ns =
is_svg || next_tag === 'svg'
? namespace_svg
: is_svg === false || anchor.parentElement?.tagName === 'foreignObject'
? null
: anchor.parentElement?.namespaceURI ?? null;
// The namespace may not be statically known but we can't really infer it either,
// because on the first render on the client (without hydration) the parent will be undefined,
// and the element itself could be a tag that changes the namespace.
const ns = is_svg || next_tag === 'svg' ? namespace_svg : null;
if (effect) {
if (next_tag === null) {

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
test({ assert, target }) {
const path = target.querySelector('path');
assert.equal(path?.namespaceURI, 'http://www.w3.org/2000/svg');
}
});

@ -0,0 +1,11 @@
<svelte:options namespace="svg" />
<script>
import Svg from "./svg.svelte";
let tag = "path";
</script>
<Svg>
<svelte:element this="{tag}" d="M21 12a9 9 0 1 1-6.219-8.56"/>
</Svg>

@ -0,0 +1 @@
<svg><slot></slot></svg>

After

Width:  |  Height:  |  Size: 24 B

Loading…
Cancel
Save