diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 7bfdaf1850..23308391db 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -1,7 +1,12 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_template_chunk } from './shared/utils.js'; +import {build_template_chunk, get_expression_id} from './shared/utils.js'; +import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js'; +import {build_attribute_value} from "./shared/element.js"; +import { visit_event_attribute } from './shared/events.js'; +import {normalize_attribute} from "../../../../../utils.js"; +import {is_ignored} from "../../../../state.js"; /** * @param {AST.TitleElement} node @@ -15,10 +20,46 @@ export function TitleElement(node, context) { ); const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value)); - if (has_state) { context.state.update.push(statement); } else { context.state.init.push(statement); } + + // TODO: is this the right approach? + + /** @type {Array} */ + const attributes = []; + for (const attribute of node.attributes) { + switch (attribute.type) { + case 'Attribute': + attributes.push(attribute); + break; + } + } + + const node_id = {"type": "Identifier", "name": "title"} + + for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { + if (is_event_attribute(attribute)) { + visit_event_attribute(attribute, context); + continue; + } + + const name = normalize_attribute(attribute.name); + const { value, has_state } = build_attribute_value( + attribute.value, + context, + (value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value) + ); + + const update = b.call( + '$.set_attribute', + false, + b.literal(name), + value, + is_ignored(node, 'hydration_attribute_changed') && b.true + ); + (has_state ? context.state.update : context.state.init).push(b.stmt(update)); + } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js index c42df4c646..c94eee8454 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js @@ -2,6 +2,7 @@ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; import { process_children, build_template } from './shared/utils.js'; +import { build_element_attributes } from "./shared/element.js"; /** * @param {AST.TitleElement} node @@ -9,7 +10,9 @@ import { process_children, build_template } from './shared/utils.js'; */ export function TitleElement(node, context) { // title is guaranteed to contain only text/expression tag children - const template = [b.literal('')]; + const template = [b.literal('<title')]; + build_element_attributes(node, { ...context, state: { ...context.state, template } }); + template.push(b.literal('>')); process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } }); template.push(b.literal('')); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index b0bcb8fd6f..50ad3db2ab 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -25,9 +25,9 @@ import { escape_html } from '../../../../../../escaping.js'; const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; /** - * Writes the output to the template output. Some elements may have attributes on them that require the + * Writes the output to the template output. Some elements may have attributes on them that require * their output to be the child content instead. In this case, an object is returned. - * @param {AST.RegularElement | AST.SvelteElement} node + * @param {AST.RegularElement | AST.SvelteElement | AST.TitleElement} node * @param {import('zimmerframe').Context} context */ export function build_element_attributes(node, context) { @@ -203,7 +203,7 @@ export function build_element_attributes(node, context) { if (has_spread) { build_element_spread_attributes(node, attributes, style_directives, class_directives, context); } else { - const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null; + const css_hash = node.type !== 'TitleElement' && node.metadata.scoped ? context.state.analysis.css.hash : null; for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { const name = get_attribute_name(node, attribute); @@ -273,12 +273,12 @@ export function build_element_attributes(node, context) { } /** - * @param {AST.RegularElement | AST.SvelteElement} element + * @param {AST.RegularElement | AST.SvelteElement | AST.TitleElement} element * @param {AST.Attribute} attribute */ function get_attribute_name(element, attribute) { let name = attribute.name; - if (!element.metadata.svg && !element.metadata.mathml) { + if (element.type !== 'TitleElement' && !element.metadata.svg && !element.metadata.mathml) { name = name.toLowerCase(); // don't lookup boolean aliases here, the server runtime function does only // check for the lowercase variants of boolean attributes @@ -288,7 +288,7 @@ function get_attribute_name(element, attribute) { /** * - * @param {AST.RegularElement | AST.SvelteElement} element + * @param {AST.RegularElement | AST.SvelteElement | AST.TitleElement} element * @param {Array} attributes * @param {AST.StyleDirective[]} style_directives * @param {AST.ClassDirective[]} class_directives @@ -330,10 +330,12 @@ function build_element_spread_attributes( styles = b.object(properties); } - if (element.metadata.svg || element.metadata.mathml) { - flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE; - } else if (is_custom_element_node(element)) { - flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE; + if (element.type !== 'TitleElement') { + if (element.metadata.svg || element.metadata.mathml) { + flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE; + } else if (is_custom_element_node(element)) { + flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE; + } } const object = b.object( @@ -353,7 +355,7 @@ function build_element_spread_attributes( ); const css_hash = - element.metadata.scoped && context.state.analysis.css.hash + element.type !== 'TitleElement' && element.metadata.scoped && context.state.analysis.css.hash ? b.literal(context.state.analysis.css.hash) : b.null; diff --git a/packages/svelte/tests/runtime-legacy/samples/head-title-attributes/_config.js b/packages/svelte/tests/runtime-legacy/samples/head-title-attributes/_config.js new file mode 100644 index 0000000000..4f8ee14ba0 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/head-title-attributes/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, component, window }) { + assert.equal(window.document.title, 'Foo'); + + const elems = window.document.getElementsByTagName('title'); + assert.equal(elems.length, 1); + const attrValue = elems[0].getAttribute('aria-live'); + assert.equal(attrValue, 'assertive'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/head-title-attributes/main.svelte b/packages/svelte/tests/runtime-legacy/samples/head-title-attributes/main.svelte new file mode 100644 index 0000000000..a02079597a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/head-title-attributes/main.svelte @@ -0,0 +1,3 @@ + + Foo + diff --git a/packages/svelte/tests/server-side-rendering/samples/head-title-attributes/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/head-title-attributes/_expected_head.html new file mode 100644 index 0000000000..ad1751f8e6 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/head-title-attributes/_expected_head.html @@ -0,0 +1 @@ +Foo diff --git a/packages/svelte/tests/server-side-rendering/samples/head-title-attributes/main.svelte b/packages/svelte/tests/server-side-rendering/samples/head-title-attributes/main.svelte new file mode 100644 index 0000000000..a02079597a --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/head-title-attributes/main.svelte @@ -0,0 +1,3 @@ + + Foo + diff --git a/packages/svelte/tests/server-side-rendering/samples/head-title-spread-attributes/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/head-title-spread-attributes/_expected_head.html new file mode 100644 index 0000000000..eada47b7a0 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/head-title-spread-attributes/_expected_head.html @@ -0,0 +1 @@ +Foo diff --git a/packages/svelte/tests/server-side-rendering/samples/head-title-spread-attributes/main.svelte b/packages/svelte/tests/server-side-rendering/samples/head-title-spread-attributes/main.svelte new file mode 100644 index 0000000000..671b7ec897 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/head-title-spread-attributes/main.svelte @@ -0,0 +1,11 @@ + + + + Foo +