Skip to content

Commit dec07f0

Browse files
committed
feat: inline dom expression too
1 parent 5770063 commit dec07f0

File tree

6 files changed

+96
-39
lines changed

6 files changed

+96
-39
lines changed

packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,5 @@ export function ExpressionTag(node, context) {
1616
}
1717
}
1818

19-
const attribute_parent = context.path.find((parent) => parent.type === 'Attribute');
20-
/**
21-
* if the expression tag is part of an attribute we want to check if it's inlinable before marking
22-
* the subtree as dynamic. This is because if it's inlinable it will be inlined in the template
23-
* directly making the whole thing actually static.
24-
*/
25-
if (!attribute_parent || !is_inlinable_expression(attribute_parent, context.state.scope)) {
26-
// TODO ideally we wouldn't do this here, we'd just do it on encountering
27-
// an `Identifier` within the tag. But we currently need to handle `{42}` etc
28-
mark_subtree_dynamic(context.path);
29-
}
30-
3119
context.next({ ...context.state, expression: node.metadata.expression });
3220
}

packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function Identifier(node, context) {
2828
* before marking the subtree as dynamic. This is because if it's inlinable it will be inlined in the template
2929
* directly making the whole thing actually static.
3030
*/
31-
if (!attribute_parent || !is_inlinable_expression(attribute_parent, context.state.scope)) {
31+
if (!attribute_parent || !is_inlinable_expression(attribute_parent.value, context.state.scope)) {
3232
mark_subtree_dynamic(context.path);
3333
}
3434

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
build_template_literal,
3232
build_update,
3333
build_update_assignment,
34+
escape_template_quasis,
3435
get_states_and_calls
3536
} from './shared/utils.js';
3637

@@ -360,22 +361,30 @@ export function RegularElement(node, context) {
360361
get_states_and_calls(trimmed);
361362

362363
if (states_and_calls && states_and_calls.states === 0) {
363-
child_state.init.push(
364-
b.stmt(
365-
b.assignment(
366-
'=',
367-
b.member(context.state.node, 'textContent'),
368-
build_template_literal(trimmed, context.visit, child_state).value
369-
)
370-
)
371-
);
364+
let { value } = build_template_literal(trimmed, context.visit, child_state);
365+
// if the expression is inlinable we just push it to the template
366+
if (is_inlinable_expression(trimmed, context.state.scope)) {
367+
// escaping every quasi if it's a template literal
368+
if (value.type === 'TemplateLiteral') {
369+
escape_template_quasis(value);
370+
}
371+
state.template.push(value);
372+
} else {
373+
// else we programmatically set the value
374+
child_state.init.push(
375+
b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value))
376+
);
377+
}
372378
} else {
373379
/** @type {Expression} */
374380
let arg = context.state.node;
375381

376382
// If `hydrate_node` is set inside the element, we need to reset it
377-
// after the element has been hydrated
378-
let needs_reset = trimmed.some((node) => node.type !== 'Text');
383+
// after the element has been hydrated (we don't need to reset if it's been inlined)
384+
let needs_reset =
385+
trimmed.some((node) => node.type !== 'Text') &&
386+
(!trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') ||
387+
!is_inlinable_expression(trimmed, context.state.scope));
379388

380389
// The same applies if it's a `<template>` element, since we need to
381390
// set the value of `hydrate_node` to `node.content`
@@ -578,7 +587,11 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
578587
);
579588
}
580589

581-
const inlinable_expression = is_inlinable_expression(attribute, context.state.scope);
590+
// we need to special case textarea value because it's not an actual attribute
591+
const inlinable_expression =
592+
is_inlinable_expression(attribute.value, context.state.scope) &&
593+
attribute.name !== 'value' &&
594+
element.name !== 'textarea';
582595
if (attribute.metadata.expression.has_state) {
583596
if (has_call) {
584597
state.init.push(build_update(update));

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
55
import * as b from '../../../../../utils/builders.js';
66
import { is_inlinable_expression } from '../../../../utils.js';
7-
import { build_template_literal, build_update } from './utils.js';
7+
import { build_template_literal, build_update, escape_template_quasis } from './utils.js';
88

99
/**
1010
* Processes an array of template nodes, joining sibling text/expression nodes
@@ -83,7 +83,17 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
8383
} else if (has_state && !within_bound_contenteditable) {
8484
state.update.push(update);
8585
} else {
86-
state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value)));
86+
// if the expression is inlinable we just push it to the template
87+
if (is_inlinable_expression(sequence, state.scope)) {
88+
// escaping every quasi if it's a template literal
89+
if (value.type === 'TemplateLiteral') {
90+
escape_template_quasis(value);
91+
}
92+
state.template.push(value);
93+
} else {
94+
// else we programmatically set the value
95+
state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value)));
96+
}
8797
}
8898
}
8999

@@ -159,7 +169,7 @@ function is_static_element(node, state) {
159169
!is_text_attribute(attribute) &&
160170
// If the attribute is not a text attribute but is inlinable we will directly inline it in the
161171
// the template so before returning false we need to check that the attribute is not inlinable
162-
!is_inlinable_expression(attribute, state.scope)
172+
!is_inlinable_expression(attribute.value, state.scope)
163173
) {
164174
return false;
165175
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super } from 'estree' */
1+
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super, TemplateLiteral } from 'estree' */
22
/** @import { AST, SvelteNode } from '#compiler' */
33
/** @import { ComponentClientTransformState } from '../../types' */
44
import { walk } from 'zimmerframe';
@@ -9,6 +9,7 @@ import { regex_is_valid_identifier } from '../../../../patterns.js';
99
import { create_derived } from '../../utils.js';
1010
import is_reference from 'is-reference';
1111
import { locator } from '../../../../../state.js';
12+
import { escape_html } from '../../../../../../escaping.js';
1213

1314
/**
1415
* @param {Array<AST.Text | AST.ExpressionTag>} values
@@ -31,6 +32,15 @@ export function get_states_and_calls(values) {
3132

3233
return { states, calls };
3334
}
35+
/**
36+
* Escape the html in every quesi in the template literal
37+
* @param {TemplateLiteral} template
38+
*/
39+
export function escape_template_quasis(template) {
40+
for (let quasi of template.quasis) {
41+
quasi.value.raw = escape_html(quasi.value.raw);
42+
}
43+
}
3444

3545
/**
3646
* @param {Array<AST.Text | AST.ExpressionTag>} values

packages/svelte/src/compiler/phases/utils.js

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
/** @import { AST, Binding } from '#compiler' */
22
/** @import { Scope } from './scope' */
3+
/** @import * as ESTree from 'estree' */
4+
5+
import { walk } from 'zimmerframe';
6+
7+
/**
8+
* @param {ESTree.Expression} expr
9+
*/
10+
export function extract_identifiers(expr) {
11+
/** @type {ESTree.Identifier[]} */
12+
let nodes = [];
13+
14+
walk(
15+
expr,
16+
{},
17+
{
18+
Identifier(node, { path }) {
19+
const parent = path.at(-1);
20+
if (parent?.type !== 'MemberExpression' || parent.property !== node || parent.computed) {
21+
nodes.push(node);
22+
}
23+
}
24+
}
25+
);
26+
27+
return nodes;
28+
}
329

430
/**
531
* Whether a variable can be referenced directly from template string.
@@ -17,21 +43,31 @@ function can_inline_variable(binding) {
1743
}
1844

1945
/**
20-
* @param {AST.Attribute} attribute
46+
* @param {AST.Attribute["value"]} value
2147
* @param {Scope} scope
2248
*/
23-
export function is_inlinable_expression(attribute, scope) {
24-
if (attribute.value === true) return false; // not an expression
25-
let nodes = Array.isArray(attribute.value) ? attribute.value : [attribute.value];
49+
export function is_inlinable_expression(value, scope) {
50+
if (value === true) return false; // not an expression
51+
let nodes = Array.isArray(value) ? value : [value];
2652
let has_expression_tag = false;
2753
for (let value of nodes) {
2854
if (value.type === 'ExpressionTag') {
29-
if (value.expression.type === 'Identifier') {
30-
const binding = scope.owner(value.expression.name)?.declarations.get(value.expression.name);
31-
if (!can_inline_variable(binding)) {
32-
return false;
33-
}
34-
} else {
55+
const identifiers = extract_identifiers(value.expression);
56+
// if not every identifier is inlinable we bail
57+
if (
58+
identifiers.length > 0 &&
59+
identifiers.some((id) => {
60+
const binding = scope.owner(id.name)?.declarations.get(id.name);
61+
return !can_inline_variable(binding);
62+
})
63+
) {
64+
return false;
65+
} else if (
66+
// we need to special case null and boolean values because
67+
// we want to set them programmatically
68+
value.expression.type === 'Literal' &&
69+
(value.expression.value === null || typeof value.expression.value === 'boolean')
70+
) {
3571
return false;
3672
}
3773
has_expression_tag = true;

0 commit comments

Comments
 (0)