Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lemon-paws-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: add `<svelte:html>` element
11 changes: 11 additions & 0 deletions documentation/docs/05-special-elements/04-svelte-html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: <svelte:html>
---

```svelte
<svelte:html attribute={value} onevent={handler} />
```

Similarly to `<svelte:body>`, this element allows you to add properties and listeners to events on `document.documentElement`. This is useful for attributes such as `lang` which influence how the browser interprets the content.

As with `<svelte:window>`, `<svelte:document>` and `<svelte:body>`, this element may only appear the top level of your component and must never be inside a block or element.
6 changes: 6 additions & 0 deletions documentation/docs/98-reference/.generated/compile-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,12 @@ Invalid component definition — must be an `{expression}`
`<svelte:head>` cannot have attributes nor directives
```

### svelte_html_illegal_attribute

```
`<svelte:html>` can only have regular attributes
```

### svelte_meta_duplicate

```
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/elements.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1998,6 +1998,7 @@ export interface SvelteHTMLElements {
'svelte:window': SvelteWindowAttributes;
'svelte:document': SvelteDocumentAttributes;
'svelte:body': HTMLAttributes<HTMLElement>;
'svelte:html': HTMLAttributes<HTMLElement>;
'svelte:fragment': { slot?: string };
'svelte:options': {
customElement?:
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/messages/compile-errors/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ HTML restricts where certain elements can appear. In case of a violation the bro

> `<svelte:head>` cannot have attributes nor directives

## svelte_html_illegal_attribute

> `<svelte:html>` can only have regular attributes

## svelte_meta_duplicate

> A component can only have one `<%name%>` element
Expand Down
9 changes: 9 additions & 0 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,15 @@ export function svelte_head_illegal_attribute(node) {
e(node, "svelte_head_illegal_attribute", "`<svelte:head>` cannot have attributes nor directives");
}

/**
* `<svelte:html>` can only have regular attributes
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_html_illegal_attribute(node) {
e(node, "svelte_html_illegal_attribute", "`<svelte:html>` can only have regular attributes");
}

/**
* A component can only have one `<%name%>` element
* @param {null | number | NodeLike} node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const root_only_meta_tags = new Map([
['svelte:head', 'SvelteHead'],
['svelte:options', 'SvelteOptions'],
['svelte:window', 'SvelteWindow'],
['svelte:html', 'SvelteHTML'],
['svelte:document', 'SvelteDocument'],
['svelte:body', 'SvelteBody']
]);
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteHTML } from './visitors/SvelteHTML.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
Expand Down Expand Up @@ -169,6 +170,7 @@ const visitors = {
SvelteElement,
SvelteFragment,
SvelteHead,
SvelteHTML,
SvelteSelf,
SvelteWindow,
TaggedTemplateExpression,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';

/**
* @param {AST.SvelteHTML} node
* @param {Context} context
*/
export function SvelteHTML(node, context) {
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') {
e.svelte_html_illegal_attribute(attribute);
}
}

if (node.fragment.nodes.length > 0) {
e.svelte_meta_invalid_content(node, node.name);
}

context.next();
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteHTML } from './visitors/SvelteHTML.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { TitleElement } from './visitors/TitleElement.js';
Expand Down Expand Up @@ -123,6 +124,7 @@ const visitors = {
SvelteElement,
SvelteFragment,
SvelteHead,
SvelteHTML,
SvelteSelf,
SvelteWindow,
TitleElement,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/** @import { ExpressionStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_dom_property, normalize_attribute } from '../../../../../utils.js';
import { is_ignored } from '../../../../state.js';
import { is_event_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { build_attribute_value } from './shared/element.js';
import { visit_event_attribute } from './shared/events.js';

/**
* @param {AST.SvelteHTML} element
* @param {ComponentContext} context
*/
export function SvelteHTML(element, context) {
const node_id = b.id('$.document.documentElement');

for (const attribute of element.attributes) {
if (attribute.type === 'Attribute') {
if (is_event_attribute(attribute)) {
visit_event_attribute(attribute, context);
} else {
const name = normalize_attribute(attribute.name);
const { value, has_state } = build_attribute_value(attribute.value, context);

/** @type {ExpressionStatement} */
let update;

if (name === 'class') {
update = b.stmt(b.call('$.set_class', node_id, value));
} else if (is_dom_property(name)) {
update = b.stmt(b.assignment('=', b.member(node_id, name), value));
} else {
update = b.stmt(
b.call(
'$.set_attribute',
node_id,
b.literal(name),
value,
is_ignored(element, 'hydration_attribute_changed') && b.true
)
);
}

if (has_state) {
context.state.update.push(update);
} else {
context.state.init.push(update);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ export function visit_event_attribute(node, context) {

const type = /** @type {SvelteNode} */ (context.path.at(-1)).type;

if (type === 'SvelteDocument' || type === 'SvelteWindow' || type === 'SvelteBody') {
if (
type === 'SvelteDocument' ||
type === 'SvelteWindow' ||
type === 'SvelteBody' ||
type === 'SvelteHTML'
) {
// These nodes are above the component tree, and its events should run parent first
context.state.init.push(statement);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import * as b from '../../../../../utils/builders.js';

/**
*
* Puts all event listeners onto the given element
* @param {AST.SvelteBody | AST.SvelteDocument | AST.SvelteWindow} node
* @param {string} id
* @param {ComponentContext} context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteHTML } from './visitors/SvelteHTML.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
Expand Down Expand Up @@ -74,6 +75,7 @@ const template_visitors = {
SvelteElement,
SvelteFragment,
SvelteHead,
SvelteHTML,
SvelteSelf,
TitleElement
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/** @import { Property } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { normalize_attribute } from '../../../../../utils.js';
import { is_event_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { build_attribute_value } from './shared/utils.js';

/**
* @param {AST.SvelteHTML} element
* @param {ComponentContext} context
*/
export function SvelteHTML(element, context) {
/** @type {Property[]} */
const attributes = [];

for (const attribute of element.attributes) {
if (attribute.type === 'Attribute' && !is_event_attribute(attribute)) {
const name = normalize_attribute(attribute.name);
const value = build_attribute_value(attribute.value, context);
attributes.push(b.init(name, value));
}
}

context.state.template.push(
b.stmt(b.call('$.svelte_html', b.id('$$payload'), b.object(attributes)))
);
}
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/phases/3-transform/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export function clean_nodes(
node.type === 'ConstTag' ||
node.type === 'DebugTag' ||
node.type === 'SvelteBody' ||
node.type === 'SvelteHTML' ||
node.type === 'SvelteWindow' ||
node.type === 'SvelteDocument' ||
node.type === 'SvelteHead' ||
Expand Down
6 changes: 6 additions & 0 deletions packages/svelte/src/compiler/types/template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ export namespace AST {
};
}

export interface SvelteHTML extends BaseElement {
type: 'SvelteHTML';
name: 'svelte:html';
}

export interface SvelteBody extends BaseElement {
type: 'SvelteBody';
name: 'svelte:body';
Expand Down Expand Up @@ -491,6 +496,7 @@ export type ElementLike =
| AST.TitleElement
| AST.SlotElement
| AST.RegularElement
| AST.SvelteHTML
| AST.SvelteBody
| AST.SvelteComponent
| AST.SvelteDocument
Expand Down
13 changes: 13 additions & 0 deletions packages/svelte/src/internal/server/blocks/svelte-html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** @import { Payload } from '#server' */

import { escape } from '..';

/**
* @param {Payload} payload
* @param {Record<string, string>} attributes
*/
export function svelte_html(payload, attributes) {
for (const name in attributes) {
payload.htmlAttributes.set(name, escape(attributes[name], true));
}
}
17 changes: 14 additions & 3 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ const RAW_TEXT_ELEMENTS = ['textarea', 'script', 'style', 'title'];
* @param {Payload} to_copy
* @returns {Payload}
*/
export function copy_payload({ out, css, head }) {
export function copy_payload({ out, htmlAttributes, css, head }) {
return {
out,
htmlAttributes: new Map(htmlAttributes),
css: new Set(css),
head: {
title: head.title,
Expand Down Expand Up @@ -96,7 +97,12 @@ export let on_destroy = [];
*/
export function render(component, options = {}) {
/** @type {Payload} */
const payload = { out: '', css: new Set(), head: { title: '', out: '' } };
const payload = {
out: '',
htmlAttributes: new Map(),
css: new Set(),
head: { title: '', out: '' }
};

const prev_on_destroy = on_destroy;
on_destroy = [];
Expand Down Expand Up @@ -138,7 +144,10 @@ export function render(component, options = {}) {
return {
head,
html: payload.out,
body: payload.out
body: payload.out,
htmlAttributes: [...payload.htmlAttributes]
.map(([name, value]) => `${name}="${value}"`)
.join(' ')
};
}

Expand Down Expand Up @@ -527,6 +536,8 @@ export { attr };

export { html } from './blocks/html.js';

export { svelte_html } from './blocks/svelte-html.js';

export { push, pop } from './context.js';

export { push_element, pop_element } from './dev.js';
Expand Down
3 changes: 3 additions & 0 deletions packages/svelte/src/internal/server/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface Component {

export interface Payload {
out: string;
htmlAttributes: Map<string, string>;
css: Set<{ hash: string; code: string }>;
head: {
title: string;
Expand All @@ -27,4 +28,6 @@ export interface RenderOutput {
html: string;
/** HTML that goes somewhere into the `<body>` */
body: string;
/** Attributes that go onto the `<html>` */
htmlAttributes: string;
}
1 change: 1 addition & 0 deletions packages/svelte/svelte-html.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ declare global {
'svelte:window': HTMLProps<'svelte:window', HTMLAttributes>;
'svelte:body': HTMLProps<'svelte:body', HTMLAttributes>;
'svelte:document': HTMLProps<'svelte:document', HTMLAttributes>;
'svelte:html': HTMLProps<'svelte:html', HTMLAttributes>;
'svelte:fragment': { slot?: string };
'svelte:head': { [name: string]: any };
// don't type svelte:options, it would override the types in svelte/elements and it isn't extendable anyway
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default test({
error: {
code: 'svelte_meta_invalid_tag',
message:
'Valid `<svelte:...>` tag names are svelte:head, svelte:options, svelte:window, svelte:document, svelte:body, svelte:element, svelte:component, svelte:self or svelte:fragment',
'Valid `<svelte:...>` tag names are svelte:head, svelte:options, svelte:window, svelte:html, svelte:document, svelte:body, svelte:element, svelte:component, svelte:self or svelte:fragment',
position: [10, 32]
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { test } from '../../test';

export default test({
async test({ assert }) {
assert.deepEqual(document.documentElement.lang, 'de');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svelte:html lang="de"></svelte:html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<svelte:html foo="bar"></svelte:html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { test } from '../../test';

export default test({
htmlAttributes: 'foo="bar"'
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
let ignored;
</script>

<svelte:html foo="foo" onevent={ignored}></svelte:html>

<Nested/>
Loading
Loading