1
1
/** @import { Expression } from 'estree' */
2
2
/** @import { ExpressionTag, SvelteNode, Text } from '#compiler' */
3
3
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
4
+ import { is_event_attribute , is_text_attribute } from '../../../../../utils/ast.js' ;
4
5
import * as b from '../../../../../utils/builders.js' ;
5
6
import { build_template_literal , build_update } from './utils.js' ;
6
7
@@ -9,42 +10,71 @@ import { build_template_literal, build_update } from './utils.js';
9
10
* (e.g. `{a} b {c}`) into a single update function. Along the way it creates
10
11
* corresponding template node references these updates are applied to.
11
12
* @param {SvelteNode[] } nodes
12
- * @param {(is_text: boolean) => Expression } expression
13
+ * @param {(is_text: boolean) => Expression } initial
13
14
* @param {boolean } is_element
14
15
* @param {ComponentContext } context
15
16
*/
16
- export function process_children ( nodes , expression , is_element , { visit, state } ) {
17
+ export function process_children ( nodes , initial , is_element , { visit, state } ) {
17
18
const within_bound_contenteditable = state . metadata . bound_contenteditable ;
19
+ let prev = initial ;
20
+ let skipped = 0 ;
18
21
19
22
/** @typedef {Array<Text | ExpressionTag> } Sequence */
20
-
21
23
/** @type {Sequence } */
22
24
let sequence = [ ] ;
23
25
26
+ /** @param {boolean } is_text */
27
+ function get_node ( is_text ) {
28
+ if ( skipped === 0 ) {
29
+ return prev ( is_text ) ;
30
+ }
31
+
32
+ return b . call (
33
+ '$.sibling' ,
34
+ prev ( false ) ,
35
+ ( is_text || skipped !== 1 ) && b . literal ( skipped ) ,
36
+ is_text && b . true
37
+ ) ;
38
+ }
39
+
40
+ /**
41
+ * @param {boolean } is_text
42
+ * @param {string } name
43
+ */
44
+ function flush_node ( is_text , name ) {
45
+ const expression = get_node ( is_text ) ;
46
+ let id = expression ;
47
+
48
+ if ( id . type !== 'Identifier' ) {
49
+ id = b . id ( state . scope . generate ( name ) ) ;
50
+ state . init . push ( b . var ( id , expression ) ) ;
51
+ }
52
+
53
+ prev = ( ) => id ;
54
+ skipped = 1 ; // the next node is `$.sibling(id)`
55
+
56
+ return id ;
57
+ }
58
+
24
59
/**
25
60
* @param {Sequence } sequence
26
61
*/
27
62
function flush_sequence ( sequence ) {
28
- if ( sequence . length === 1 ) {
29
- const node = sequence [ 0 ] ;
30
-
31
- if ( node . type === 'Text' ) {
32
- let prev = expression ;
33
- expression = ( ) => b . call ( '$.sibling' , prev ( false ) ) ;
34
- state . template . push ( node . raw ) ;
35
- return ;
36
- }
63
+ if ( sequence . length === 1 && sequence [ 0 ] . type === 'Text' ) {
64
+ skipped += 1 ;
65
+ state . template . push ( sequence [ 0 ] . raw ) ;
66
+ return ;
37
67
}
38
68
39
- // if this is a standalone `{expression}`, make sure we handle the case where
40
- // no text node was created because the expression was empty during SSR
41
- const needs_hydration_check = sequence . length === 1 ;
42
- const id = get_node_id ( expression ( needs_hydration_check ) , state , 'text' ) ;
43
-
44
69
state . template . push ( ' ' ) ;
45
70
46
71
const { has_state, has_call, value } = build_template_literal ( sequence , visit , state ) ;
47
72
73
+ // if this is a standalone `{expression}`, make sure we handle the case where
74
+ // no text node was created because the expression was empty during SSR
75
+ const is_text = sequence . length === 1 ;
76
+ const id = flush_node ( is_text , 'text' ) ;
77
+
48
78
const update = b . stmt ( b . call ( '$.set_text' , id , value ) ) ;
49
79
50
80
if ( has_call && ! within_bound_contenteditable ) {
@@ -54,13 +84,9 @@ export function process_children(nodes, expression, is_element, { visit, state }
54
84
} else {
55
85
state . init . push ( b . stmt ( b . assignment ( '=' , b . member ( id , 'nodeValue' ) , value ) ) ) ;
56
86
}
57
-
58
- expression = ( is_text ) => b . call ( '$.sibling' , id , is_text && b . true ) ;
59
87
}
60
88
61
- for ( let i = 0 ; i < nodes . length ; i += 1 ) {
62
- const node = nodes [ i ] ;
63
-
89
+ for ( const node of nodes ) {
64
90
if ( node . type === 'Text' || node . type === 'ExpressionTag' ) {
65
91
sequence . push ( node ) ;
66
92
} else {
@@ -69,60 +95,62 @@ export function process_children(nodes, expression, is_element, { visit, state }
69
95
sequence = [ ] ;
70
96
}
71
97
72
- if (
73
- node . type === 'SvelteHead' ||
74
- node . type === 'TitleElement' ||
75
- node . type === 'SnippetBlock'
76
- ) {
77
- // These nodes do not contribute to the sibling/child tree
78
- // TODO what about e.g. ConstTag and all the other things that
79
- // get hoisted inside clean_nodes?
80
- visit ( node , state ) ;
98
+ let child_state = state ;
99
+
100
+ if ( is_static_element ( node ) ) {
101
+ skipped += 1 ;
102
+ } else if ( node . type === 'EachBlock' && nodes . length === 1 && is_element ) {
103
+ node . metadata . is_controlled = true ;
81
104
} else {
82
- if ( node . type === 'EachBlock' && nodes . length === 1 && is_element ) {
83
- node . metadata . is_controlled = true ;
84
- visit ( node , state ) ;
85
- } else {
86
- const id = get_node_id (
87
- expression ( false ) ,
88
- state ,
89
- node . type === 'RegularElement' ? node . name : 'node'
90
- ) ;
91
-
92
- expression = ( is_text ) => b . call ( '$.sibling' , id , is_text && b . true ) ;
93
-
94
- visit ( node , {
95
- ...state ,
96
- node : id
97
- } ) ;
98
- }
105
+ const id = flush_node ( false , node . type === 'RegularElement' ? node . name : 'node' ) ;
106
+ child_state = { ...state , node : id } ;
99
107
}
108
+
109
+ visit ( node , child_state ) ;
100
110
}
101
111
}
102
112
103
113
if ( sequence . length > 0 ) {
104
- // if the final item in a fragment is static text,
105
- // we need to force `hydrate_node` to advance
106
- if ( sequence . length === 1 && sequence [ 0 ] . type === 'Text' && nodes . length > 1 ) {
107
- state . init . push ( b . stmt ( b . call ( '$.next' ) ) ) ;
108
- }
109
-
110
114
flush_sequence ( sequence ) ;
111
115
}
116
+
117
+ // if there are trailing static text nodes/elements,
118
+ // traverse to the last (n - 1) one when hydrating
119
+ if ( skipped > 1 ) {
120
+ skipped -= 1 ;
121
+ state . init . push ( b . stmt ( get_node ( false ) ) ) ;
122
+ }
112
123
}
113
124
114
125
/**
115
- * @param {Expression } expression
116
- * @param {ComponentClientTransformState } state
117
- * @param {string } name
126
+ *
127
+ * @param {SvelteNode } node
118
128
*/
119
- function get_node_id ( expression , state , name ) {
120
- let id = expression ;
129
+ function is_static_element ( node ) {
130
+ if ( node . type !== 'RegularElement' ) return false ;
131
+ if ( node . fragment . metadata . dynamic ) return false ;
121
132
122
- if ( id . type !== 'Identifier' ) {
123
- id = b . id ( state . scope . generate ( name ) ) ;
133
+ for ( const attribute of node . attributes ) {
134
+ if ( attribute . type !== 'Attribute' ) {
135
+ return false ;
136
+ }
137
+
138
+ if ( is_event_attribute ( attribute ) ) {
139
+ return false ;
140
+ }
141
+
142
+ if ( attribute . value !== true && ! is_text_attribute ( attribute ) ) {
143
+ return false ;
144
+ }
124
145
125
- state . init . push ( b . var ( id , expression ) ) ;
146
+ if ( node . name === 'option' && attribute . name === 'value' ) {
147
+ return false ;
148
+ }
149
+
150
+ if ( node . name . includes ( '-' ) ) {
151
+ return false ; // we're setting all attributes on custom elements through properties
152
+ }
126
153
}
127
- return id ;
154
+
155
+ return true ;
128
156
}
0 commit comments