diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 11a9de0951..6cc51b034b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -150,7 +150,8 @@ export default function element(parser) { svg: false, mathml: false, scoped: false, - has_spread: false + has_spread: false, + auto_opens: null }, parent: null } diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 5dc893ced9..c4b0d2f6fb 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -622,15 +622,23 @@ const validation = { ancestor.type === 'RegularElement' && ancestor.name === context.state.parent_element ) { + past_parent = true; + if (!is_tag_valid_with_parent(node.name, context.state.parent_element)) { - if (only_warn) { + if (node.name === 'tr' && ancestor.name === 'table') { + // Handle this and do what the browser does: auto-open a prior to the first child + ancestor.metadata.auto_opens = ''; + // Note that we don't properly handle cases like , but that's a rare edge case + } else if (node.name === 'td' && ancestor.name === 'table') { + // Handle this and do what the browser does: auto-open a prior to the first child + ancestor.metadata.auto_opens = ''; + // Note that we don't properly handle cases like
, but that's a rare edge case + } else if (only_warn) { w.node_invalid_placement_ssr(node, `<${node.name}>`, context.state.parent_element); } else { e.node_invalid_placement(node, `<${node.name}>`, context.state.parent_element); } } - - past_parent = true; } } else if (ancestor.type === 'RegularElement') { if (!is_tag_valid_with_ancestor(node.name, ancestor.name)) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 3e8fff654f..feb93ac54a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -2196,6 +2196,10 @@ export const template_visitors = { context.state.template.push('>'); + if (node.metadata.auto_opens !== null) { + context.state.template.push(node.metadata.auto_opens); + } + /** @type {SourceLocation[]} */ const child_locations = []; @@ -2248,10 +2252,22 @@ export const template_visitors = { child_state.init.push(b.stmt(b.call('$.reset', arg))); } - process_children(trimmed, () => b.call('$.child', arg), true, { - ...context, - state: child_state - }); + process_children( + trimmed, + // TODO: this doesn't work when the table is a sibling, as the expression is then not used + () => { + let call = b.call('$.child', arg); + for (let i = (node.metadata.auto_opens?.split('<').length ?? 1) - 1; i > 0; i--) { + call = b.call('$.child', call); + } + return call; + }, + true, + { + ...context, + state: child_state + } + ); if (needs_reset) { child_state.init.push(b.stmt(b.call('$.reset', context.state.node))); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js index 622e8138a2..67d7cb71c9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js @@ -39,6 +39,10 @@ export function RegularElement(node, context) { return; } + if (node.metadata.auto_opens !== null) { + context.state.template.push(b.literal(node.metadata.auto_opens)); + } + const { hoisted, trimmed } = clean_nodes( node, node.fragment.nodes, diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index ec35a08227..300838f92b 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -303,7 +303,10 @@ export interface RegularElement extends BaseElement { mathml: boolean; /** `true` if contains a SpreadAttribute */ has_spread: boolean; + /** `true` if should get a hash on the `class` attribute */ scoped: boolean; + /** Contains a string of the tag(s) that are implicitly opened after this element */ + auto_opens: string | null; }; } diff --git a/packages/svelte/tests/runtime-runes/samples/invalid-html-table-autorepair/_config.js b/packages/svelte/tests/runtime-runes/samples/invalid-html-table-autorepair/_config.js new file mode 100644 index 0000000000..7fd3ad36aa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/invalid-html-table-autorepair/_config.js @@ -0,0 +1,57 @@ +import { test } from '../../test'; + +let console_error = console.error; + +/** + * @type {any[]} + */ +const log = []; + +export default test({ + solo: true, + compileOptions: { + dev: true // enable validation to ensure it doesn't throw + }, + + html: ` + + + + + + +
works1
+ + + + + + + + + + +
works2
works3
+ + + + + + + + + + + + +
works4
works5
+ + + + + + + +
works6
+` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/invalid-html-table-autorepair/main.svelte b/packages/svelte/tests/runtime-runes/samples/invalid-html-table-autorepair/main.svelte new file mode 100644 index 0000000000..0465cc2719 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/invalid-html-table-autorepair/main.svelte @@ -0,0 +1,28 @@ + + + + +
works1
+ + + {#each ['works2', 'works3'] as cell} + + + + {/each} +
{cell}
+ + + + + + + + + + +
works4
works5
+ + + +
works6
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index b393174e0f..5361e0da4e 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1728,7 +1728,10 @@ declare module 'svelte/compiler' { mathml: boolean; /** `true` if contains a SpreadAttribute */ has_spread: boolean; + /** `true` if should get a hash on the `class` attribute */ scoped: boolean; + /** Contains a string of the tag(s) that are implicitly opened after this element */ + auto_opens: string | null; }; }