1
1
/**
2
2
* @import { TemplateOperations } from "../types.js"
3
3
* @import { Namespace } from "#compiler"
4
- * @import { CallExpression, Statement } from "estree"
4
+ * @import { CallExpression, Statement, ObjectExpression, Identifier, ArrayExpression, Property, Expression, Literal } from "estree"
5
5
*/
6
6
import { NAMESPACE_SVG , NAMESPACE_MATHML } from '../../../../../constants.js' ;
7
7
import * as b from '../../../../utils/builders.js' ;
8
+ import { regex_is_valid_identifier } from '../../../patterns.js' ;
8
9
import fix_attribute_casing from './fix-attribute-casing.js' ;
9
10
10
- class Scope {
11
- declared = new Map ( ) ;
12
-
13
- /**
14
- * @param {string } _name
15
- */
16
- generate ( _name ) {
17
- let name = _name . replace ( / [ ^ a - z A - Z 0 - 9 _ $ ] / g, '_' ) . replace ( / ^ [ 0 - 9 ] / , '_' ) ;
18
- if ( ! this . declared . has ( name ) ) {
19
- this . declared . set ( name , 1 ) ;
20
- return name ;
21
- }
22
- let count = this . declared . get ( name ) ;
23
- this . declared . set ( name , count + 1 ) ;
24
- return `${ name } _${ count } ` ;
25
- }
26
- }
27
-
28
11
/**
29
12
* @param {TemplateOperations } items
30
- * @param {Namespace } namespace
31
13
*/
32
- export function template_to_functions ( items , namespace ) {
33
- let elements = [ ] ;
34
-
35
- let body = [ ] ;
36
-
37
- let scope = new Scope ( ) ;
14
+ export function template_to_functions ( items ) {
15
+ let elements = b . array ( [ ] ) ;
38
16
39
17
/**
40
18
* @type {Array<Element> }
41
19
*/
42
20
let elements_stack = [ ] ;
43
21
44
- /**
45
- * @type {Array<string> }
46
- */
47
- let namespace_stack = [ ] ;
48
-
49
- /**
50
- * @type {number }
51
- */
52
- let foreign_object_count = 0 ;
53
-
54
22
/**
55
23
* @type {Element | undefined }
56
24
*/
@@ -71,199 +39,138 @@ export function template_to_functions(items, namespace) {
71
39
// we closed one element, we remove it from the stack and eventually revert back
72
40
// the namespace to the previous one
73
41
if ( instruction . kind === 'pop_element' ) {
74
- const removed = elements_stack . pop ( ) ;
75
- if ( removed ?. namespaced ) {
76
- namespace_stack . pop ( ) ;
77
- }
78
- if ( removed ?. element === 'foreignObject' ) {
79
- foreign_object_count -- ;
80
- }
42
+ elements_stack . pop ( ) ;
81
43
continue ;
82
44
}
83
45
84
- // if the inserted node is in the svg/mathml we push the namespace to the stack because we need to
85
- // create with createElementNS
86
- if ( instruction . metadata ?. svg || instruction . metadata ?. mathml ) {
87
- namespace_stack . push ( instruction . metadata . svg ? NAMESPACE_SVG : NAMESPACE_MATHML ) ;
88
- }
89
-
90
46
// @ts -expect-error we can't be here if `swap_current_element` but TS doesn't know that
91
47
const value = map [ instruction . kind ] (
92
48
...[
93
- // for set prop we need to send the last element (not the one in the stack since
94
- // it get's added to the stack only after the push_element instruction)...for all the rest
95
- // the first prop is a the scope to generate the name of the variable
96
- ...( instruction . kind === 'set_prop' ? [ last_current_element ] : [ scope ] ) ,
97
- // for create element we also need to add the namespace...namespaces in the stack get's precedence over
98
- // the "global" namespace (and if we are in a foreignObject we default to html)
99
49
...( instruction . kind === 'create_element'
100
- ? [
101
- foreign_object_count > 0
102
- ? undefined
103
- : namespace_stack . at ( - 1 ) ??
104
- ( namespace === 'svg'
105
- ? NAMESPACE_SVG
106
- : namespace === 'mathml'
107
- ? NAMESPACE_MATHML
108
- : undefined )
109
- ]
110
- : [ ] ) ,
50
+ ? [ ]
51
+ : [ instruction . kind === 'set_prop' ? last_current_element : elements_stack . at ( - 1 ) ] ) ,
111
52
...( instruction . args ?? [ ] )
112
53
]
113
54
) ;
114
55
115
- if ( value ) {
116
- // this will compose the body of the function
117
- body . push ( value . call ) ;
118
- }
119
-
120
56
// with set_prop we don't need to do anything else, in all other cases we also need to
121
57
// append the element/node/anchor to the current active element or push it in the elements array
122
58
if ( instruction . kind !== 'set_prop' ) {
123
- if ( elements_stack . length >= 1 && value ) {
124
- const { call } = map . insert ( /** @type {Element } */ ( elements_stack . at ( - 1 ) ) , value ) ;
125
- body . push ( call ) ;
126
- } else if ( value ) {
127
- elements . push ( b . id ( value . name ) ) ;
59
+ if ( elements_stack . length >= 1 && value !== undefined ) {
60
+ map . insert ( /** @type {Element } */ ( elements_stack . at ( - 1 ) ) , value ) ;
61
+ } else if ( value !== undefined ) {
62
+ elements . elements . push ( value ) ;
128
63
}
129
64
// keep track of the last created element (it will be pushed to the stack after the props are set)
130
65
if ( instruction . kind === 'create_element' ) {
131
66
last_current_element = /** @type {Element } */ ( value ) ;
132
- if ( last_current_element . element === 'foreignObject' ) {
133
- foreign_object_count ++ ;
134
- }
135
67
}
136
68
}
137
69
}
138
- // every function needs to return a fragment so we create one and push all the elements there
139
- const fragment = scope . generate ( 'fragment' ) ;
140
- body . push ( b . var ( fragment , b . call ( 'document.createDocumentFragment' ) ) ) ;
141
- body . push ( b . call ( fragment + '.append' , ...elements ) ) ;
142
- body . push ( b . return ( b . id ( fragment ) ) ) ;
143
70
144
- return b . arrow ( [ ] , b . block ( body ) ) ;
71
+ return elements ;
145
72
}
146
73
147
74
/**
148
- * @typedef {{ call: Statement, name: string, add_is: (value: string)=>void, namespaced: boolean; element: string; } } Element
75
+ * @typedef {ObjectExpression } Element
149
76
*/
150
77
151
78
/**
152
- * @typedef {{ call: Statement, name: string } } Anchor
79
+ * @typedef {void | null | ArrayExpression } Anchor
153
80
*/
154
81
155
82
/**
156
- * @typedef {{ call: Statement, name: string } } Text
83
+ * @typedef {void | Literal } Text
157
84
*/
158
85
159
86
/**
160
87
* @typedef { Element | Anchor| Text } Node
161
88
*/
162
89
163
90
/**
164
- * @param {Scope } scope
165
- * @param {Namespace } namespace
166
91
* @param {string } element
167
92
* @returns {Element }
168
93
*/
169
- function create_element ( scope , namespace , element ) {
170
- const name = scope . generate ( element ) ;
171
- let fn = namespace != null ? 'document.createElementNS' : 'document.createElement' ;
172
- let args = [ b . literal ( element ) ] ;
173
- if ( namespace != null ) {
174
- args . unshift ( b . literal ( namespace ) ) ;
175
- }
176
- const call = b . var ( name , b . call ( fn , ...args ) ) ;
177
- /**
178
- * if there's an "is" attribute we can't just add it as a property, it needs to be
179
- * specified on creation like this `document.createElement('button', { is: 'my-button' })`
180
- *
181
- * Since the props are appended after the creation we change the generated call arguments and we push
182
- * the is attribute later on on `set_prop`
183
- * @param {string } value
184
- */
185
- function add_is ( value ) {
186
- /** @type {CallExpression } */ ( call . declarations [ 0 ] . init ) . arguments . push (
187
- b . object ( [ b . prop ( 'init' , b . literal ( 'is' ) , b . literal ( value ) ) ] )
188
- ) ;
94
+ function create_element ( element ) {
95
+ return b . object ( [ b . prop ( 'init' , b . id ( 'e' ) , b . literal ( element ) ) ] ) ;
96
+ }
97
+
98
+ /**
99
+ *
100
+ * @param {Element } element
101
+ * @param {string } name
102
+ * @param {Expression } init
103
+ * @returns {Property }
104
+ */
105
+ function get_or_create_prop ( element , name , init ) {
106
+ let prop = element . properties . find (
107
+ ( prop ) => prop . type === 'Property' && /** @type {Identifier } */ ( prop . key ) . name === name
108
+ ) ;
109
+ if ( ! prop ) {
110
+ prop = b . prop ( 'init' , b . id ( name ) , init ) ;
111
+ element . properties . push ( prop ) ;
189
112
}
190
- return {
191
- call,
192
- name,
193
- element,
194
- add_is,
195
- namespaced : namespace != null
196
- } ;
113
+ return /** @type {Property } */ ( prop ) ;
197
114
}
198
115
199
116
/**
200
- * @param {Scope } scope
117
+ * @param {Element } element
201
118
* @param {string } data
202
119
* @returns {Anchor }
203
120
*/
204
- function create_anchor ( scope , data = '' ) {
205
- const name = scope . generate ( 'comment' ) ;
206
- return {
207
- call : b . var ( name , b . call ( 'document.createComment' , b . literal ( data ) ) ) ,
208
- name
209
- } ;
121
+ function create_anchor ( element , data = '' ) {
122
+ if ( ! element ) return data ? b . array ( [ b . literal ( data ) ] ) : null ;
123
+ const c = get_or_create_prop ( element , 'c' , b . array ( [ ] ) ) ;
124
+ /** @type {ArrayExpression } */ ( c . value ) . elements . push ( data ? b . array ( [ b . literal ( data ) ] ) : null ) ;
210
125
}
211
126
212
127
/**
213
- * @param {Scope } scope
128
+ * @param {Element } element
214
129
* @param {string } value
215
130
* @returns {Text }
216
131
*/
217
- function create_text ( scope , value ) {
218
- const name = scope . generate ( 'text' ) ;
219
- return {
220
- call : b . var ( name , b . call ( 'document.createTextNode' , b . literal ( value ) ) ) ,
221
- name
222
- } ;
132
+ function create_text ( element , value ) {
133
+ if ( ! element ) return b . literal ( value ) ;
134
+ const c = get_or_create_prop ( element , 'c' , b . array ( [ ] ) ) ;
135
+ /** @type {ArrayExpression } */ ( c . value ) . elements . push ( b . literal ( value ) ) ;
223
136
}
224
137
225
138
/**
226
139
*
227
- * @param {Element } el
140
+ * @param {Element } element
228
141
* @param {string } prop
229
142
* @param {string } value
230
143
*/
231
- function set_prop ( el , prop , value ) {
232
- // see comment above about the "is" attribute
144
+ function set_prop ( element , prop , value ) {
145
+ const p = get_or_create_prop ( element , 'p' , b . object ( [ ] ) ) ;
146
+
233
147
if ( prop === 'is' ) {
234
- el . add_is ( value ) ;
148
+ element . properties . push ( b . prop ( 'init' , b . id ( prop ) , b . literal ( value ) ) ) ;
235
149
return ;
236
150
}
237
151
238
- const [ namespace ] = prop . split ( ':' ) ;
239
- let fn = namespace !== prop ? '.setAttributeNS' : '.setAttribute' ;
240
- let args = [ b . literal ( fix_attribute_casing ( prop ) ) , b . literal ( value ?? '' ) ] ;
152
+ const prop_correct_case = fix_attribute_casing ( prop ) ;
241
153
242
- // attributes like `xlink:href` need to be set with the `xlink` namespace
243
- if ( namespace === 'xlink' ) {
244
- args . unshift ( b . literal ( 'http://www.w3.org/1999/xlink' ) ) ;
245
- }
154
+ const is_valid_id = regex_is_valid_identifier . test ( prop_correct_case ) ;
246
155
247
- return {
248
- call : b . call ( el . name + fn , ...args )
249
- } ;
156
+ /** @type {ObjectExpression } */ ( p . value ) . properties . push (
157
+ b . prop (
158
+ 'init' ,
159
+ ( is_valid_id ? b . id : b . literal ) ( prop_correct_case ) ,
160
+ b . literal ( value ) ,
161
+ ! is_valid_id
162
+ )
163
+ ) ;
250
164
}
251
165
252
166
/**
253
167
*
254
- * @param {Element } el
255
- * @param {Node } child
256
- * @param {Node } [anchor]
168
+ * @param {Element } element
169
+ * @param {Element } child
257
170
*/
258
- function insert ( el , child , anchor ) {
259
- return {
260
- call : b . call (
261
- // if we have a template element we need to push into it's content rather than the element itself
262
- el . name + ( el . element === 'template' ? '.content' : '' ) + '.insertBefore' ,
263
- b . id ( child . name ) ,
264
- b . id ( anchor ?. name ?? 'undefined' )
265
- )
266
- } ;
171
+ function insert ( element , child ) {
172
+ const c = get_or_create_prop ( element , 'c' , b . array ( [ ] ) ) ;
173
+ /** @type {ArrayExpression } */ ( c . value ) . elements . push ( child ) ;
267
174
}
268
175
269
176
let map = {
0 commit comments