Skip to content

Commit 6ff1862

Browse files
eEQKdummdidumm
authored andcommitted
fix: create <svelte:element> instances with the correct namespace (#10006)
Infer namespace from parents where possible, and do a runtime-best-effort where it's not statically known fixes #9645 --------- Co-authored-by: Simon Holthausen <[email protected]>
1 parent 62c8d21 commit 6ff1862

File tree

17 files changed

+223
-80
lines changed

17 files changed

+223
-80
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ jobs:
5555
- name: type check
5656
run: pnpm check
5757
- name: lint
58+
if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail (avoids multiple runs uncovering different issues at different steps)
5859
run: pnpm lint
5960
- name: build and check generated types
61+
if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail
6062
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); }

packages/svelte/src/compiler/phases/1-parse/read/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { namespace_svg } from '../../../../constants.js';
12
import { error } from '../../../errors.js';
23

34
const regex_valid_tag_name = /^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/;
@@ -156,7 +157,7 @@ export default function read_options(node) {
156157
error(attribute, 'invalid-svelte-option-namespace');
157158
}
158159

159-
if (value === 'http://www.w3.org/2000/svg') {
160+
if (value === namespace_svg) {
160161
component_options.namespace = 'svg';
161162
} else if (value === 'html' || value === 'svg' || value === 'foreign') {
162163
component_options.namespace = value;

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ export default function tag(parser) {
139139
name,
140140
attributes: [],
141141
fragment: create_fragment(true),
142-
parent: null
142+
parent: null,
143+
metadata: {
144+
svg: false
145+
}
143146
};
144147

145148
parser.allow_whitespace();

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { warn } from '../../warnings.js';
2020
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
2121
import { regex_starts_with_newline } from '../patterns.js';
2222
import { create_attribute, is_element_node } from '../nodes.js';
23-
import { DelegatedEvents } from '../../../constants.js';
23+
import { DelegatedEvents, namespace_svg } from '../../../constants.js';
2424
import { should_proxy_or_freeze } from '../3-transform/client/utils.js';
2525

2626
/**
@@ -1104,8 +1104,47 @@ const common_visitors = {
11041104

11051105
context.state.analysis.elements.push(node);
11061106
},
1107-
SvelteElement(node, { state }) {
1108-
state.analysis.elements.push(node);
1107+
SvelteElement(node, context) {
1108+
context.state.analysis.elements.push(node);
1109+
1110+
if (
1111+
context.state.options.namespace !== 'foreign' &&
1112+
node.tag.type === 'Literal' &&
1113+
typeof node.tag.value === 'string' &&
1114+
SVGElements.includes(node.tag.value)
1115+
) {
1116+
node.metadata.svg = true;
1117+
return;
1118+
}
1119+
1120+
for (const attribute of node.attributes) {
1121+
if (attribute.type === 'Attribute') {
1122+
if (attribute.name === 'xmlns' && is_text_attribute(attribute)) {
1123+
node.metadata.svg = attribute.value[0].data === namespace_svg;
1124+
return;
1125+
}
1126+
}
1127+
}
1128+
1129+
for (let i = context.path.length - 1; i >= 0; i--) {
1130+
const ancestor = context.path[i];
1131+
if (
1132+
ancestor.type === 'Component' ||
1133+
ancestor.type === 'SvelteComponent' ||
1134+
ancestor.type === 'SvelteFragment' ||
1135+
ancestor.type === 'SnippetBlock'
1136+
) {
1137+
// Inside a slot or a snippet -> this resets the namespace, so we can't determine it
1138+
return;
1139+
}
1140+
if (ancestor.type === 'SvelteElement' || ancestor.type === 'RegularElement') {
1141+
node.metadata.svg =
1142+
ancestor.type === 'RegularElement' && ancestor.name === 'foreignObject'
1143+
? false
1144+
: ancestor.metadata.svg;
1145+
return;
1146+
}
1147+
}
11091148
}
11101149
};
11111150

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { binding_properties } from '../../../bindings.js';
1010
import {
1111
clean_nodes,
12-
determine_element_namespace,
12+
determine_namespace_for_children,
1313
escape_html,
1414
infer_namespace
1515
} from '../../utils.js';
@@ -43,11 +43,7 @@ import { sanitize_template_string } from '../../../../utils/sanitize_template_st
4343
*/
4444
function get_attribute_name(element, attribute, context) {
4545
let name = attribute.name;
46-
if (
47-
element.type === 'RegularElement' &&
48-
!element.metadata.svg &&
49-
context.state.metadata.namespace !== 'foreign'
50-
) {
46+
if (!element.metadata.svg && context.state.metadata.namespace !== 'foreign') {
5147
name = name.toLowerCase();
5248
if (name in AttributeAliases) {
5349
name = AttributeAliases[name];
@@ -1854,7 +1850,7 @@ export const template_visitors = {
18541850
const metadata = context.state.metadata;
18551851
const child_metadata = {
18561852
...context.state.metadata,
1857-
namespace: determine_element_namespace(node, context.state.metadata.namespace, context.path)
1853+
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
18581854
};
18591855

18601856
context.state.template.push(`<${node.name}`);
@@ -2079,9 +2075,6 @@ export const template_visitors = {
20792075
/** @type {import('estree').ExpressionStatement[]} */
20802076
const lets = [];
20812077

2082-
/** @type {string | null} */
2083-
let namespace = null;
2084-
20852078
// Create a temporary context which picks up the init/update statements.
20862079
// They'll then be added to the function parameter of $.element
20872080
const element_id = b.id(context.state.scope.generate('$$element'));
@@ -2102,9 +2095,6 @@ export const template_visitors = {
21022095
for (const attribute of node.attributes) {
21032096
if (attribute.type === 'Attribute') {
21042097
attributes.push(attribute);
2105-
if (attribute.name === 'xmlns' && is_text_attribute(attribute)) {
2106-
namespace = attribute.value[0].data;
2107-
}
21082098
} else if (attribute.type === 'SpreadAttribute') {
21092099
attributes.push(attribute);
21102100
} else if (attribute.type === 'ClassDirective') {
@@ -2153,19 +2143,32 @@ export const template_visitors = {
21532143
}
21542144
}
21552145
inner.push(...inner_context.state.after_update);
2156-
inner.push(...create_block(node, 'dynamic_element', node.fragment.nodes, context));
2146+
inner.push(
2147+
...create_block(node, 'dynamic_element', node.fragment.nodes, {
2148+
...context,
2149+
state: {
2150+
...context.state,
2151+
metadata: {
2152+
...context.state.metadata,
2153+
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
2154+
}
2155+
}
2156+
})
2157+
);
21572158
context.state.after_update.push(
21582159
b.stmt(
21592160
b.call(
21602161
'$.element',
21612162
context.state.node,
21622163
get_tag,
2164+
node.metadata.svg === true
2165+
? b.true
2166+
: node.metadata.svg === false
2167+
? b.false
2168+
: b.literal(null),
21632169
inner.length === 0
21642170
? /** @type {any} */ (undefined)
2165-
: b.arrow([element_id, b.id('$$anchor')], b.block(inner)),
2166-
namespace === 'http://www.w3.org/2000/svg'
2167-
? b.literal(true)
2168-
: /** @type {any} */ (undefined)
2171+
: b.arrow([element_id, b.id('$$anchor')], b.block(inner))
21692172
)
21702173
)
21712174
);

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from '../../constants.js';
1616
import {
1717
clean_nodes,
18-
determine_element_namespace,
18+
determine_namespace_for_children,
1919
escape_html,
2020
infer_namespace,
2121
transform_inspect_rune
@@ -482,11 +482,7 @@ function serialize_set_binding(node, context, fallback) {
482482
*/
483483
function get_attribute_name(element, attribute, context) {
484484
let name = attribute.name;
485-
if (
486-
element.type === 'RegularElement' &&
487-
!element.metadata.svg &&
488-
context.state.metadata.namespace !== 'foreign'
489-
) {
485+
if (!element.metadata.svg && context.state.metadata.namespace !== 'foreign') {
490486
name = name.toLowerCase();
491487
// don't lookup boolean aliases here, the server runtime function does only
492488
// check for the lowercase variants of boolean attributes
@@ -761,10 +757,10 @@ function serialize_element_spread_attributes(
761757
}
762758

763759
const lowercase_attributes =
764-
element.type !== 'RegularElement' || element.metadata.svg || is_custom_element_node(element)
760+
element.metadata.svg || (element.type === 'RegularElement' && is_custom_element_node(element))
765761
? b.false
766762
: b.true;
767-
const is_svg = element.type === 'RegularElement' && element.metadata.svg ? b.true : b.false;
763+
const is_svg = element.metadata.svg ? b.true : b.false;
768764
/** @type {import('estree').Expression[]} */
769765
const args = [
770766
b.array(values),
@@ -1165,7 +1161,7 @@ const template_visitors = {
11651161
RegularElement(node, context) {
11661162
const metadata = {
11671163
...context.state.metadata,
1168-
namespace: determine_element_namespace(node, context.state.metadata.namespace, context.path)
1164+
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
11691165
};
11701166

11711167
context.state.template.push(t_string(`<${node.name}`));
@@ -1255,11 +1251,16 @@ const template_visitors = {
12551251
context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', b.thunk(tag))));
12561252
}
12571253

1254+
const metadata = {
1255+
...context.state.metadata,
1256+
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
1257+
};
12581258
/** @type {import('./types').ComponentContext} */
12591259
const inner_context = {
12601260
...context,
12611261
state: {
12621262
...context.state,
1263+
metadata,
12631264
template: [],
12641265
init: []
12651266
}
@@ -1276,7 +1277,10 @@ const template_visitors = {
12761277
inner_context.state.template.push(t_string('>'));
12771278

12781279
const before = serialize_template(inner_context.state.template);
1279-
const main = create_block(node, node.fragment.nodes, context);
1280+
const main = create_block(node, node.fragment.nodes, {
1281+
...context,
1282+
state: { ...context.state, metadata }
1283+
});
12801284
const after = serialize_template([
12811285
t_expression(inner_id),
12821286
t_string('</'),

packages/svelte/src/compiler/phases/3-transform/utils.js

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export function clean_nodes(
188188
}
189189

190190
/**
191-
* Infers the new namespace for the children of a node.
191+
* Infers the namespace for the children of a node that should be used when creating the `$.template(...)`.
192192
* @param {import('#compiler').Namespace} namespace
193193
* @param {import('#compiler').SvelteNode} parent
194194
* @param {import('#compiler').SvelteNode[]} nodes
@@ -201,19 +201,28 @@ export function infer_namespace(namespace, parent, nodes, path) {
201201
path.at(-1)
202202
: parent;
203203

204-
if (
205-
namespace !== 'foreign' &&
204+
if (namespace !== 'foreign') {
205+
if (parent_node?.type === 'RegularElement' && parent_node.name === 'foreignObject') {
206+
return 'html';
207+
}
208+
209+
if (parent_node?.type === 'RegularElement' || parent_node?.type === 'SvelteElement') {
210+
return parent_node.metadata.svg ? 'svg' : 'html';
211+
}
212+
206213
// Re-evaluate the namespace inside slot nodes that reset the namespace
207-
(parent_node === undefined ||
214+
if (
215+
parent_node === undefined ||
208216
parent_node.type === 'Root' ||
209217
parent_node.type === 'Component' ||
210218
parent_node.type === 'SvelteComponent' ||
211219
parent_node.type === 'SvelteFragment' ||
212-
parent_node.type === 'SnippetBlock')
213-
) {
214-
const new_namespace = check_nodes_for_namespace(nodes, 'keep');
215-
if (new_namespace !== 'keep' && new_namespace !== 'maybe_html') {
216-
namespace = new_namespace;
220+
parent_node.type === 'SnippetBlock'
221+
) {
222+
const new_namespace = check_nodes_for_namespace(nodes, 'keep');
223+
if (new_namespace !== 'keep' && new_namespace !== 'maybe_html') {
224+
return new_namespace;
225+
}
217226
}
218227
}
219228

@@ -229,7 +238,7 @@ export function infer_namespace(namespace, parent, nodes, path) {
229238
*/
230239
function check_nodes_for_namespace(nodes, namespace) {
231240
for (const node of nodes) {
232-
if (node.type === 'RegularElement') {
241+
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
233242
if (!node.metadata.svg) {
234243
namespace = 'html';
235244
break;
@@ -279,36 +288,21 @@ function check_nodes_for_namespace(nodes, namespace) {
279288
}
280289

281290
/**
282-
* @param {import('#compiler').RegularElement} node
291+
* Determines the namespace the children of this node are in.
292+
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
283293
* @param {import('#compiler').Namespace} namespace
284-
* @param {import('#compiler').SvelteNode[]} path
285294
* @returns {import('#compiler').Namespace}
286295
*/
287-
export function determine_element_namespace(node, namespace, path) {
288-
if (namespace !== 'foreign') {
289-
let parent = path.at(-1);
290-
if (parent?.type === 'Fragment') {
291-
parent = path.at(-2);
292-
}
296+
export function determine_namespace_for_children(node, namespace) {
297+
if (namespace === 'foreign') {
298+
return namespace;
299+
}
293300

294-
if (node.name === 'foreignObject') {
295-
return 'html';
296-
} else if (
297-
namespace !== 'svg' ||
298-
parent?.type === 'Component' ||
299-
parent?.type === 'SvelteComponent' ||
300-
parent?.type === 'SvelteFragment' ||
301-
parent?.type === 'SnippetBlock'
302-
) {
303-
if (node.metadata.svg) {
304-
return 'svg';
305-
} else {
306-
return 'html';
307-
}
308-
}
301+
if (node.name === 'foreignObject') {
302+
return 'html';
309303
}
310304

311-
return namespace;
305+
return node.metadata.svg ? 'svg' : 'html';
312306
}
313307

314308
/**

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,13 @@ export interface SvelteElement extends BaseElement {
308308
type: 'SvelteElement';
309309
name: 'svelte:element';
310310
tag: Expression;
311+
metadata: {
312+
/**
313+
* `true`/`false` if this is definitely (not) an svg element.
314+
* `null` means we can't know statically.
315+
*/
316+
svg: boolean | null;
317+
};
311318
}
312319

313320
export interface SvelteFragment extends BaseElement {

packages/svelte/src/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,6 @@ export const DOMBooleanAttributes = [
8585
'seamless',
8686
'selected'
8787
];
88+
89+
export const namespace_svg = 'http://www.w3.org/2000/svg';
90+
export const namespace_html = 'http://www.w3.org/1999/xhtml';

0 commit comments

Comments
 (0)