diff --git a/.changeset/full-doors-stand.md b/.changeset/full-doors-stand.md new file mode 100644 index 0000000000..15a4897b36 --- /dev/null +++ b/.changeset/full-doors-stand.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always lowercase HTML elements, for XHTML compliance diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js index 8f7f8a1f43..f546ce4962 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js @@ -30,13 +30,15 @@ export class Template { /** * @param {string} name * @param {number} start + * @param {boolean} is_html */ - push_element(name, start) { + push_element(name, start, is_html) { this.#element = { type: 'element', name, attributes: {}, children: [], + is_html, start }; @@ -100,7 +102,7 @@ function stringify(item) { for (const key in item.attributes) { const value = item.attributes[key]; - str += ` ${key}`; + str += ` ${item.is_html ? key.toLowerCase() : key}`; if (value !== undefined) str += `="${escape_html(value, true)}"`; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/types.d.ts index 1555e10ef0..2ccb4dc5c7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/types.d.ts @@ -5,6 +5,7 @@ export interface Element { name: string; attributes: Record; children: Node[]; + is_html: boolean; /** used for populating __svelte_meta */ start: number; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 7469dcda5b..0579d80b74 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -38,9 +38,11 @@ import { TEMPLATE_FRAGMENT } from '../../../../../constants.js'; * @param {ComponentContext} context */ export function RegularElement(node, context) { - context.state.template.push_element(node.name, node.start); + const is_html = context.state.metadata.namespace === 'html' && node.name !== 'svg'; + const name = is_html ? node.name.toLowerCase() : node.name; + context.state.template.push_element(name, node.start, is_html); - if (node.name === 'noscript') { + if (name === 'noscript') { context.state.template.pop_element(); return; } @@ -53,9 +55,9 @@ export function RegularElement(node, context) { // Therefore we need to use importNode instead, which doesn't have this caveat. // Additionally, Webkit browsers need importNode for video elements for autoplay // to work correctly. - context.state.template.needs_import_node ||= node.name === 'video' || is_custom_element; + context.state.template.needs_import_node ||= name === 'video' || is_custom_element; - context.state.template.contains_script_tag ||= node.name === 'script'; + context.state.template.contains_script_tag ||= name === 'script'; /** @type {Array} */ const attributes = []; @@ -161,7 +163,7 @@ export function RegularElement(node, context) { } } - if (node.name === 'input') { + if (name === 'input') { const has_value_attribute = attributes.some( (attribute) => attribute.type === 'Attribute' && @@ -190,7 +192,7 @@ export function RegularElement(node, context) { } } - if (node.name === 'textarea') { + if (name === 'textarea') { const attribute = lookup.get('value') ?? lookup.get('checked'); const needs_content_reset = attribute && !is_text_attribute(attribute); @@ -206,10 +208,7 @@ export function RegularElement(node, context) { /** If true, needs `__value` for inputs */ const needs_special_value_handling = - node.name === 'option' || - node.name === 'select' || - bindings.has('group') || - bindings.has('checked'); + name === 'option' || name === 'select' || bindings.has('group') || bindings.has('checked'); if (has_spread) { build_attribute_effect( @@ -258,7 +257,6 @@ export function RegularElement(node, context) { let { value } = build_attribute_value(attribute.value, context); context.state.init.push(b.stmt(b.call('$.autofocus', node_id, value))); } else if (name === 'class') { - const is_html = context.state.metadata.namespace === 'html' && node.name !== 'svg'; build_set_class(node, node_id, attribute, class_directives, context, is_html); } else if (name === 'style') { build_set_style(node_id, attribute, style_directives, context); @@ -279,7 +277,7 @@ export function RegularElement(node, context) { } if ( - is_load_error_element(node.name) && + is_load_error_element(name) && (has_spread || has_use || lookup.has('onload') || lookup.has('onerror')) ) { context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id))); @@ -307,8 +305,7 @@ export function RegularElement(node, context) { ...context.state, metadata, scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)), - preserve_whitespace: - context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea' + preserve_whitespace: context.state.preserve_whitespace || name === 'pre' || name === 'textarea' }; const { hoisted, trimmed } = clean_nodes( @@ -317,7 +314,7 @@ export function RegularElement(node, context) { context.path, state.metadata.namespace, state, - node.name === 'script' || state.preserve_whitespace, + name === 'script' || state.preserve_whitespace, state.options.preserveComments ); @@ -363,7 +360,7 @@ export function RegularElement(node, context) { context.state.template.push_comment(); // Create a separate template for the rich content - const template_name = context.state.scope.root.unique(`${node.name}_content`); + const template_name = context.state.scope.root.unique(`${name}_content`); const fragment_id = b.id(context.state.scope.generate('fragment')); const anchor_id = b.id(context.state.scope.generate('anchor')); @@ -414,7 +411,7 @@ export function RegularElement(node, context) { // The same applies if it's a `