Skip to content

Commit 99e1665

Browse files
trueadmdummdidumm
andauthored
feat: improve ssr html mismatch validation (#10658)
* feat: improve ssr html mismatch validation * update types * Update packages/svelte/src/internal/server/index.js Co-authored-by: Simon H <[email protected]> * Update packages/svelte/src/compiler/validate-options.js Co-authored-by: Simon H <[email protected]> * feedback --------- Co-authored-by: Simon H <[email protected]>
1 parent 3fe4940 commit 99e1665

File tree

12 files changed

+313
-166
lines changed

12 files changed

+313
-166
lines changed

.changeset/hungry-singers-share.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+
feat: improve ssr html mismatch validation

packages/svelte/src/compiler/phases/1-parse/utils/html.js

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { interactive_elements } from '../../../../constants.js';
12
import entities from './entities.js';
23

34
const windows_1252 = [
@@ -121,16 +122,6 @@ function validate_code(code) {
121122

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

124-
// while `input` is also an interactive element, it is never moved by the browser, so we don't need to check for it
125-
export const interactive_elements = new Set([
126-
'a',
127-
'button',
128-
'iframe',
129-
'embed',
130-
'select',
131-
'textarea'
132-
]);
133-
134125
/** @type {Record<string, Set<string>>} */
135126
const disallowed_contents = {
136127
li: new Set(['li']),
@@ -153,36 +144,6 @@ const disallowed_contents = {
153144
th: new Set(['td', 'th', 'tr'])
154145
};
155146

156-
export const disallowed_parapgraph_contents = [
157-
'address',
158-
'article',
159-
'aside',
160-
'blockquote',
161-
'details',
162-
'div',
163-
'dl',
164-
'fieldset',
165-
'figcapture',
166-
'figure',
167-
'footer',
168-
'form',
169-
'h1',
170-
'h2',
171-
'h3',
172-
'h4',
173-
'h5',
174-
'h6',
175-
'header',
176-
'hr',
177-
'menu',
178-
'nav',
179-
'ol',
180-
'pre',
181-
'section',
182-
'table',
183-
'ul'
184-
];
185-
186147
for (const interactive_element of interactive_elements) {
187148
disallowed_contents[interactive_element] = interactive_elements;
188149
}

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 5 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
disallowed_parapgraph_contents,
3+
interactive_elements,
4+
is_tag_valid_with_parent
5+
} from '../../../constants.js';
16
import { error } from '../../errors.js';
27
import {
38
extract_identifiers,
@@ -8,7 +13,6 @@ import {
813
} from '../../utils/ast.js';
914
import { warn } from '../../warnings.js';
1015
import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
11-
import { disallowed_parapgraph_contents, interactive_elements } from '../1-parse/utils/html.js';
1216
import { binding_properties } from '../bindings.js';
1317
import { ContentEditableBindings, EventModifiers, SVGElements } from '../constants.js';
1418
import { is_custom_element_node } from '../nodes.js';
@@ -226,127 +230,6 @@ function validate_slot_attribute(context, attribute) {
226230
}
227231
}
228232

229-
// https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
230-
const implied_end_tags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
231-
232-
/**
233-
* @param {string} tag
234-
* @param {string} parent_tag
235-
* @returns {boolean}
236-
*/
237-
function is_tag_valid_with_parent(tag, parent_tag) {
238-
// First, let's check if we're in an unusual parsing mode...
239-
switch (parent_tag) {
240-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
241-
case 'select':
242-
return tag === 'option' || tag === 'optgroup' || tag === '#text';
243-
case 'optgroup':
244-
return tag === 'option' || tag === '#text';
245-
// Strictly speaking, seeing an <option> doesn't mean we're in a <select>
246-
// but
247-
case 'option':
248-
return tag === '#text';
249-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
250-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
251-
// No special behavior since these rules fall back to "in body" mode for
252-
// all except special table nodes which cause bad parsing behavior anyway.
253-
254-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
255-
case 'tr':
256-
return (
257-
tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template'
258-
);
259-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
260-
case 'tbody':
261-
case 'thead':
262-
case 'tfoot':
263-
return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';
264-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
265-
case 'colgroup':
266-
return tag === 'col' || tag === 'template';
267-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
268-
case 'table':
269-
return (
270-
tag === 'caption' ||
271-
tag === 'colgroup' ||
272-
tag === 'tbody' ||
273-
tag === 'tfoot' ||
274-
tag === 'thead' ||
275-
tag === 'style' ||
276-
tag === 'script' ||
277-
tag === 'template'
278-
);
279-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
280-
case 'head':
281-
return (
282-
tag === 'base' ||
283-
tag === 'basefont' ||
284-
tag === 'bgsound' ||
285-
tag === 'link' ||
286-
tag === 'meta' ||
287-
tag === 'title' ||
288-
tag === 'noscript' ||
289-
tag === 'noframes' ||
290-
tag === 'style' ||
291-
tag === 'script' ||
292-
tag === 'template'
293-
);
294-
// https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
295-
case 'html':
296-
return tag === 'head' || tag === 'body' || tag === 'frameset';
297-
case 'frameset':
298-
return tag === 'frame';
299-
case '#document':
300-
return tag === 'html';
301-
}
302-
303-
// Probably in the "in body" parsing mode, so we outlaw only tag combos
304-
// where the parsing rules cause implicit opens or closes to be added.
305-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
306-
switch (tag) {
307-
case 'h1':
308-
case 'h2':
309-
case 'h3':
310-
case 'h4':
311-
case 'h5':
312-
case 'h6':
313-
return (
314-
parent_tag !== 'h1' &&
315-
parent_tag !== 'h2' &&
316-
parent_tag !== 'h3' &&
317-
parent_tag !== 'h4' &&
318-
parent_tag !== 'h5' &&
319-
parent_tag !== 'h6'
320-
);
321-
322-
case 'rp':
323-
case 'rt':
324-
return implied_end_tags.indexOf(parent_tag) === -1;
325-
326-
case 'body':
327-
case 'caption':
328-
case 'col':
329-
case 'colgroup':
330-
case 'frameset':
331-
case 'frame':
332-
case 'head':
333-
case 'html':
334-
case 'tbody':
335-
case 'td':
336-
case 'tfoot':
337-
case 'th':
338-
case 'thead':
339-
case 'tr':
340-
// These tags are only valid with a few parents that have special child
341-
// parsing rules -- if we're down here, then none of those matched and
342-
// so we allow it only if we don't know what the parent is, as all other
343-
// cases are invalid.
344-
return parent_tag == null;
345-
}
346-
347-
return true;
348-
}
349-
350233
/**
351234
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
352235
*/

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,12 @@ const template_visitors = {
12071207
inner_context.visit(node, state);
12081208
}
12091209

1210+
if (context.state.options.dev) {
1211+
context.state.template.push(
1212+
t_statement(b.stmt(b.call('$.push_element', b.literal(node.name), b.id('$$payload'))))
1213+
);
1214+
}
1215+
12101216
process_children(trimmed, node, inner_context);
12111217

12121218
if (body_expression !== null) {
@@ -1239,6 +1245,9 @@ const template_visitors = {
12391245
if (!VoidElements.includes(node.name) && metadata.namespace !== 'foreign') {
12401246
context.state.template.push(t_string(`</${node.name}>`));
12411247
}
1248+
if (context.state.options.dev) {
1249+
context.state.template.push(t_statement(b.stmt(b.call('$.pop_element'))));
1250+
}
12421251
},
12431252
SvelteElement(node, context) {
12441253
let tag = /** @type {import('estree').Expression} */ (context.visit(node.tag));
@@ -1281,6 +1290,12 @@ const template_visitors = {
12811290

12821291
serialize_element_attributes(node, inner_context);
12831292

1293+
if (context.state.options.dev) {
1294+
context.state.template.push(
1295+
t_statement(b.stmt(b.call('$.push_element', tag, b.id('$$payload'))))
1296+
);
1297+
}
1298+
12841299
context.state.template.push(
12851300
t_statement(
12861301
b.if(
@@ -1304,6 +1319,9 @@ const template_visitors = {
13041319
),
13051320
t_expression(anchor_id)
13061321
);
1322+
if (context.state.options.dev) {
1323+
context.state.template.push(t_statement(b.stmt(b.call('$.pop_element'))));
1324+
}
13071325
},
13081326
EachBlock(node, context) {
13091327
const state = context.state;

0 commit comments

Comments
 (0)