Skip to content

Commit 9eca3d0

Browse files
authored
fix: allow nested <dt>/<dd> elements if they are within a <dl> element (#12681)
* fix: allow nested `<dt>`/`<dd>` elements if they are within a `<dl>` element This introduces a resets array, which means descendants that are forbidden are allowed again, if an element within the resets array is encountered between the tag and the forbidden descendant fixes #12676 * better name
1 parent 9411b6f commit 9eca3d0

File tree

6 files changed

+71
-17
lines changed

6 files changed

+71
-17
lines changed

.changeset/mean-parents-film.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: allow nested `<dt>`/`<dd>` elements if they are within a `<dl>` element

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export function RegularElement(node, context) {
9898
if (context.state.parent_element) {
9999
let past_parent = false;
100100
let only_warn = false;
101+
const ancestors = [context.state.parent_element];
101102

102103
for (let i = context.path.length - 1; i >= 0; i--) {
103104
const ancestor = context.path[i];
@@ -129,7 +130,9 @@ export function RegularElement(node, context) {
129130
past_parent = true;
130131
}
131132
} else if (ancestor.type === 'RegularElement') {
132-
if (!is_tag_valid_with_ancestor(node.name, ancestor.name)) {
133+
ancestors.push(ancestor.name);
134+
135+
if (!is_tag_valid_with_ancestor(node.name, ancestors)) {
133136
if (only_warn) {
134137
w.node_invalid_placement_ssr(node, `\`<${node.name}>\``, ancestor.name);
135138
} else {

packages/svelte/src/html-tree-validation.js

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
* Map of elements that have certain elements that are not allowed inside them, in the sense that they will auto-close the parent/ancestor element.
33
* Theoretically one could take advantage of it but most of the time it will just result in confusing behavior and break when SSR'd.
44
* There are more elements that are invalid inside other elements, but they're not auto-closed and so don't break SSR and are therefore not listed here.
5-
* @type {Record<string, { direct: string[]} | { descendant: string[] }>}
5+
* @type {Record<string, { direct: string[]} | { descendant: string[]; reset_by?: string[] }>}
66
*/
77
const autoclosing_children = {
88
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
99
li: { direct: ['li'] },
10-
dt: { descendant: ['dt', 'dd'] },
11-
dd: { descendant: ['dt', 'dd'] },
10+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt#technical_summary
11+
dt: { descendant: ['dt', 'dd'], reset_by: ['dl'] },
12+
dd: { descendant: ['dt', 'dd'], reset_by: ['dl'] },
1213
p: {
1314
descendant: [
1415
'address',
@@ -75,7 +76,7 @@ export function closing_tag_omitted(current, next) {
7576
/**
7677
* Map of elements that have certain elements that are not allowed inside them, in the sense that the browser will somehow repair the HTML.
7778
* There are more elements that are invalid inside other elements, but they're not repaired and so don't break SSR and are therefore not listed here.
78-
* @type {Record<string, { direct: string[]} | { descendant: string[]; only?: string[] } | { only: string[] }>}
79+
* @type {Record<string, { direct: string[]} | { descendant: string[]; reset_by?: string[]; only?: string[] } | { only: string[] }>}
7980
*/
8081
const disallowed_children = {
8182
...autoclosing_children,
@@ -137,12 +138,24 @@ const disallowed_children = {
137138
* Returns false if the tag is not allowed inside the ancestor tag (which is grandparent and above) such that it will result
138139
* in the browser repairing the HTML, which will likely result in an error during hydration.
139140
* @param {string} tag
140-
* @param {string} ancestor Must not be the parent, but higher up the tree
141+
* @param {string[]} ancestors All nodes starting with the parent, up until the ancestor, which means two entries minimum
141142
* @returns {boolean}
142143
*/
143-
export function is_tag_valid_with_ancestor(tag, ancestor) {
144-
const disallowed = disallowed_children[ancestor];
145-
return !disallowed || ('descendant' in disallowed ? !disallowed.descendant.includes(tag) : true);
144+
export function is_tag_valid_with_ancestor(tag, ancestors) {
145+
const target = ancestors[ancestors.length - 1];
146+
const disallowed = disallowed_children[target];
147+
if (!disallowed) return true;
148+
149+
if ('reset_by' in disallowed && disallowed.reset_by) {
150+
for (let i = ancestors.length - 2; i >= 0; i--) {
151+
// A reset means that forbidden descendants are allowed again
152+
if (disallowed.reset_by.includes(ancestors[i])) {
153+
return true;
154+
}
155+
}
156+
}
157+
158+
return 'descendant' in disallowed ? !disallowed.descendant.includes(tag) : true;
146159
}
147160

148161
/**

packages/svelte/src/internal/server/dev.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,22 @@ function print_error(payload, parent, child) {
5959
export function push_element(payload, tag, line, column) {
6060
var filename = /** @type {Component} */ (current_component).function[FILENAME];
6161
var child = { tag, parent, filename, line, column };
62-
var ancestor = parent?.parent;
6362

64-
if (parent !== null && !is_tag_valid_with_parent(tag, parent.tag)) {
65-
print_error(payload, parent, child);
66-
}
63+
if (parent !== null) {
64+
var ancestor = parent.parent;
65+
var ancestors = [parent.tag];
66+
67+
if (!is_tag_valid_with_parent(tag, parent.tag)) {
68+
print_error(payload, parent, child);
69+
}
6770

68-
while (ancestor != null) {
69-
if (!is_tag_valid_with_ancestor(tag, ancestor.tag)) {
70-
print_error(payload, ancestor, child);
71+
while (ancestor != null) {
72+
ancestors.push(ancestor.tag);
73+
if (!is_tag_valid_with_ancestor(tag, ancestors)) {
74+
print_error(payload, ancestor, child);
75+
}
76+
ancestor = ancestor.parent;
7177
}
72-
ancestor = ancestor.parent;
7378
}
7479

7580
parent = child;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"code": "node_invalid_placement",
4+
"message": "`<dt>` is invalid inside `<dd>`",
5+
"start": {
6+
"line": 11,
7+
"column": 3
8+
},
9+
"end": {
10+
"line": 11,
11+
"column": 19
12+
}
13+
}
14+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<dl>
2+
<dt>valid</dt>
3+
<dd>
4+
<!-- dl resets the validation -->
5+
<dl>
6+
<dt>valid</dt>
7+
<dd>valid</dd>
8+
</dl>
9+
<!-- other tags don't -->
10+
<div>
11+
<dt>invalid</dt>
12+
</div>
13+
</dd>
14+
</dl>

0 commit comments

Comments
 (0)