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: `
+
+
+
+
+
+ works2 |
+
+
+ works3 |
+
+
+
+
+
+
+
+ works4 |
+
+
+
+
+ works5 |
+
+
+
+
+
+`
+});
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 @@
+
+
+
+ {#each ['works2', 'works3'] as cell}
+
+ {cell} |
+
+ {/each}
+
+
+
+
+ works4 |
+
+
+
+ works5 |
+
+
+
+
+
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;
};
}