Skip to content

chore: refactor HTML validation #11969

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

Closed
wants to merge 1 commit into from
Closed
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
34 changes: 3 additions & 31 deletions packages/svelte/src/compiler/phases/1-parse/utils/html.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -120,34 +120,6 @@ function validate_code(code) {
return NUL;
}

// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission

/** @type {Record<string, Set<string>>} */
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 `<li>one<li>two`?

Expand All @@ -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;
}
}
Expand Down
35 changes: 5 additions & 30 deletions packages/svelte/src/compiler/phases/2-analyze/validation.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
}
Expand Down
61 changes: 61 additions & 0 deletions packages/svelte/src/compiler/utils/html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const interactive_elements = ['a', 'button', 'iframe', 'embed', 'select', 'textarea'];

/** @type {Record<string, string[]>} */
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<string, string[]>} */
export const disallowed_parents = {};

for (const parent in disallowed_children) {
for (const child of disallowed_children[parent]) {
(disallowed_parents[child] ??= []).push(parent);
}
}
41 changes: 0 additions & 41 deletions packages/svelte/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

Expand Down
22 changes: 5 additions & 17 deletions packages/svelte/src/internal/server/dev.js
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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;
}
Expand Down