pull/15983/merge
Joe Schafer 4 months ago committed by GitHub
commit b9e11d1037
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
Allow attributes on the title element.

@ -1082,12 +1082,6 @@ Expected a valid element or component name. Components must have a valid variabl
A `<textarea>` can have either a value attribute or (equivalently) child content, but not both A `<textarea>` can have either a value attribute or (equivalently) child content, but not both
``` ```
### title_illegal_attribute
```
`<title>` cannot have attributes nor directives
```
### title_invalid_content ### title_invalid_content
``` ```

@ -398,10 +398,6 @@ See https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-ele
> A `<textarea>` can have either a value attribute or (equivalently) child content, but not both > A `<textarea>` can have either a value attribute or (equivalently) child content, but not both
## title_illegal_attribute
> `<title>` cannot have attributes nor directives
## title_invalid_content ## title_invalid_content
> `<title>` can only contain text and {tags} > `<title>` can only contain text and {tags}

@ -1573,15 +1573,6 @@ export function textarea_invalid_content(node) {
e(node, 'textarea_invalid_content', `A \`<textarea>\` can have either a value attribute or (equivalently) child content, but not both\nhttps://svelte.dev/e/textarea_invalid_content`); e(node, 'textarea_invalid_content', `A \`<textarea>\` can have either a value attribute or (equivalently) child content, but not both\nhttps://svelte.dev/e/textarea_invalid_content`);
} }
/**
* `<title>` cannot have attributes nor directives
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function title_illegal_attribute(node) {
e(node, 'title_illegal_attribute', `\`<title>\` cannot have attributes nor directives\nhttps://svelte.dev/e/title_illegal_attribute`);
}
/** /**
* `<title>` can only contain text and {tags} * `<title>` can only contain text and {tags}
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
@ -1647,4 +1638,4 @@ export function unterminated_string_constant(node) {
*/ */
export function void_element_invalid_content(node) { export function void_element_invalid_content(node) {
e(node, 'void_element_invalid_content', `Void elements cannot have children or closing tags\nhttps://svelte.dev/e/void_element_invalid_content`); e(node, 'void_element_invalid_content', `Void elements cannot have children or closing tags\nhttps://svelte.dev/e/void_element_invalid_content`);
} }

@ -7,10 +7,6 @@ import * as e from '../../../errors.js';
* @param {Context} context * @param {Context} context
*/ */
export function TitleElement(node, context) { export function TitleElement(node, context) {
for (const attribute of node.attributes) {
e.title_illegal_attribute(attribute);
}
for (const child of node.fragment.nodes) { for (const child of node.fragment.nodes) {
if (child.type !== 'Text' && child.type !== 'ExpressionTag') { if (child.type !== 'Text' && child.type !== 'ExpressionTag') {
e.title_invalid_content(child); e.title_invalid_content(child);

@ -1,7 +1,12 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders'; 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 * @param {AST.TitleElement} node
@ -15,10 +20,46 @@ export function TitleElement(node, context) {
); );
const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value)); const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value));
if (has_state) { if (has_state) {
context.state.update.push(statement); context.state.update.push(statement);
} else { } else {
context.state.init.push(statement); 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 { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { process_children, build_template } from './shared/utils.js'; import { process_children, build_template } from './shared/utils.js';
import { build_element_attributes } from "./shared/element.js";
/** /**
* @param {AST.TitleElement} node * @param {AST.TitleElement} node
@ -9,7 +10,9 @@ import { process_children, build_template } from './shared/utils.js';
*/ */
export function TitleElement(node, context) { export function TitleElement(node, context) {
// title is guaranteed to contain only text/expression tag children // 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 } }); process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } });
template.push(b.literal('</title>')); template.push(b.literal('</title>'));

@ -25,9 +25,9 @@ import { escape_html } from '../../../../../../escaping.js';
const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; 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. * 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 * @param {import('zimmerframe').Context<AST.SvelteNode, ComponentServerTransformState>} context
*/ */
export function build_element_attributes(node, context) { export function build_element_attributes(node, context) {
@ -203,7 +203,7 @@ export function build_element_attributes(node, context) {
if (has_spread) { if (has_spread) {
build_element_spread_attributes(node, attributes, style_directives, class_directives, context); build_element_spread_attributes(node, attributes, style_directives, class_directives, context);
} else { } 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)) { for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
const name = get_attribute_name(node, attribute); 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 * @param {AST.Attribute} attribute
*/ */
function get_attribute_name(element, attribute) { function get_attribute_name(element, attribute) {
let name = attribute.name; let name = attribute.name;
if (!element.metadata.svg && !element.metadata.mathml) { if (element.type !== 'TitleElement' && !element.metadata.svg && !element.metadata.mathml) {
name = name.toLowerCase(); name = name.toLowerCase();
// don't lookup boolean aliases here, the server runtime function does only // don't lookup boolean aliases here, the server runtime function does only
// check for the lowercase variants of boolean attributes // 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 {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {AST.StyleDirective[]} style_directives * @param {AST.StyleDirective[]} style_directives
* @param {AST.ClassDirective[]} class_directives * @param {AST.ClassDirective[]} class_directives
@ -330,10 +330,12 @@ function build_element_spread_attributes(
styles = b.object(properties); styles = b.object(properties);
} }
if (element.metadata.svg || element.metadata.mathml) { if (element.type !== 'TitleElement') {
flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE; if (element.metadata.svg || element.metadata.mathml) {
} else if (is_custom_element_node(element)) { flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE;
flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE; } else if (is_custom_element_node(element)) {
flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE;
}
} }
const object = b.object( const object = b.object(
@ -353,7 +355,7 @@ function build_element_spread_attributes(
); );
const css_hash = 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.literal(context.state.analysis.css.hash)
: b.null; : 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>

@ -1,14 +0,0 @@
[
{
"code": "title_illegal_attribute",
"message": "`<title>` cannot have attributes nor directives",
"start": {
"line": 2,
"column": 8
},
"end": {
"line": 2,
"column": 25
}
}
]
Loading…
Cancel
Save