11/**
22 * @import { TemplateOperations } from "../types.js"
33 * @import { Namespace } from "#compiler"
4- * @import { CallExpression, Statement } from "estree"
4+ * @import { CallExpression, Statement, ObjectExpression, Identifier, ArrayExpression, Property, Expression, Literal } from "estree"
55 */
66import { NAMESPACE_SVG , NAMESPACE_MATHML } from '../../../../../constants.js' ;
77import * as b from '../../../../utils/builders.js' ;
8+ import { regex_is_valid_identifier } from '../../../patterns.js' ;
89import fix_attribute_casing from './fix-attribute-casing.js' ;
910
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-
2811/**
2912 * @param {TemplateOperations } items
30- * @param {Namespace } namespace
3113 */
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 ( [ ] ) ;
3816
3917 /**
4018 * @type {Array<Element> }
4119 */
4220 let elements_stack = [ ] ;
4321
44- /**
45- * @type {Array<string> }
46- */
47- let namespace_stack = [ ] ;
48-
49- /**
50- * @type {number }
51- */
52- let foreign_object_count = 0 ;
53-
5422 /**
5523 * @type {Element | undefined }
5624 */
@@ -71,199 +39,138 @@ export function template_to_functions(items, namespace) {
7139 // we closed one element, we remove it from the stack and eventually revert back
7240 // the namespace to the previous one
7341 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 ( ) ;
8143 continue ;
8244 }
8345
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-
9046 // @ts -expect-error we can't be here if `swap_current_element` but TS doesn't know that
9147 const value = map [ instruction . kind ] (
9248 ...[
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)
9949 ...( 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 ) ] ) ,
11152 ...( instruction . args ?? [ ] )
11253 ]
11354 ) ;
11455
115- if ( value ) {
116- // this will compose the body of the function
117- body . push ( value . call ) ;
118- }
119-
12056 // with set_prop we don't need to do anything else, in all other cases we also need to
12157 // append the element/node/anchor to the current active element or push it in the elements array
12258 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 ) ;
12863 }
12964 // keep track of the last created element (it will be pushed to the stack after the props are set)
13065 if ( instruction . kind === 'create_element' ) {
13166 last_current_element = /** @type {Element } */ ( value ) ;
132- if ( last_current_element . element === 'foreignObject' ) {
133- foreign_object_count ++ ;
134- }
13567 }
13668 }
13769 }
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 ) ) ) ;
14370
144- return b . arrow ( [ ] , b . block ( body ) ) ;
71+ return elements ;
14572}
14673
14774/**
148- * @typedef {{ call: Statement, name: string, add_is: (value: string)=>void, namespaced: boolean; element: string; } } Element
75+ * @typedef {ObjectExpression } Element
14976 */
15077
15178/**
152- * @typedef {{ call: Statement, name: string } } Anchor
79+ * @typedef {void | null | ArrayExpression } Anchor
15380 */
15481
15582/**
156- * @typedef {{ call: Statement, name: string } } Text
83+ * @typedef {void | Literal } Text
15784 */
15885
15986/**
16087 * @typedef { Element | Anchor| Text } Node
16188 */
16289
16390/**
164- * @param {Scope } scope
165- * @param {Namespace } namespace
16691 * @param {string } element
16792 * @returns {Element }
16893 */
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 ) ;
189112 }
190- return {
191- call,
192- name,
193- element,
194- add_is,
195- namespaced : namespace != null
196- } ;
113+ return /** @type {Property } */ ( prop ) ;
197114}
198115
199116/**
200- * @param {Scope } scope
117+ * @param {Element } element
201118 * @param {string } data
202119 * @returns {Anchor }
203120 */
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 ) ;
210125}
211126
212127/**
213- * @param {Scope } scope
128+ * @param {Element } element
214129 * @param {string } value
215130 * @returns {Text }
216131 */
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 ) ) ;
223136}
224137
225138/**
226139 *
227- * @param {Element } el
140+ * @param {Element } element
228141 * @param {string } prop
229142 * @param {string } value
230143 */
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+
233147 if ( prop === 'is' ) {
234- el . add_is ( value ) ;
148+ element . properties . push ( b . prop ( 'init' , b . id ( prop ) , b . literal ( value ) ) ) ;
235149 return ;
236150 }
237151
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 ) ;
241153
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 ) ;
246155
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+ ) ;
250164}
251165
252166/**
253167 *
254- * @param {Element } el
255- * @param {Node } child
256- * @param {Node } [anchor]
168+ * @param {Element } element
169+ * @param {Element } child
257170 */
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 ) ;
267174}
268175
269176let map = {
0 commit comments