Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d1141a1

Browse filesBrowse files
feat: functional template generation (#15538)
Co-authored-by: Rich Harris <rich.harris@vercel.com>
1 parent 5a84098 commit d1141a1
Copy full SHA for d1141a1

File tree

Expand file treeCollapse file tree

61 files changed

+666
-288
lines changed
Filter options

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Dismiss banner
Expand file treeCollapse file tree

61 files changed

+666
-288
lines changed

‎.changeset/forty-llamas-unite.md

Copy file name to clipboard
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: XHTML compliance

‎.changeset/smart-boats-accept.md

Copy file name to clipboard
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: add `fragments: 'html' | 'tree'` option for wider CSP compliance

‎packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Copy file name to clipboardExpand all lines: packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+1-6Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,6 @@ export function client_component(analysis, options) {
154154
legacy_reactive_imports: [],
155155
legacy_reactive_statements: new Map(),
156156
metadata: {
157-
context: {
158-
template_needs_import_node: false,
159-
template_contains_script_tag: false
160-
},
161157
namespace: options.namespace,
162158
bound_contenteditable: false
163159
},
@@ -174,8 +170,7 @@ export function client_component(analysis, options) {
174170
update: /** @type {any} */ (null),
175171
expressions: /** @type {any} */ (null),
176172
after_update: /** @type {any} */ (null),
177-
template: /** @type {any} */ (null),
178-
locations: /** @type {any} */ (null)
173+
template: /** @type {any} */ (null)
179174
};
180175

181176
const module = /** @type {ESTree.Program} */ (

‎packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js

Copy file name to clipboardExpand all lines: packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js
+18Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+68Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/** @import { Location } from 'locate-character' */
2+
/** @import { Namespace } from '#compiler' */
3+
/** @import { ComponentClientTransformState } from '../types.js' */
4+
/** @import { Node } from './types.js' */
5+
import { TEMPLATE_USE_MATHML, TEMPLATE_USE_SVG } from '../../../../../constants.js';
6+
import { dev, locator } from '../../../../state.js';
7+
import * as b from '../../../../utils/builders.js';
8+
9+
/**
10+
* @param {Node[]} nodes
11+
*/
12+
function build_locations(nodes) {
13+
const array = b.array([]);
14+
15+
for (const node of nodes) {
16+
if (node.type !== 'element') continue;
17+
18+
const { line, column } = /** @type {Location} */ (locator(node.start));
19+
20+
const expression = b.array([b.literal(line), b.literal(column)]);
21+
const children = build_locations(node.children);
22+
23+
if (children.elements.length > 0) {
24+
expression.elements.push(children);
25+
}
26+
27+
array.elements.push(expression);
28+
}
29+
30+
return array;
31+
}
32+
33+
/**
34+
* @param {ComponentClientTransformState} state
35+
* @param {Namespace} namespace
36+
* @param {number} [flags]
37+
*/
38+
export function transform_template(state, namespace, flags = 0) {
39+
const tree = state.options.fragments === 'tree';
40+
41+
const expression = tree ? state.template.as_tree() : state.template.as_html();
42+
43+
if (tree) {
44+
if (namespace === 'svg') flags |= TEMPLATE_USE_SVG;
45+
if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML;
46+
}
47+
48+
let call = b.call(
49+
tree ? `$.from_tree` : `$.from_${namespace}`,
50+
expression,
51+
flags ? b.literal(flags) : undefined
52+
);
53+
54+
if (state.template.contains_script_tag) {
55+
call = b.call(`$.with_script`, call);
56+
}
57+
58+
if (dev) {
59+
call = b.call(
60+
'$.add_locations',
61+
call,
62+
b.member(b.id(state.analysis.name), '$.FILENAME', true),
63+
build_locations(state.template.nodes)
64+
);
65+
}
66+
67+
return call;
68+
}
+162Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/** @import { AST } from '#compiler' */
2+
/** @import { Node, Element } from './types'; */
3+
import { escape_html } from '../../../../../escaping.js';
4+
import { is_void } from '../../../../../utils.js';
5+
import * as b from '#compiler/builders';
6+
import fix_attribute_casing from './fix-attribute-casing.js';
7+
import { regex_starts_with_newline } from '../../../patterns.js';
8+
9+
export class Template {
10+
/**
11+
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
12+
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
13+
*/
14+
contains_script_tag = false;
15+
16+
/** `true` if the HTML template needs to be instantiated with `importNode` */
17+
needs_import_node = false;
18+
19+
/** @type {Node[]} */
20+
nodes = [];
21+
22+
/** @type {Node[][]} */
23+
#stack = [this.nodes];
24+
25+
/** @type {Element | undefined} */
26+
#element;
27+
28+
#fragment = this.nodes;
29+
30+
/**
31+
* @param {string} name
32+
* @param {number} start
33+
*/
34+
push_element(name, start) {
35+
this.#element = {
36+
type: 'element',
37+
name,
38+
attributes: {},
39+
children: [],
40+
start
41+
};
42+
43+
this.#fragment.push(this.#element);
44+
45+
this.#fragment = /** @type {Element} */ (this.#element).children;
46+
this.#stack.push(this.#fragment);
47+
}
48+
49+
/** @param {string} [data] */
50+
push_comment(data) {
51+
this.#fragment.push({ type: 'comment', data });
52+
}
53+
54+
/** @param {AST.Text[]} nodes */
55+
push_text(nodes) {
56+
this.#fragment.push({ type: 'text', nodes });
57+
}
58+
59+
pop_element() {
60+
this.#stack.pop();
61+
this.#fragment = /** @type {Node[]} */ (this.#stack.at(-1));
62+
}
63+
64+
/**
65+
* @param {string} key
66+
* @param {string | undefined} value
67+
*/
68+
set_prop(key, value) {
69+
/** @type {Element} */ (this.#element).attributes[key] = value;
70+
}
71+
72+
as_html() {
73+
return b.template([b.quasi(this.nodes.map(stringify).join(''), true)], []);
74+
}
75+
76+
as_tree() {
77+
// if the first item is a comment we need to add another comment for effect.start
78+
if (this.nodes[0].type === 'comment') {
79+
this.nodes.unshift({ type: 'comment', data: undefined });
80+
}
81+
82+
return b.array(this.nodes.map(objectify));
83+
}
84+
}
85+
86+
/**
87+
* @param {Node} item
88+
*/
89+
function stringify(item) {
90+
if (item.type === 'text') {
91+
return item.nodes.map((node) => node.raw).join('');
92+
}
93+
94+
if (item.type === 'comment') {
95+
return item.data ? `<!--${item.data}-->` : '<!>';
96+
}
97+
98+
let str = `<${item.name}`;
99+
100+
for (const key in item.attributes) {
101+
const value = item.attributes[key];
102+
103+
str += ` ${key}`;
104+
if (value !== undefined) str += `="${escape_html(value, true)}"`;
105+
}
106+
107+
if (is_void(item.name)) {
108+
str += '/>'; // XHTML compliance
109+
} else {
110+
str += `>`;
111+
str += item.children.map(stringify).join('');
112+
str += `</${item.name}>`;
113+
}
114+
115+
return str;
116+
}
117+
118+
/** @param {Node} item */
119+
function objectify(item) {
120+
if (item.type === 'text') {
121+
return b.literal(item.nodes.map((node) => node.data).join(''));
122+
}
123+
124+
if (item.type === 'comment') {
125+
return item.data ? b.array([b.literal(`// ${item.data}`)]) : null;
126+
}
127+
128+
const element = b.array([b.literal(item.name)]);
129+
130+
const attributes = b.object([]);
131+
132+
for (const key in item.attributes) {
133+
const value = item.attributes[key];
134+
135+
attributes.properties.push(
136+
b.prop(
137+
'init',
138+
b.key(fix_attribute_casing(key)),
139+
value === undefined ? b.void0 : b.literal(value)
140+
)
141+
);
142+
}
143+
144+
if (attributes.properties.length > 0 || item.children.length > 0) {
145+
element.elements.push(attributes.properties.length > 0 ? attributes : b.null);
146+
}
147+
148+
if (item.children.length > 0) {
149+
const children = item.children.map(objectify);
150+
element.elements.push(...children);
151+
152+
// special case — strip leading newline from `<pre>` and `<textarea>`
153+
if (item.name === 'pre' || item.name === 'textarea') {
154+
const first = children[0];
155+
if (first?.type === 'Literal') {
156+
first.value = /** @type {string} */ (first.value).replace(regex_starts_with_newline, '');
157+
}
158+
}
159+
}
160+
161+
return element;
162+
}
+22Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { AST } from '#compiler';
2+
3+
export interface Element {
4+
type: 'element';
5+
name: string;
6+
attributes: Record<string, string | undefined>;
7+
children: Node[];
8+
/** used for populating __svelte_meta */
9+
start: number;
10+
}
11+
12+
export interface Text {
13+
type: 'text';
14+
nodes: AST.Text[];
15+
}
16+
17+
export interface Comment {
18+
type: 'comment';
19+
data: string | undefined;
20+
}
21+
22+
export type Node = Element | Text | Comment;

‎packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

Copy file name to clipboardExpand all lines: packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
+3-20Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@ import type {
33
Statement,
44
LabeledStatement,
55
Identifier,
6-
PrivateIdentifier,
76
Expression,
87
AssignmentExpression,
98
UpdateExpression,
109
VariableDeclaration
1110
} from 'estree';
12-
import type { AST, Namespace, StateField, ValidatedCompileOptions } from '#compiler';
11+
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
1312
import type { TransformState } from '../types.js';
1413
import type { ComponentAnalysis } from '../../types.js';
15-
import type { SourceLocation } from '#shared';
14+
import type { Template } from './transform-template/template.js';
1615

1716
export interface ClientTransformState extends TransformState {
1817
/**
@@ -53,26 +52,10 @@ export interface ComponentClientTransformState extends ClientTransformState {
5352
/** Expressions used inside the render effect */
5453
readonly expressions: Expression[];
5554
/** The HTML template string */
56-
readonly template: Array<string | Expression>;
57-
readonly locations: SourceLocation[];
55+
readonly template: Template;
5856
readonly metadata: {
5957
namespace: Namespace;
6058
bound_contenteditable: boolean;
61-
/**
62-
* Stuff that is set within the children of one `Fragment` visitor that is relevant
63-
* to said fragment. Shouldn't be destructured or otherwise spread unless inside the
64-
* `Fragment` visitor to keep the object reference intact (it's also nested
65-
* within `metadata` for this reason).
66-
*/
67-
context: {
68-
/** `true` if the HTML template needs to be instantiated with `importNode` */
69-
template_needs_import_node: boolean;
70-
/**
71-
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
72-
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
73-
*/
74-
template_contains_script_tag: boolean;
75-
};
7659
};
7760
readonly preserve_whitespace: boolean;
7861

‎packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js

Copy file name to clipboardExpand all lines: packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { get_value } from './shared/declarations.js';
1111
* @param {ComponentContext} context
1212
*/
1313
export function AwaitBlock(node, context) {
14-
context.state.template.push('<!>');
14+
context.state.template.push_comment();
1515

1616
// Visit {#await <expression>} first to ensure that scopes are in the correct order
1717
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression)));

‎packages/svelte/src/compiler/phases/3-transform/client/visitors/Comment.js

Copy file name to clipboardExpand all lines: packages/svelte/src/compiler/phases/3-transform/client/visitors/Comment.js
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
*/
88
export function Comment(node, context) {
99
// We'll only get here if comments are not filtered out, which they are unless preserveComments is true
10-
context.state.template.push(`<!--${node.data}-->`);
10+
context.state.template.push_comment(node.data);
1111
}

‎packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js

Copy file name to clipboardExpand all lines: packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function EachBlock(node, context) {
3232
);
3333

3434
if (!each_node_meta.is_controlled) {
35-
context.state.template.push('<!>');
35+
context.state.template.push_comment();
3636
}
3737

3838
let flags = 0;

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.