Skip to content

Commit 7e70188

Browse files
committed
fix: correctly infer <a> tag namespace
`<a>` tags are valid in both the SVG and HTML namespace. If there's no parent, we therefore have to look downwards to see if it's the parent of a SVG or HTML element. fixes #7807 fixes #13793
1 parent a952860 commit 7e70188

File tree

10 files changed

+138
-1
lines changed

10 files changed

+138
-1
lines changed

.changeset/modern-pets-punch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: correctly infer `<a>` tag namespace

packages/svelte/src/compiler/phases/2-analyze/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export interface AnalysisState {
99
analysis: ComponentAnalysis;
1010
options: ValidatedCompileOptions;
1111
ast_type: 'instance' | 'template' | 'module';
12+
/**
13+
* Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root.
14+
* Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between.
15+
*/
1216
parent_element: string | null;
1317
has_props_rune: boolean;
1418
/** Which slots the current parent component has */

packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,17 @@ export function RegularElement(node, context) {
174174
}
175175

176176
context.next({ ...context.state, parent_element: node.name });
177+
178+
// Special case: <a> tags are valid in both the SVG and HTML namespace.
179+
// If there's no parent, look downwards to see if it's the parent of a SVG or HTML element.
180+
if (node.name === 'a' && !context.state.parent_element) {
181+
for (const child of node.fragment.nodes) {
182+
if (child.type === 'RegularElement') {
183+
if (child.metadata.svg && child.name !== 'svg') {
184+
node.metadata.svg = true;
185+
break;
186+
}
187+
}
188+
}
189+
}
177190
}

packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @import { AST } from '#compiler' */
2-
/** @import { Context } from '../../types' */
2+
/** @import { AnalysisState, Context } from '../../types' */
33
import * as e from '../../../../errors.js';
44
import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js';
55
import { determine_slot } from '../../../../utils/slot.js';
@@ -96,6 +96,7 @@ export function visit_component(node, context) {
9696
const component_slots = new Set();
9797

9898
for (const slot_name in nodes) {
99+
/** @type {AnalysisState} */
99100
const state = {
100101
...context.state,
101102
scope: node.metadata.scopes[slot_name],
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `
5+
<div><a><span>Hello</span></a></div>
6+
<div><a><span>Hello</span></a></div>
7+
<div><a><span>Hello</span></a></div>
8+
`,
9+
test({ assert, target }) {
10+
const div = target.querySelectorAll('div');
11+
const a = target.querySelectorAll('a');
12+
const span = target.querySelectorAll('span');
13+
14+
for (const element of div) {
15+
assert.equal(element.namespaceURI, 'http://www.w3.org/1999/xhtml');
16+
}
17+
18+
for (const element of a) {
19+
assert.equal(element.namespaceURI, 'http://www.w3.org/1999/xhtml');
20+
}
21+
22+
for (const element of span) {
23+
assert.equal(element.namespaceURI, 'http://www.w3.org/1999/xhtml');
24+
}
25+
}
26+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let { children } = $props();
3+
</script>
4+
5+
<div>
6+
{@render children()}
7+
</div>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script>
2+
import Div from './div.svelte';
3+
</script>
4+
5+
<div>
6+
<a>
7+
<span>Hello</span>
8+
</a>
9+
</div>
10+
11+
<div>
12+
{#snippet test()}
13+
<a>
14+
<span>Hello</span>
15+
</a>
16+
{/snippet}
17+
{@render test()}
18+
</div>
19+
20+
<Div>
21+
<a>
22+
<span>Hello</span>
23+
</a>
24+
</Div>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `
5+
<svg><a><text>Hello</text></a></svg>
6+
<svg><a><text>Hello</text></a></svg>
7+
<svg><a><text>Hello</text></a></svg>
8+
`,
9+
test({ assert, target }) {
10+
const svg = target.querySelectorAll('svg');
11+
const a = target.querySelectorAll('a');
12+
const text = target.querySelectorAll('text');
13+
14+
for (const element of svg) {
15+
assert.equal(element.namespaceURI, 'http://www.w3.org/2000/svg');
16+
}
17+
18+
for (const element of a) {
19+
assert.equal(element.namespaceURI, 'http://www.w3.org/2000/svg');
20+
}
21+
22+
for (const element of text) {
23+
assert.equal(element.namespaceURI, 'http://www.w3.org/2000/svg');
24+
}
25+
}
26+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script>
2+
import Svg from './svg.svelte';
3+
</script>
4+
5+
<svg>
6+
<a>
7+
<text>Hello</text>
8+
</a>
9+
</svg>
10+
11+
<svg>
12+
{#snippet test()}
13+
<a>
14+
<text>Hello</text>
15+
</a>
16+
{/snippet}
17+
{@render test()}
18+
</svg>
19+
20+
<Svg>
21+
<a>
22+
<text>Hello</text>
23+
</a>
24+
</Svg>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let { children } = $props();
3+
</script>
4+
5+
<svg>
6+
{@render children()}
7+
</svg>

0 commit comments

Comments
 (0)