WIP: attempt to add render title attributes

SSR is pretty straight-forward. The client side is more difficult since
the TitleElement doesn't have a node_id.
pull/15983/head
Joe Schafer 4 months ago
parent 5b736069f5
commit 874011089b

@ -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<AST.Attribute | AST.SpreadAttribute>} */
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));
}
}

@ -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('<title>')];
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('</title>'));

@ -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<AST.SvelteNode, ComponentServerTransformState>} 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<AST.Attribute | AST.SpreadAttribute>} 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;

@ -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');
}
});

@ -0,0 +1,3 @@
<svelte:head>
<title aria-live="assertive">Foo</title>
</svelte:head>

@ -0,0 +1,3 @@
<svelte:head>
<title aria-live="assertive">Foo</title>
</svelte:head>

@ -0,0 +1,11 @@
<script>
let props = {
value: 'bar',
form: 'qux',
list: 'quu',
};
</script>
<svelte:head>
<title {...props}>Foo</title>
</svelte:head>
Loading…
Cancel
Save