Skip to content

fix: allow nested <dt>/<dd> elements if they are within a <dl> element #12681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mean-parents-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: allow nested `<dt>`/`<dd>` elements if they are within a `<dl>` element
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export function RegularElement(node, context) {
if (context.state.parent_element) {
let past_parent = false;
let only_warn = false;
const ancestors = [context.state.parent_element];

for (let i = context.path.length - 1; i >= 0; i--) {
const ancestor = context.path[i];
Expand Down Expand Up @@ -129,7 +130,9 @@ export function RegularElement(node, context) {
past_parent = true;
}
} else if (ancestor.type === 'RegularElement') {
if (!is_tag_valid_with_ancestor(node.name, ancestor.name)) {
ancestors.push(ancestor.name);

if (!is_tag_valid_with_ancestor(node.name, ancestors)) {
if (only_warn) {
w.node_invalid_placement_ssr(node, `\`<${node.name}>\``, ancestor.name);
} else {
Expand Down
29 changes: 21 additions & 8 deletions packages/svelte/src/html-tree-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
* 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.
* Theoretically one could take advantage of it but most of the time it will just result in confusing behavior and break when SSR'd.
* 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.
* @type {Record<string, { direct: string[]} | { descendant: string[] }>}
* @type {Record<string, { direct: string[]} | { descendant: string[]; reset_by?: string[] }>}
*/
const autoclosing_children = {
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
li: { direct: ['li'] },
dt: { descendant: ['dt', 'dd'] },
dd: { descendant: ['dt', 'dd'] },
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt#technical_summary
dt: { descendant: ['dt', 'dd'], reset_by: ['dl'] },
dd: { descendant: ['dt', 'dd'], reset_by: ['dl'] },
p: {
descendant: [
'address',
Expand Down Expand Up @@ -75,7 +76,7 @@ export function closing_tag_omitted(current, next) {
/**
* Map of elements that have certain elements that are not allowed inside them, in the sense that the browser will somehow repair the HTML.
* 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.
* @type {Record<string, { direct: string[]} | { descendant: string[]; only?: string[] } | { only: string[] }>}
* @type {Record<string, { direct: string[]} | { descendant: string[]; reset_by?: string[]; only?: string[] } | { only: string[] }>}
*/
const disallowed_children = {
...autoclosing_children,
Expand Down Expand Up @@ -137,12 +138,24 @@ const disallowed_children = {
* Returns false if the tag is not allowed inside the ancestor tag (which is grandparent and above) such that it will result
* in the browser repairing the HTML, which will likely result in an error during hydration.
* @param {string} tag
* @param {string} ancestor Must not be the parent, but higher up the tree
* @param {string[]} ancestors All nodes starting with the parent, up until the ancestor, which means two entries minimum
* @returns {boolean}
*/
export function is_tag_valid_with_ancestor(tag, ancestor) {
const disallowed = disallowed_children[ancestor];
return !disallowed || ('descendant' in disallowed ? !disallowed.descendant.includes(tag) : true);
export function is_tag_valid_with_ancestor(tag, ancestors) {
const target = ancestors[ancestors.length - 1];
const disallowed = disallowed_children[target];
if (!disallowed) return true;

if ('reset_by' in disallowed && disallowed.reset_by) {
for (let i = ancestors.length - 2; i >= 0; i--) {
// A reset means that forbidden descendants are allowed again
if (disallowed.reset_by.includes(ancestors[i])) {
return true;
}
}
}

return 'descendant' in disallowed ? !disallowed.descendant.includes(tag) : true;
}

/**
Expand Down
21 changes: 13 additions & 8 deletions packages/svelte/src/internal/server/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,22 @@ function print_error(payload, parent, child) {
export function push_element(payload, tag, line, column) {
var filename = /** @type {Component} */ (current_component).function[FILENAME];
var child = { tag, parent, filename, line, column };
var ancestor = parent?.parent;

if (parent !== null && !is_tag_valid_with_parent(tag, parent.tag)) {
print_error(payload, parent, child);
}
if (parent !== null) {
var ancestor = parent.parent;
var ancestors = [parent.tag];

if (!is_tag_valid_with_parent(tag, parent.tag)) {
print_error(payload, parent, child);
}

while (ancestor != null) {
if (!is_tag_valid_with_ancestor(tag, ancestor.tag)) {
print_error(payload, ancestor, child);
while (ancestor != null) {
ancestors.push(ancestor.tag);
if (!is_tag_valid_with_ancestor(tag, ancestors)) {
print_error(payload, ancestor, child);
}
ancestor = ancestor.parent;
}
ancestor = ancestor.parent;
}

parent = child;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"code": "node_invalid_placement",
"message": "`<dt>` is invalid inside `<dd>`",
"start": {
"line": 11,
"column": 3
},
"end": {
"line": 11,
"column": 19
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<dl>
<dt>valid</dt>
<dd>
<!-- dl resets the validation -->
<dl>
<dt>valid</dt>
<dd>valid</dd>
</dl>
<!-- other tags don't -->
<div>
<dt>invalid</dt>
</div>
</dd>
</dl>
Loading