From 52056cfac63961cce211143900d33492e3b81896 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 8 Jun 2024 11:09:03 -0400 Subject: [PATCH] chore: refactor HTML validation --- .../src/compiler/phases/1-parse/utils/html.js | 34 +---------- .../compiler/phases/2-analyze/validation.js | 35 ++--------- packages/svelte/src/compiler/utils/html.js | 61 +++++++++++++++++++ packages/svelte/src/constants.js | 41 ------------- packages/svelte/src/internal/server/dev.js | 22 ++----- 5 files changed, 74 insertions(+), 119 deletions(-) create mode 100644 packages/svelte/src/compiler/utils/html.js diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/html.js b/packages/svelte/src/compiler/phases/1-parse/utils/html.js index 3cc9a5a20724..380374c52dfd 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/html.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/html.js @@ -1,4 +1,4 @@ -import { interactive_elements } from '../../../../constants.js'; +import { disallowed_children } from '../../../utils/html.js'; import entities from './entities.js'; const windows_1252 = [ @@ -120,34 +120,6 @@ function validate_code(code) { return NUL; } -// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission - -/** @type {Record>} */ -const disallowed_contents = { - li: new Set(['li']), - dt: new Set(['dt', 'dd']), - dd: new Set(['dt', 'dd']), - p: new Set( - 'address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split( - ' ' - ) - ), - rt: new Set(['rt', 'rp']), - rp: new Set(['rt', 'rp']), - optgroup: new Set(['optgroup']), - option: new Set(['option', 'optgroup']), - thead: new Set(['tbody', 'tfoot']), - tbody: new Set(['tbody', 'tfoot']), - tfoot: new Set(['tbody']), - tr: new Set(['tr', 'tbody']), - td: new Set(['td', 'th', 'tr']), - th: new Set(['td', 'th', 'tr']) -}; - -for (const interactive_element of interactive_elements) { - disallowed_contents[interactive_element] = interactive_elements; -} - // can this be a child of the parent element, or does it implicitly // close it, like `
  • one
  • two`? @@ -156,8 +128,8 @@ for (const interactive_element of interactive_elements) { * @param {string} [next] */ export function closing_tag_omitted(current, next) { - if (disallowed_contents[current]) { - if (!next || disallowed_contents[current].has(next)) { + if (disallowed_children[current]) { + if (!next || disallowed_children[current].includes(next)) { return true; } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index e54828375e65..fea99ce85155 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -1,9 +1,5 @@ import is_reference from 'is-reference'; -import { - disallowed_paragraph_contents, - interactive_elements, - is_tag_valid_with_parent -} from '../../../constants.js'; +import { is_tag_valid_with_parent } from '../../../constants.js'; import * as e from '../../errors.js'; import { extract_identifiers, @@ -32,6 +28,7 @@ import { import { Scope, get_rune } from '../scope.js'; import { merge } from '../visitors.js'; import { a11y_validators } from './a11y.js'; +import { disallowed_parents } from '../../utils/html.js'; /** @param {import('#compiler').Attribute} attribute */ function validate_attribute(attribute) { @@ -568,34 +565,12 @@ const validation = { } } - // can't add form to interactive elements because those are also used by the parser - // to check for the last auto-closing parent. - if (node.name === 'form') { - const path = context.path; - for (let parent of path) { - if (parent.type === 'RegularElement' && parent.name === 'form') { - e.node_invalid_placement(node, `<${node.name}>`, parent.name); - } - } - } - - if (interactive_elements.has(node.name)) { - const path = context.path; - for (let parent of path) { - if ( - parent.type === 'RegularElement' && - parent.name === node.name && - interactive_elements.has(parent.name) - ) { - e.node_invalid_placement(node, `<${node.name}>`, parent.name); - } - } - } + if (node.name in disallowed_parents) { + const parents = disallowed_parents[node.name]; - if (disallowed_paragraph_contents.includes(node.name)) { const path = context.path; for (let parent of path) { - if (parent.type === 'RegularElement' && parent.name === 'p') { + if (parent.type === 'RegularElement' && parents.includes(parent.name)) { e.node_invalid_placement(node, `<${node.name}>`, parent.name); } } diff --git a/packages/svelte/src/compiler/utils/html.js b/packages/svelte/src/compiler/utils/html.js new file mode 100644 index 000000000000..a15a9f3e0a84 --- /dev/null +++ b/packages/svelte/src/compiler/utils/html.js @@ -0,0 +1,61 @@ +const interactive_elements = ['a', 'button', 'iframe', 'embed', 'select', 'textarea']; + +/** @type {Record} */ +export const disallowed_children = { + dd: ['dd', 'dt'], + dt: ['dd', 'dt'], + form: ['form'], + li: ['li'], + optgroup: ['optgroup'], + option: ['option', 'optgroup'], + p: [ + 'address', + 'article', + 'aside', + 'blockquote', + 'div', + 'dl', + 'fieldset', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'header', + 'hgroup', + 'hr', + 'main', + 'menu', + 'nav', + 'ol', + 'p', + 'pre', + 'section', + 'table', + 'ul' + ], + rp: ['rp', 'rt'], + rt: ['rp', 'rt'], + tbody: ['tbody', 'tfoot'], + td: ['td', 'th', 'tr'], + th: ['td', 'th', 'tr'], + tfoot: ['tbody'], + thead: ['tbody', 'tfoot'], + tr: ['tr', 'tbody'] +}; + +for (const interactive_element of interactive_elements) { + disallowed_children[interactive_element] = [...interactive_elements]; +} + +/** @type {Record} */ +export const disallowed_parents = {}; + +for (const parent in disallowed_children) { + for (const child of disallowed_children[parent]) { + (disallowed_parents[child] ??= []).push(parent); + } +} diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index a8963d685462..1bbfded003c6 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -112,47 +112,6 @@ export const DOMBooleanAttributes = [ export const namespace_svg = 'http://www.w3.org/2000/svg'; export const namespace_mathml = 'http://www.w3.org/1998/Math/MathML'; -// while `input` is also an interactive element, it is never moved by the browser, so we don't need to check for it -export const interactive_elements = new Set([ - 'a', - 'button', - 'iframe', - 'embed', - 'select', - 'textarea' -]); - -export const disallowed_paragraph_contents = [ - 'address', - 'article', - 'aside', - 'blockquote', - 'details', - 'div', - 'dl', - 'fieldset', - 'figcapture', - 'figure', - 'footer', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'header', - 'hr', - 'menu', - 'nav', - 'ol', - 'pre', - 'section', - 'table', - 'ul', - 'p' -]; - // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags const implied_end_tags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt']; diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js index 601668e0ad78..cd26fc850c6d 100644 --- a/packages/svelte/src/internal/server/dev.js +++ b/packages/svelte/src/internal/server/dev.js @@ -1,8 +1,5 @@ -import { - disallowed_paragraph_contents, - interactive_elements, - is_tag_valid_with_parent -} from '../../constants.js'; +import { disallowed_parents } from '../../compiler/utils/html.js'; +import { is_tag_valid_with_parent } from '../../constants.js'; import { current_component } from './context.js'; /** @@ -63,21 +60,12 @@ export function push_element(payload, tag, line, column) { print_error(payload, parent, child); } - if (interactive_elements.has(tag)) { - let element = parent; - while (element !== null) { - if (interactive_elements.has(element.tag)) { - print_error(payload, element, child); - break; - } - element = element.parent; - } - } + if (tag in disallowed_parents) { + const parents = disallowed_parents[tag]; - if (disallowed_paragraph_contents.includes(tag)) { let element = parent; while (element !== null) { - if (element.tag === 'p') { + if (parents.includes(element.tag)) { print_error(payload, element, child); break; }