From 9eca3d07360ef01a7524af5b1c63155f86f22a65 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 1 Aug 2024 03:37:48 +0200 Subject: [PATCH] fix: allow nested `
`/`
` elements if they are within a `
` element (#12681) * fix: allow nested `
`/`
` elements if they are within a `
` 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 --- .changeset/mean-parents-film.md | 5 ++++ .../2-analyze/visitors/RegularElement.js | 5 +++- packages/svelte/src/html-tree-validation.js | 29 ++++++++++++++----- packages/svelte/src/internal/server/dev.js | 21 +++++++++----- .../invalid-node-placement-6/errors.json | 14 +++++++++ .../invalid-node-placement-6/input.svelte | 14 +++++++++ 6 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 .changeset/mean-parents-film.md create mode 100644 packages/svelte/tests/validator/samples/invalid-node-placement-6/errors.json create mode 100644 packages/svelte/tests/validator/samples/invalid-node-placement-6/input.svelte diff --git a/.changeset/mean-parents-film.md b/.changeset/mean-parents-film.md new file mode 100644 index 0000000000..689ae01ecc --- /dev/null +++ b/.changeset/mean-parents-film.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow nested `
`/`
` elements if they are within a `
` element diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js index 3207919ada..bde068184c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js @@ -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]; @@ -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 { diff --git a/packages/svelte/src/html-tree-validation.js b/packages/svelte/src/html-tree-validation.js index c7f2554966..fb4fbeadf5 100644 --- a/packages/svelte/src/html-tree-validation.js +++ b/packages/svelte/src/html-tree-validation.js @@ -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} + * @type {Record} */ 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', @@ -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} + * @type {Record} */ const disallowed_children = { ...autoclosing_children, @@ -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; } /** diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js index 1904995086..cc43d642fe 100644 --- a/packages/svelte/src/internal/server/dev.js +++ b/packages/svelte/src/internal/server/dev.js @@ -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; diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-6/errors.json b/packages/svelte/tests/validator/samples/invalid-node-placement-6/errors.json new file mode 100644 index 0000000000..b80d739621 --- /dev/null +++ b/packages/svelte/tests/validator/samples/invalid-node-placement-6/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "node_invalid_placement", + "message": "`
` is invalid inside `
`", + "start": { + "line": 11, + "column": 3 + }, + "end": { + "line": 11, + "column": 19 + } + } +] diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-6/input.svelte b/packages/svelte/tests/validator/samples/invalid-node-placement-6/input.svelte new file mode 100644 index 0000000000..0077ffd36e --- /dev/null +++ b/packages/svelte/tests/validator/samples/invalid-node-placement-6/input.svelte @@ -0,0 +1,14 @@ +
+
valid
+
+ +
+
valid
+
valid
+
+ +
+
invalid
+ +
+