@lightningjs/blits 1.30.2 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +1029 -1019
  2. package/LICENSE +202 -202
  3. package/NOTICE +12 -12
  4. package/README.md +33 -33
  5. package/bin/index.js +8 -8
  6. package/blits.d.ts +6 -6
  7. package/index.d.ts +1099 -1099
  8. package/index.js +28 -28
  9. package/package.json +57 -56
  10. package/public/assets/fps_sprite.base64.js +1 -1
  11. package/scripts/postpublish.js +59 -59
  12. package/scripts/prepublishOnly.js +84 -84
  13. package/scripts/prepush.js +78 -78
  14. package/src/announcer/announcer.js +162 -162
  15. package/src/announcer/speechSynthesis.js +107 -107
  16. package/src/application.js +100 -100
  17. package/src/component/base/announcer.js +27 -27
  18. package/src/component/base/events.js +69 -69
  19. package/src/component/base/index.js +33 -33
  20. package/src/component/base/methods.js +244 -244
  21. package/src/component/base/router.js +41 -41
  22. package/src/component/base/timeouts_intervals.js +99 -99
  23. package/src/component/base/utils.js +72 -72
  24. package/src/component/setup/computed.js +48 -48
  25. package/src/component/setup/index.js +62 -62
  26. package/src/component/setup/input.js +30 -30
  27. package/src/component/setup/methods.js +37 -37
  28. package/src/component/setup/props.js +62 -62
  29. package/src/component/setup/routes.js +42 -42
  30. package/src/component/setup/state.js +59 -59
  31. package/src/component/setup/watch.js +35 -35
  32. package/src/component.js +437 -437
  33. package/src/components/Circle.js +73 -73
  34. package/src/components/FPScounter.js +1510 -1510
  35. package/src/components/RouterView.js +81 -81
  36. package/src/components/Sprite.js +94 -94
  37. package/src/components/index.js +28 -28
  38. package/src/constants.js +21 -21
  39. package/src/engine.js +1 -1
  40. package/src/engines/L3/element.js +744 -744
  41. package/src/engines/L3/fontLoader.js +38 -38
  42. package/src/engines/L3/index.js +7 -7
  43. package/src/engines/L3/launch.js +123 -123
  44. package/src/engines/L3/shaderLoader.js +12 -12
  45. package/src/engines/index.js +1 -1
  46. package/src/focus.js +90 -90
  47. package/src/helpers/deepEqualArray.js +50 -50
  48. package/src/helpers/fetchJson.js +34 -34
  49. package/src/helpers/sequence.js +26 -26
  50. package/src/launch.js +123 -123
  51. package/src/lib/blitsfileconverter/blitsfileconverter.js +52 -52
  52. package/src/lib/codegenerator/generator.js +788 -788
  53. package/src/lib/colors/colors.js +92 -92
  54. package/src/lib/colors/htmlColors.js +168 -168
  55. package/src/lib/componentId.js +49 -49
  56. package/src/lib/eventListeners.js +123 -123
  57. package/src/lib/hooks.js +59 -59
  58. package/src/lib/lifecycle.js +85 -85
  59. package/src/lib/log.js +117 -117
  60. package/src/lib/precompiler/precompiler.js +73 -73
  61. package/src/lib/reactivity/effect.js +93 -93
  62. package/src/lib/reactivity/reactive.js +208 -208
  63. package/src/lib/reactivityguard/computedprops.js +218 -218
  64. package/src/lib/symbols.js +108 -108
  65. package/src/lib/templateparser/parser.js +386 -386
  66. package/src/lib/utils.js +27 -27
  67. package/src/plugin.js +27 -27
  68. package/src/plugins/appstate.js +26 -26
  69. package/src/plugins/index.js +20 -20
  70. package/src/plugins/language.js +152 -152
  71. package/src/plugins/log.js +25 -25
  72. package/src/plugins/theme.js +94 -94
  73. package/src/router/router.js +516 -514
  74. package/src/router/transitions/fadeIn.js +27 -27
  75. package/src/router/transitions/fadeInOut.js +33 -33
  76. package/src/router/transitions/index.js +21 -21
  77. package/src/router/transitions/slideIn.js +28 -28
  78. package/src/router/transitions/slideInOut.js +33 -33
  79. package/src/settings.js +40 -40
  80. package/vite/blitsFileConverter.js +67 -67
  81. package/vite/index.js +28 -28
  82. package/vite/msdfGenerator.js +269 -269
  83. package/vite/preCompiler.js +47 -42
  84. package/vite/reactivityGuard.js +45 -45
  85. package/vscode/data/template-attributes.json +589 -589
  86. package/.editorconfig +0 -7
  87. package/.eslintignore +0 -1
  88. package/.eslintrc.cjs +0 -65
  89. package/.github/workflows/comment-lint-results.yml +0 -125
  90. package/.github/workflows/comment-test-results.yml +0 -109
  91. package/.github/workflows/deploy-docs.yml +0 -27
  92. package/.github/workflows/run-blits-tests.yml +0 -142
  93. package/.github/workflows/run-lint.yml +0 -128
  94. package/.husky/pre-commit +0 -1
  95. package/.husky/pre-push +0 -1
  96. package/.vscode/extensions.json +0 -7
  97. package/.vscode/settings.json +0 -9
  98. package/COPYING +0 -1
  99. package/banner.png +0 -0
  100. package/docs/.nojekyll +0 -0
  101. package/docs/README.md +0 -19
  102. package/docs/_sidebar.md +0 -37
  103. package/docs/built-in/directives.md +0 -50
  104. package/docs/built-in/for-loop.md +0 -204
  105. package/docs/built-in/layout.md +0 -168
  106. package/docs/components/component_state.md +0 -90
  107. package/docs/components/computed_properties.md +0 -45
  108. package/docs/components/lifecycle_events.md +0 -42
  109. package/docs/components/methods.md +0 -45
  110. package/docs/components/props.md +0 -56
  111. package/docs/components/user_input.md +0 -186
  112. package/docs/components/utility_methods.md +0 -320
  113. package/docs/components/watchers.md +0 -34
  114. package/docs/essentials/application_root.md +0 -19
  115. package/docs/essentials/components.md +0 -68
  116. package/docs/essentials/displaying_images.md +0 -67
  117. package/docs/essentials/displaying_text.md +0 -135
  118. package/docs/essentials/element_attributes.md +0 -195
  119. package/docs/essentials/settings.md +0 -117
  120. package/docs/essentials/template_syntax.md +0 -83
  121. package/docs/getting_started/file_structure.md +0 -58
  122. package/docs/getting_started/getting_started.md +0 -45
  123. package/docs/getting_started/intro.md +0 -20
  124. package/docs/index.html +0 -47
  125. package/docs/performance/lazy-loading.md +0 -161
  126. package/docs/plugins/global_app_state.md +0 -98
  127. package/docs/plugins/language.md +0 -172
  128. package/docs/plugins/text-to-speech-announcer.md +0 -120
  129. package/docs/plugins/theme.md +0 -176
  130. package/docs/router/basics.md +0 -175
  131. package/docs/router/hooks.md +0 -70
  132. package/docs/router/transitions.md +0 -146
  133. package/docs/sidebar.json +0 -171
  134. package/docs/transitions_animations/transitions.md +0 -150
  135. package/jsconfig.json +0 -12
  136. package/src/component/setup/props.test.js +0 -238
  137. package/src/component.test.js +0 -388
  138. package/src/components/Circle.orig.js +0 -31
  139. package/src/components/FPScounter.orig.js +0 -121
  140. package/src/components/RouterView.orig.js +0 -54
  141. package/src/components/Sprite.orig.js +0 -58
  142. package/src/components/index.orig.js +0 -28
  143. package/src/engines/L3/element.test.js +0 -579
  144. package/src/helpers/deepEqualArray.test.js +0 -127
  145. package/src/lib/codegenerator/generator.test.js +0 -3801
  146. package/src/lib/colors/colors.test.js +0 -290
  147. package/src/lib/componentId.test.js +0 -84
  148. package/src/lib/eventListeners.test.js +0 -236
  149. package/src/lib/reactivity/effect.test.js +0 -35
  150. package/src/lib/reactivity/reactive.test.js +0 -27
  151. package/src/lib/templateparser/parser.test.js +0 -1411
  152. package/src/router/router.test.js +0 -473
@@ -1,788 +1,788 @@
1
- /*
2
- * Copyright 2023 Comcast Cable Communications Management, LLC
3
- * Licensed under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License.
5
- * You may obtain a copy of the License at
6
- *
7
- * http://www.apache.org/licenses/LICENSE-2.0
8
- *
9
- * Unless required by applicable law or agreed to in writing, software
10
- * distributed under the License is distributed on an "AS IS" BASIS,
11
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- * See the License for the specific language governing permissions and
13
- * limitations under the License.
14
- *
15
- * SPDX-License-Identifier: Apache-2.0
16
- */
17
-
18
- let counter
19
- let isDev
20
-
21
- export default function (templateObject = { children: [] }, devMode = false) {
22
- const ctx = {
23
- renderCode: [
24
- 'const elms = []',
25
- 'let componentType',
26
- 'const rootComponent = component',
27
- 'let propData',
28
- 'let slotComponent',
29
- 'let inSlot = false',
30
- 'let slotChildCounter = 0',
31
- ],
32
- effectsCode: [],
33
- context: { props: [], components: this.components },
34
- }
35
-
36
- counter = -1
37
- isDev = devMode
38
- if (isDev === true) {
39
- ctx.renderCode.push(`
40
- function propInComponent(prop, kind="dynamic") {
41
- const property = prop.includes('.') ? prop.split('.')[0] : prop
42
- if (kind === 'reactive' || prop.includes('.') === false) {
43
- if (property in component === false) {
44
- Log.warn('Property ' + property + ' was accessed during render but is not defined on instance')
45
- }
46
- } else {
47
- const nestedKeys = prop.split('.')
48
- let base = component
49
- for(let i =0; i<nestedKeys.length;i++){
50
- if (base[nestedKeys[i]] === undefined) {
51
- Log.warn('Property ' + nestedKeys.slice(0,i+1).join('.') + ' was accessed during render but is not defined on instance')
52
- }
53
- base = base[nestedKeys[i]]
54
- }
55
- }
56
- }
57
- `)
58
- }
59
- generateCode.call(ctx, templateObject)
60
- ctx.renderCode.push('return elms')
61
-
62
- return {
63
- render: new Function(
64
- 'parent',
65
- 'component',
66
- 'context',
67
- 'components',
68
- 'effect',
69
- 'getRaw',
70
- 'Log',
71
- ctx.renderCode.join('\n')
72
- ),
73
- effects: ctx.effectsCode.map(
74
- (code) =>
75
- new Function('component', 'elms', 'context', 'components', 'rootComponent', 'effect', code)
76
- ),
77
- context: ctx.context,
78
- }
79
- }
80
-
81
- // This is used to get only variable from expression
82
- const extractVariables = function (value) {
83
- const regEx = /\$\$?\w+(\.\w+)?/g
84
- const matches = value.match(regEx)
85
- if (matches !== null) {
86
- const shaderRegex = /\$shader/
87
- return matches.filter((match) => !shaderRegex.test(match))
88
- } else {
89
- return false
90
- }
91
- }
92
-
93
- const verifyVariables = function (value, renderCode, type = 'dynamic') {
94
- const variablesToBeVerified = extractVariables(value)
95
- if (variablesToBeVerified !== false) {
96
- for (let i = 0; i < variablesToBeVerified.length; i++) {
97
- let variable = variablesToBeVerified[i]
98
- if (type === 'reactive' && variable.includes('.')) {
99
- variable = variable.split('.')[0]
100
- }
101
- renderCode.push(`propInComponent('${variable.replace('$', '')}', '${type}')`)
102
- }
103
- }
104
- }
105
-
106
- const generateElementCode = function (
107
- templateObject,
108
- parent = false,
109
- options = { key: false, component: 'component.', forceEffect: false, forloop: false }
110
- ) {
111
- const renderCode = options.forceEffect ? this.effectsCode : this.renderCode
112
- if (parent) {
113
- renderCode.push(`parent = ${parent}`)
114
- }
115
-
116
- if ('key' in templateObject) options.key = interpolate(templateObject.key, options.component)
117
- const elm = options.key ? `elms[${counter}][${options.key}]` : `elms[${counter}]`
118
-
119
- if (options.key) {
120
- renderCode.push(`
121
- if(elms[${counter}] === undefined) {
122
- elms[${counter}] = {}
123
- }
124
- `)
125
- }
126
-
127
- renderCode.push(`const elementConfig${counter} = {}`)
128
-
129
- if (options.forloop) {
130
- renderCode.push(`if(${elm} === undefined) {`)
131
- }
132
-
133
- renderCode.push(`
134
- ${elm} = this.element({parent: parent || 'root'}, inSlot === true ? slotComponent : component)
135
- `)
136
-
137
- if (options.forloop) {
138
- renderCode.push('}')
139
- }
140
-
141
- const children = templateObject['children']
142
- delete templateObject['children']
143
-
144
- if (templateObject[Symbol.for('componentType')] === 'Slot') {
145
- renderCode.push(`elementConfig${counter}[Symbol.for('isSlot')] = true`)
146
- }
147
-
148
- Object.keys(templateObject).forEach((key) => {
149
- if (key === 'slot') {
150
- renderCode.push(`
151
- elementConfig${counter}['parent'] = slotComponent[Symbol.for('slots')] !== undefined && Array.isArray(slotComponent[Symbol.for('slots')]) === true && slotComponent[Symbol.for('slots')].filter(slot => slot.ref === '${templateObject.slot}').shift() || parent
152
- `)
153
- }
154
-
155
- if (key === 'key') return
156
-
157
- const value = templateObject[key]
158
-
159
- if (isReactiveKey(key)) {
160
- if (options.holder && key === ':color') return
161
- if (options.holder) {
162
- this.effectsCode.push(`
163
- if(typeof skip${counter} === 'undefined' ||
164
- skip${counter}.indexOf('${key.substring(1)}') === -1)
165
- ${elm}.set('${key.substring(1)}', ${interpolate(templateObject[key], options.component)})
166
- `)
167
- } else {
168
- this.effectsCode.push(`
169
- ${elm}.set('${key.substring(1)}', ${interpolate(
170
- templateObject[key],
171
- options.component
172
- )})
173
- `)
174
- }
175
- // value.includes('.') === false &&
176
- if (isDev === true && options.component !== 'scope.' && value.includes('$')) {
177
- verifyVariables(value, renderCode, 'reactive')
178
- }
179
- renderCode.push(
180
- `elementConfig${counter}['${key.substring(1)}'] = ${interpolate(value, options.component)}`
181
- )
182
- } else {
183
- if (isDev === true && options.component !== 'scope.' && value.includes('$')) {
184
- verifyVariables(value, renderCode)
185
- }
186
- renderCode.push(`elementConfig${counter}['${key}'] = ${cast(value, key, options.component)}`)
187
- }
188
- })
189
-
190
- if (options.holder === true) {
191
- renderCode.push(`
192
- const skip${counter} = []
193
- if(typeof cmp${counter} !== 'undefined') {
194
- for(let key in cmp${counter}[Symbol.for('config')].props) {
195
- delete elementConfig${counter}[cmp${counter}[Symbol.for('config')].props[key]]
196
- skip${counter}.push(cmp${counter}[Symbol.for('config')].props[key])
197
- }
198
- }
199
- `)
200
- }
201
-
202
- if (options.forloop) {
203
- renderCode.push(`if(${elm}.nodeId === undefined) {`)
204
- }
205
-
206
- renderCode.push(`${elm}.populate(elementConfig${counter})`)
207
-
208
- renderCode.push(`
209
- if(inSlot === true) {
210
- slotChildCounter -= 1
211
- }
212
- `)
213
-
214
- if (options.forloop) {
215
- renderCode.push('}')
216
- }
217
-
218
- if (children) {
219
- generateCode.call(this, { children }, `${elm}`, options)
220
- }
221
- }
222
-
223
- const generateComponentCode = function (
224
- templateObject,
225
- parent = false,
226
- options = {
227
- key: false,
228
- component: 'component.',
229
- forceEffect: false,
230
- holder: false,
231
- forloop: false,
232
- }
233
- ) {
234
- const renderCode = options.forceEffect ? this.effectsCode : this.renderCode
235
-
236
- renderCode.push(`
237
- const cmp${counter} =
238
- (context.components && context.components['${
239
- templateObject[Symbol.for('componentType')]
240
- }']) || components['${templateObject[Symbol.for('componentType')]}']
241
- `)
242
-
243
- if ('key' in templateObject) {
244
- options.key = interpolate(templateObject.key, options.component)
245
- }
246
-
247
- const children = templateObject.children
248
- delete templateObject.children
249
- generateElementCode.call(this, templateObject, parent, { ...options, ...{ holder: true } })
250
-
251
- parent = options.key ? `elms[${counter}][${options.key}]` : `elms[${counter}]`
252
-
253
- counter++
254
-
255
- const elm = options.key ? `elms[${counter}][${options.key}]` : `elms[${counter}]`
256
-
257
- if (options.key) {
258
- renderCode.push(`
259
- if(elms[${counter}] === undefined) {
260
- elms[${counter}] = {}
261
- }
262
- `)
263
- }
264
-
265
- if (parent) {
266
- renderCode.push(`parent = ${parent}`)
267
- }
268
-
269
- renderCode.push(`const props${counter} = {}`)
270
-
271
- if (options.forloop) {
272
- renderCode.push(`if(${elm} === undefined) {`)
273
- }
274
-
275
- Object.keys(templateObject).forEach((key) => {
276
- if (isReactiveKey(key)) {
277
- this.effectsCode.push(`
278
- ${elm}[Symbol.for('props')]['${key.substring(1)}'] = ${interpolate(
279
- templateObject[key],
280
- options.component
281
- )}`)
282
- renderCode.push(`
283
- propData = ${interpolate(templateObject[key], options.component)}
284
- if (Array.isArray(propData) === true) {
285
- propData = getRaw(propData).slice(0)
286
- }
287
- props${counter}['${key.substring(1)}'] = propData`)
288
- } else {
289
- renderCode.push(
290
- `props${counter}['${key}'] = ${cast(templateObject[key], key, options.component)}`
291
- )
292
- }
293
- })
294
-
295
- renderCode.push(`
296
- componentType = props${counter}['is'] || '${templateObject[Symbol.for('componentType')]}'
297
-
298
- let component${counter}
299
- if(typeof componentType === 'string') {
300
- component${counter} = context.components && context.components[componentType] || components[componentType]
301
- if(!component${counter}) {
302
- throw new Error('Component "${templateObject[Symbol.for('componentType')]}" not found')
303
- }
304
- } else if(typeof componentType === 'function' && componentType[Symbol.for('isComponent')] === true) {
305
- component${counter} = componentType
306
- }
307
-
308
- ${elm} = component${counter}.call(null, {props: props${counter}}, ${parent}, component)
309
-
310
- if (${elm}[Symbol.for('slots')][0]) {
311
- parent = ${elm}[Symbol.for('slots')][0]
312
- slotComponent = ${elm}
313
- inSlot = true
314
- } else {
315
- parent = ${elm}[Symbol.for('children')][0]
316
- }
317
- `)
318
-
319
- if (options.forloop) {
320
- renderCode.push('}')
321
- }
322
-
323
- if (children) {
324
- if (!options.forloop) {
325
- renderCode.push(`
326
- if(inSlot === true) {
327
- slotChildCounter = ${children.length} + 1
328
- }
329
- `)
330
- }
331
- counter++
332
- generateElementCode.call(this, { children }, false, { ...options })
333
- }
334
-
335
- if (!options.forloop) {
336
- renderCode.push(`
337
- if (inSlot === true && slotChildCounter === 0) {
338
- inSlot = false
339
- }
340
- `)
341
- }
342
- }
343
-
344
- const generateForLoopCode = function (templateObject, parent) {
345
- const forLoop = templateObject[':for']
346
- delete templateObject[':for']
347
- const range = templateObject['range'] || templateObject[':range'] || '{}'
348
- delete templateObject['range']
349
- delete templateObject[':range']
350
-
351
- const key = templateObject['key']
352
- const forKey = interpolate(key, 'scope.')
353
-
354
- const shallow = !!!(
355
- templateObject['$shallow'] && templateObject['$shallow'].toLowerCase() === 'false'
356
- )
357
- delete templateObject['$shallow']
358
-
359
- delete templateObject['key']
360
- const regex = /(.+)\s+in\s+(.+)/gi
361
- // const regex = /(:?\(*)(.+)\s+in\s+(.+)/gi
362
-
363
- const result = regex.exec(forLoop)
364
-
365
- // can be improved with a smarter regex
366
- const [item, index] = result[1]
367
- .replace('(', '')
368
- .replace(')', '')
369
- .split(/\s*,\s*/)
370
-
371
- const scopeRegex = new RegExp(`(scope\\.(?!${item}\\.|${index}|key)(\\w+))`, 'gi')
372
-
373
- // local context
374
- const ctx = {
375
- renderCode: [],
376
- effectsCode: [],
377
- context: { props: [], components: this.components },
378
- }
379
-
380
- if (parent) {
381
- ctx.renderCode.push(`parent = ${parent}`)
382
- }
383
-
384
- // If the index variable is not defined, the key attribute would not reference it.
385
- if (index !== undefined) {
386
- const indexRegex = new RegExp(`\\$${index}(?!['\\w])`)
387
- const indexResult = indexRegex.exec(key)
388
- if (Array.isArray(indexResult)) {
389
- ctx.renderCode.push(
390
- `console.warn(" Using '${index}' in the key, like key=${key}, is not recommended")`
391
- )
392
- }
393
- }
394
-
395
- const forStartCounter = counter
396
-
397
- ctx.renderCode.push(`
398
- const created${forStartCounter} = []
399
-
400
- let from${forStartCounter}
401
- let to${forStartCounter}
402
-
403
- const forloop${forStartCounter} = (collection = [], elms, created) => {
404
- const rawCollection = getRaw(collection)
405
- const keys = new Set()
406
- let l = rawCollection.length
407
-
408
- const range = ${interpolate(range)} || {}
409
- from${forStartCounter} = range['from'] || 0
410
- to${forStartCounter} = 'to' in range ? range['to'] : rawCollection.length
411
-
412
- while(l--) {
413
- const ${item} = rawCollection[l]
414
- `)
415
- // push reference of index variable
416
- if (index !== undefined) {
417
- ctx.renderCode.push(`
418
- const ${index} = l
419
- `)
420
- }
421
- ctx.renderCode.push(`
422
- if(l < to${forStartCounter} && l >= from${forStartCounter}) {
423
- keys.add('' + ${interpolate(key, '') || 'l'})
424
- }
425
- }
426
- `)
427
-
428
- // keep track of the index in the render code so we can inject
429
- // the code that takes care of destroying elements (generated later on)
430
- // in the right spot
431
- const indexToInjectDestroyCode = ctx.renderCode.length
432
-
433
- ctx.renderCode.push(`
434
- created.length = 0
435
- const length = rawCollection.length
436
- const effects = []
437
- for(let __index = 0; __index < length; __index++) {
438
- if(__index < from${forStartCounter} || __index >= to${forStartCounter}) continue
439
- const scope = Object.create(component)
440
- parent = ${parent}
441
- scope['${item}'] = rawCollection[__index]
442
- `)
443
- // If the index variable is declared, include it in the scope object
444
- if (index !== '') {
445
- ctx.renderCode.push(`
446
- scope['${index}'] = __index
447
- `)
448
- }
449
- ctx.renderCode.push(`
450
- scope['key'] = '' + ${forKey || '__index'}
451
- `)
452
- if ('ref' in templateObject && templateObject.ref.indexOf('$') === -1) {
453
- // automatically map the ref for each item in the loop based on the given ref key
454
- ctx.renderCode.push(`
455
- scope['__ref'] = '${templateObject.ref}' + __index
456
- `)
457
- templateObject.ref = '$__ref'
458
- }
459
-
460
- ctx.renderCode.push(`
461
- created.push(scope.key)
462
- `)
463
-
464
- if (
465
- templateObject[Symbol.for('componentType')] === 'Element' ||
466
- templateObject[Symbol.for('componentType')] === 'Slot' ||
467
- templateObject[Symbol.for('componentType')] === 'Text'
468
- ) {
469
- if (templateObject[Symbol.for('componentType')] === 'Text') {
470
- templateObject.__textnode = 'true'
471
- }
472
- generateElementCode.call(ctx, templateObject, parent, {
473
- key: 'scope.key',
474
- component: 'scope.',
475
- forceEffect: false,
476
- forloop: true,
477
- })
478
- } else {
479
- generateComponentCode.call(ctx, templateObject, false, {
480
- key: 'scope.key',
481
- component: 'scope.',
482
- forceEffect: false,
483
- forloop: true,
484
- })
485
- }
486
-
487
- // separate effects that only rely on variables in the itteration
488
- const innerScopeEffects = ctx.effectsCode.filter(
489
- (effect) => [...effect.matchAll(scopeRegex)].length === 0
490
- )
491
-
492
- // separate effects that (also) rely on variables in the outer scope
493
- const outerScopeEffects = ctx.effectsCode.filter(
494
- (effect) => [...effect.matchAll(scopeRegex)].length !== 0
495
- )
496
-
497
- if (shallow === false) {
498
- ctx.renderCode.push(`
499
- scope['${item}'] = null
500
- scope['${item}'] = collection[__index]
501
- `)
502
- }
503
-
504
- // inner scope variables are part of the main forloop
505
- innerScopeEffects.forEach((effect, index) => {
506
- const key = effect.indexOf(`scope.${index}`) > -1 ? `'${interpolate(result[2], '')}'` : null
507
- if (effect.indexOf("Symbol.for('props')") === -1) {
508
- ctx.renderCode.push(`
509
- let eff${index} = () => {
510
- ${effect}
511
- }
512
- effect(eff${index}, ${key})
513
- effects.push(eff${index})
514
- component[Symbol.for('effects')].push(eff${index})
515
- `)
516
- } else {
517
- // props shouldn't be wrapped in an effect, but simply passed on
518
- ctx.renderCode.push(`
519
- ${effect}
520
- `)
521
- }
522
- })
523
- ctx.renderCode.push(`
524
- }
525
- return effects
526
- }`)
527
-
528
- // generate code that destroys items
529
- const destroyCode = []
530
- destroyCode.push(`
531
- let i = created.length
532
-
533
- while (i--) {
534
- if (keys.has(created[i]) === false) {
535
- const key = created[i]
536
- `)
537
- const forEndCounter = counter
538
-
539
- for (let i = forStartCounter; i <= forEndCounter; i++) {
540
- destroyCode.push(`
541
- elms[${i}][key] && elms[${i}][key].destroy()
542
- delete elms[${i}][key]
543
- `)
544
- }
545
- destroyCode.push(`
546
- }
547
- }
548
- `)
549
-
550
- // inject the destroy code in the correct spot
551
- ctx.renderCode.splice(indexToInjectDestroyCode, 0, ...destroyCode)
552
-
553
- let effectKey = `${interpolate(result[2], '')}`
554
-
555
- // Get the last property from nested path
556
- if (effectKey && effectKey.includes('.')) {
557
- effectKey = effectKey.match(/[^.]+$/)[0]
558
- }
559
-
560
- // get the reference to range from and to
561
- const effectKeysRegex = /\$([^,} ]+)/g
562
- const effectKeys = [...range.matchAll(effectKeysRegex)].map((match) => `'${match[1]}'`)
563
-
564
- ctx.renderCode.push(`
565
- let forEffects${forStartCounter}
566
- const eff${forStartCounter} = () => {
567
- component[Symbol.for('removeGlobalEffects')](forEffects${forStartCounter})
568
- forEffects${forStartCounter} = null
569
- forEffects${forStartCounter} = forloop${forStartCounter}(${cast(
570
- result[2],
571
- ':for'
572
- )}, elms, created${forStartCounter})
573
- }
574
-
575
- component[Symbol.for('effects')].push(eff${forStartCounter})
576
-
577
- effect(eff${forStartCounter}, ['${effectKey}', ${effectKeys.join(',')}] )
578
- `)
579
-
580
- outerScopeEffects.forEach((effect, outerScopeEffectsIndex) => {
581
- const matches = [...effect.matchAll(scopeRegex)]
582
-
583
- let l = matches.length
584
- const refs = []
585
- while (l--) {
586
- const match = matches[l]
587
- const ref = `component.${match[2]}`
588
- refs.indexOf(ref) === -1 && refs.push(ref)
589
- // don't update the effect to point to component, if we're referring to a scope item
590
- if (match[2] !== item) {
591
- effect =
592
- effect.substring(0, match.index) + ref + effect.substring(match.index + match[1].length)
593
- }
594
- }
595
-
596
- ctx.renderCode.push(`
597
- const eff${forStartCounter}_${outerScopeEffectsIndex} = () => {
598
- void ${refs.join(', ')}
599
- for(let __index = 0; __index < ${interpolate(result[2])}.length; __index++) {
600
- if(__index < from${forStartCounter} || __index >= to${forStartCounter}) continue
601
- const scope = {}
602
- scope['${index}'] = __index
603
- scope['${item}'] = ${interpolate(result[2])}[__index]
604
- scope['key'] = ${forKey || '__index'}
605
- `)
606
-
607
- ctx.renderCode.push(`
608
- ${effect}
609
- }
610
- }
611
- component[Symbol.for('effects')].push(eff${forStartCounter}_${outerScopeEffectsIndex})
612
- effect(eff${forStartCounter}_${outerScopeEffectsIndex})
613
- `)
614
- })
615
-
616
- this.renderCode.push(ctx.renderCode.join('\n'))
617
- }
618
-
619
- const generateCode = function (templateObject, parent = false, options = {}) {
620
- templateObject.children &&
621
- templateObject.children.forEach((childTemplateObject) => {
622
- counter++
623
-
624
- if (Object.keys(childTemplateObject).indexOf(':for') > -1) {
625
- generateForLoopCode.call(this, childTemplateObject, parent)
626
- } else {
627
- if (
628
- childTemplateObject[Symbol.for('componentType')] === 'Element' ||
629
- childTemplateObject[Symbol.for('componentType')] === 'Slot' ||
630
- childTemplateObject[Symbol.for('componentType')] === 'Text' ||
631
- childTemplateObject[Symbol.for('componentType')] === 'Layout'
632
- ) {
633
- if (childTemplateObject[Symbol.for('componentType')] === 'Text') {
634
- childTemplateObject.__textnode = 'true'
635
- }
636
- if (childTemplateObject[Symbol.for('componentType')] === 'Layout') {
637
- childTemplateObject.__layout = 'true'
638
- }
639
- generateElementCode.call(this, childTemplateObject, parent, options)
640
- } else {
641
- generateComponentCode.call(this, childTemplateObject, parent, options)
642
- }
643
- }
644
- })
645
- }
646
-
647
- const interpolate = (val, component = 'component.') => {
648
- if (val === undefined) return val
649
- const replaceString = /('.*?')+/gi
650
- const replaceDollar = /\$(\$(?=\$)|\$?)/g
651
- const matches = val.matchAll(replaceString)
652
- const restore = []
653
-
654
- let i = 0
655
-
656
- // replace string and store in temp
657
- for (const match of matches) {
658
- restore.push(match[0])
659
- val = val.replace(match[0], `[@@REPLACEMENT${i}@@]`)
660
- i++
661
- }
662
-
663
- // replace dollar signs using the regex and function
664
- val = val.replace(replaceDollar, (match, group1) => {
665
- // single dollar sign
666
- if (group1 === '') {
667
- return component
668
- }
669
- // consecutive dollar sign, replace only the first $
670
- else if (group1 === '$') {
671
- return component + '$'
672
- }
673
- })
674
-
675
- // restore string replacement
676
- restore.forEach((el, idx) => {
677
- val = val.replace(`[@@REPLACEMENT${idx}@@]`, el)
678
- })
679
-
680
- return val
681
- }
682
-
683
- const cast = (val = '', key = false, component = 'component.') => {
684
- const dynamicArgumentRegex = /\$\w+/gi
685
- let castedValue
686
-
687
- // inline content
688
- if (key === 'content') {
689
- if (val.startsWith('$')) {
690
- castedValue = `${component}${val.replace('$', '')}`
691
- } else {
692
- // unescaped single quotes must be escaped while preserving escaped backslashes
693
- const escapedVal = val
694
- .replace(/\\\\/g, '__DOUBLE_BACKSLASH__')
695
- .replace(/(^|[^\\])'/g, "$1\\'")
696
- .replace(/__DOUBLE_BACKSLASH__/g, '\\\\')
697
- castedValue = `'${parseInlineContent(escapedVal, component)}'`
698
- }
699
- }
700
- // numeric
701
- else if (key !== 'color' && /[^0-9%\s.eE]/.test(val) === false && !isNaN(parseFloat(val))) {
702
- castedValue = parseFloat(val)
703
- if (val.endsWith('%')) {
704
- const map = {
705
- w: 'width',
706
- width: 'width',
707
- x: 'width',
708
- h: 'height',
709
- height: 'height',
710
- y: 'height',
711
- }
712
- const base = map[key]
713
- if (base) {
714
- castedValue = `parent.node.${base} * (${castedValue} / 100)`
715
- }
716
- }
717
- }
718
- // boolean true
719
- else if (val.toLowerCase() === 'true') {
720
- castedValue = true
721
- }
722
- // boolean false
723
- else if (val.toLowerCase() === 'false') {
724
- castedValue = false
725
- }
726
- // @-listener
727
- else if (key.startsWith('@') && val) {
728
- const c = component.slice(0, -1)
729
- castedValue = `${c}['${val.replace('$', '')}'] && ${c}['${val.replace('$', '')}'].bind(${c})`
730
- }
731
- // dynamic value
732
- else if (val.startsWith('$')) {
733
- castedValue = `${component}${val.replace('$', '')}`
734
- }
735
- // dynamic value in object
736
- else if (dynamicArgumentRegex.exec(val)) {
737
- const rex = /\w+\s*:\s*(?:[^\s,}]+|".*?"|'.*?')/g
738
- const results = val.match(rex)
739
- castedValue = {}
740
- if (results) {
741
- for (let i = 0; i < results.length; i++) {
742
- const members = results[i].split(/\s*:\s*/)
743
- if (members) {
744
- populateFields(members[0], members[1], component, castedValue)
745
- }
746
- }
747
- }
748
- return interpolateObject(castedValue)
749
- }
750
- // static string
751
- else {
752
- castedValue = `"${val}"`
753
- }
754
-
755
- return castedValue
756
- }
757
-
758
- const isReactiveKey = (str) => str.startsWith(':')
759
-
760
- const parseInlineContent = (val, component) => {
761
- const dynamicParts = /\{\{\s*(\$\S+)\s*\}\}/g
762
- const matches = [...val.matchAll(dynamicParts)]
763
-
764
- if (matches.length) {
765
- for (let [match, arg] of matches) {
766
- val = val.replace(match, `${arg.replace('$', `'+${component}`)}+'`)
767
- }
768
- }
769
- return val
770
- }
771
-
772
- // Populate value into target's prop
773
- const populateFields = (prop, val, component, target) => {
774
- if (val.startsWith('$')) {
775
- target[prop] = `${component}${val.replace('$', '')}`
776
- } else {
777
- target[prop] = val
778
- }
779
- }
780
-
781
- const interpolateObject = (input) => {
782
- const interpolatedResults = []
783
- Object.keys(input).forEach((key) => {
784
- interpolatedResults.push(`${key}: ${input[key]}`)
785
- })
786
-
787
- return ` { ${interpolatedResults.join(', ')} }`
788
- }
1
+ /*
2
+ * Copyright 2023 Comcast Cable Communications Management, LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ *
15
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+
18
+ let counter
19
+ let isDev
20
+
21
+ export default function (templateObject = { children: [] }, devMode = false) {
22
+ const ctx = {
23
+ renderCode: [
24
+ 'const elms = []',
25
+ 'let componentType',
26
+ 'const rootComponent = component',
27
+ 'let propData',
28
+ 'let slotComponent',
29
+ 'let inSlot = false',
30
+ 'let slotChildCounter = 0',
31
+ ],
32
+ effectsCode: [],
33
+ context: { props: [], components: this.components },
34
+ }
35
+
36
+ counter = -1
37
+ isDev = devMode
38
+ if (isDev === true) {
39
+ ctx.renderCode.push(`
40
+ function propInComponent(prop, kind="dynamic") {
41
+ const property = prop.includes('.') ? prop.split('.')[0] : prop
42
+ if (kind === 'reactive' || prop.includes('.') === false) {
43
+ if (property in component === false) {
44
+ Log.warn('Property ' + property + ' was accessed during render but is not defined on instance')
45
+ }
46
+ } else {
47
+ const nestedKeys = prop.split('.')
48
+ let base = component
49
+ for(let i =0; i<nestedKeys.length;i++){
50
+ if (base[nestedKeys[i]] === undefined) {
51
+ Log.warn('Property ' + nestedKeys.slice(0,i+1).join('.') + ' was accessed during render but is not defined on instance')
52
+ }
53
+ base = base[nestedKeys[i]]
54
+ }
55
+ }
56
+ }
57
+ `)
58
+ }
59
+ generateCode.call(ctx, templateObject)
60
+ ctx.renderCode.push('return elms')
61
+
62
+ return {
63
+ render: new Function(
64
+ 'parent',
65
+ 'component',
66
+ 'context',
67
+ 'components',
68
+ 'effect',
69
+ 'getRaw',
70
+ 'Log',
71
+ ctx.renderCode.join('\n')
72
+ ),
73
+ effects: ctx.effectsCode.map(
74
+ (code) =>
75
+ new Function('component', 'elms', 'context', 'components', 'rootComponent', 'effect', code)
76
+ ),
77
+ context: ctx.context,
78
+ }
79
+ }
80
+
81
+ // This is used to get only variable from expression
82
+ const extractVariables = function (value) {
83
+ const regEx = /\$\$?\w+(\.\w+)?/g
84
+ const matches = value.match(regEx)
85
+ if (matches !== null) {
86
+ const shaderRegex = /\$shader/
87
+ return matches.filter((match) => !shaderRegex.test(match))
88
+ } else {
89
+ return false
90
+ }
91
+ }
92
+
93
+ const verifyVariables = function (value, renderCode, type = 'dynamic') {
94
+ const variablesToBeVerified = extractVariables(value)
95
+ if (variablesToBeVerified !== false) {
96
+ for (let i = 0; i < variablesToBeVerified.length; i++) {
97
+ let variable = variablesToBeVerified[i]
98
+ if (type === 'reactive' && variable.includes('.')) {
99
+ variable = variable.split('.')[0]
100
+ }
101
+ renderCode.push(`propInComponent('${variable.replace('$', '')}', '${type}')`)
102
+ }
103
+ }
104
+ }
105
+
106
+ const generateElementCode = function (
107
+ templateObject,
108
+ parent = false,
109
+ options = { key: false, component: 'component.', forceEffect: false, forloop: false }
110
+ ) {
111
+ const renderCode = options.forceEffect ? this.effectsCode : this.renderCode
112
+ if (parent) {
113
+ renderCode.push(`parent = ${parent}`)
114
+ }
115
+
116
+ if ('key' in templateObject) options.key = interpolate(templateObject.key, options.component)
117
+ const elm = options.key ? `elms[${counter}][${options.key}]` : `elms[${counter}]`
118
+
119
+ if (options.key) {
120
+ renderCode.push(`
121
+ if(elms[${counter}] === undefined) {
122
+ elms[${counter}] = {}
123
+ }
124
+ `)
125
+ }
126
+
127
+ renderCode.push(`const elementConfig${counter} = {}`)
128
+
129
+ if (options.forloop) {
130
+ renderCode.push(`if(${elm} === undefined) {`)
131
+ }
132
+
133
+ renderCode.push(`
134
+ ${elm} = this.element({parent: parent || 'root'}, inSlot === true ? slotComponent : component)
135
+ `)
136
+
137
+ if (options.forloop) {
138
+ renderCode.push('}')
139
+ }
140
+
141
+ const children = templateObject['children']
142
+ delete templateObject['children']
143
+
144
+ if (templateObject[Symbol.for('componentType')] === 'Slot') {
145
+ renderCode.push(`elementConfig${counter}[Symbol.for('isSlot')] = true`)
146
+ }
147
+
148
+ Object.keys(templateObject).forEach((key) => {
149
+ if (key === 'slot') {
150
+ renderCode.push(`
151
+ elementConfig${counter}['parent'] = slotComponent[Symbol.for('slots')] !== undefined && Array.isArray(slotComponent[Symbol.for('slots')]) === true && slotComponent[Symbol.for('slots')].filter(slot => slot.ref === '${templateObject.slot}').shift() || parent
152
+ `)
153
+ }
154
+
155
+ if (key === 'key') return
156
+
157
+ const value = templateObject[key]
158
+
159
+ if (isReactiveKey(key)) {
160
+ if (options.holder && key === ':color') return
161
+ if (options.holder) {
162
+ this.effectsCode.push(`
163
+ if(typeof skip${counter} === 'undefined' ||
164
+ skip${counter}.indexOf('${key.substring(1)}') === -1)
165
+ ${elm}.set('${key.substring(1)}', ${interpolate(templateObject[key], options.component)})
166
+ `)
167
+ } else {
168
+ this.effectsCode.push(`
169
+ ${elm}.set('${key.substring(1)}', ${interpolate(
170
+ templateObject[key],
171
+ options.component
172
+ )})
173
+ `)
174
+ }
175
+ // value.includes('.') === false &&
176
+ if (isDev === true && options.component !== 'scope.' && value.includes('$')) {
177
+ verifyVariables(value, renderCode, 'reactive')
178
+ }
179
+ renderCode.push(
180
+ `elementConfig${counter}['${key.substring(1)}'] = ${interpolate(value, options.component)}`
181
+ )
182
+ } else {
183
+ if (isDev === true && options.component !== 'scope.' && value.includes('$')) {
184
+ verifyVariables(value, renderCode)
185
+ }
186
+ renderCode.push(`elementConfig${counter}['${key}'] = ${cast(value, key, options.component)}`)
187
+ }
188
+ })
189
+
190
+ if (options.holder === true) {
191
+ renderCode.push(`
192
+ const skip${counter} = []
193
+ if(typeof cmp${counter} !== 'undefined') {
194
+ for(let key in cmp${counter}[Symbol.for('config')].props) {
195
+ delete elementConfig${counter}[cmp${counter}[Symbol.for('config')].props[key]]
196
+ skip${counter}.push(cmp${counter}[Symbol.for('config')].props[key])
197
+ }
198
+ }
199
+ `)
200
+ }
201
+
202
+ if (options.forloop) {
203
+ renderCode.push(`if(${elm}.nodeId === undefined) {`)
204
+ }
205
+
206
+ renderCode.push(`${elm}.populate(elementConfig${counter})`)
207
+
208
+ renderCode.push(`
209
+ if(inSlot === true) {
210
+ slotChildCounter -= 1
211
+ }
212
+ `)
213
+
214
+ if (options.forloop) {
215
+ renderCode.push('}')
216
+ }
217
+
218
+ if (children) {
219
+ generateCode.call(this, { children }, `${elm}`, options)
220
+ }
221
+ }
222
+
223
+ const generateComponentCode = function (
224
+ templateObject,
225
+ parent = false,
226
+ options = {
227
+ key: false,
228
+ component: 'component.',
229
+ forceEffect: false,
230
+ holder: false,
231
+ forloop: false,
232
+ }
233
+ ) {
234
+ const renderCode = options.forceEffect ? this.effectsCode : this.renderCode
235
+
236
+ renderCode.push(`
237
+ const cmp${counter} =
238
+ (context.components && context.components['${
239
+ templateObject[Symbol.for('componentType')]
240
+ }']) || components['${templateObject[Symbol.for('componentType')]}']
241
+ `)
242
+
243
+ if ('key' in templateObject) {
244
+ options.key = interpolate(templateObject.key, options.component)
245
+ }
246
+
247
+ const children = templateObject.children
248
+ delete templateObject.children
249
+ generateElementCode.call(this, templateObject, parent, { ...options, ...{ holder: true } })
250
+
251
+ parent = options.key ? `elms[${counter}][${options.key}]` : `elms[${counter}]`
252
+
253
+ counter++
254
+
255
+ const elm = options.key ? `elms[${counter}][${options.key}]` : `elms[${counter}]`
256
+
257
+ if (options.key) {
258
+ renderCode.push(`
259
+ if(elms[${counter}] === undefined) {
260
+ elms[${counter}] = {}
261
+ }
262
+ `)
263
+ }
264
+
265
+ if (parent) {
266
+ renderCode.push(`parent = ${parent}`)
267
+ }
268
+
269
+ renderCode.push(`const props${counter} = {}`)
270
+
271
+ if (options.forloop) {
272
+ renderCode.push(`if(${elm} === undefined) {`)
273
+ }
274
+
275
+ Object.keys(templateObject).forEach((key) => {
276
+ if (isReactiveKey(key)) {
277
+ this.effectsCode.push(`
278
+ ${elm}[Symbol.for('props')]['${key.substring(1)}'] = ${interpolate(
279
+ templateObject[key],
280
+ options.component
281
+ )}`)
282
+ renderCode.push(`
283
+ propData = ${interpolate(templateObject[key], options.component)}
284
+ if (Array.isArray(propData) === true) {
285
+ propData = getRaw(propData).slice(0)
286
+ }
287
+ props${counter}['${key.substring(1)}'] = propData`)
288
+ } else {
289
+ renderCode.push(
290
+ `props${counter}['${key}'] = ${cast(templateObject[key], key, options.component)}`
291
+ )
292
+ }
293
+ })
294
+
295
+ renderCode.push(`
296
+ componentType = props${counter}['is'] || '${templateObject[Symbol.for('componentType')]}'
297
+
298
+ let component${counter}
299
+ if(typeof componentType === 'string') {
300
+ component${counter} = context.components && context.components[componentType] || components[componentType]
301
+ if(!component${counter}) {
302
+ throw new Error('Component "${templateObject[Symbol.for('componentType')]}" not found')
303
+ }
304
+ } else if(typeof componentType === 'function' && componentType[Symbol.for('isComponent')] === true) {
305
+ component${counter} = componentType
306
+ }
307
+
308
+ ${elm} = component${counter}.call(null, {props: props${counter}}, ${parent}, component)
309
+
310
+ if (${elm}[Symbol.for('slots')][0]) {
311
+ parent = ${elm}[Symbol.for('slots')][0]
312
+ slotComponent = ${elm}
313
+ inSlot = true
314
+ } else {
315
+ parent = ${elm}[Symbol.for('children')][0]
316
+ }
317
+ `)
318
+
319
+ if (options.forloop) {
320
+ renderCode.push('}')
321
+ }
322
+
323
+ if (children) {
324
+ if (!options.forloop) {
325
+ renderCode.push(`
326
+ if(inSlot === true) {
327
+ slotChildCounter = ${children.length} + 1
328
+ }
329
+ `)
330
+ }
331
+ counter++
332
+ generateElementCode.call(this, { children }, false, { ...options })
333
+ }
334
+
335
+ if (!options.forloop) {
336
+ renderCode.push(`
337
+ if (inSlot === true && slotChildCounter === 0) {
338
+ inSlot = false
339
+ }
340
+ `)
341
+ }
342
+ }
343
+
344
+ const generateForLoopCode = function (templateObject, parent) {
345
+ const forLoop = templateObject[':for']
346
+ delete templateObject[':for']
347
+ const range = templateObject['range'] || templateObject[':range'] || '{}'
348
+ delete templateObject['range']
349
+ delete templateObject[':range']
350
+
351
+ const key = templateObject['key']
352
+ const forKey = interpolate(key, 'scope.')
353
+
354
+ const shallow = !!!(
355
+ templateObject['$shallow'] && templateObject['$shallow'].toLowerCase() === 'false'
356
+ )
357
+ delete templateObject['$shallow']
358
+
359
+ delete templateObject['key']
360
+ const regex = /(.+)\s+in\s+(.+)/gi
361
+ // const regex = /(:?\(*)(.+)\s+in\s+(.+)/gi
362
+
363
+ const result = regex.exec(forLoop)
364
+
365
+ // can be improved with a smarter regex
366
+ const [item, index] = result[1]
367
+ .replace('(', '')
368
+ .replace(')', '')
369
+ .split(/\s*,\s*/)
370
+
371
+ const scopeRegex = new RegExp(`(scope\\.(?!${item}\\.|${index}|key)(\\w+))`, 'gi')
372
+
373
+ // local context
374
+ const ctx = {
375
+ renderCode: [],
376
+ effectsCode: [],
377
+ context: { props: [], components: this.components },
378
+ }
379
+
380
+ if (parent) {
381
+ ctx.renderCode.push(`parent = ${parent}`)
382
+ }
383
+
384
+ // If the index variable is not defined, the key attribute would not reference it.
385
+ if (index !== undefined) {
386
+ const indexRegex = new RegExp(`\\$${index}(?!['\\w])`)
387
+ const indexResult = indexRegex.exec(key)
388
+ if (Array.isArray(indexResult)) {
389
+ ctx.renderCode.push(
390
+ `console.warn(" Using '${index}' in the key, like key=${key}, is not recommended")`
391
+ )
392
+ }
393
+ }
394
+
395
+ const forStartCounter = counter
396
+
397
+ ctx.renderCode.push(`
398
+ const created${forStartCounter} = []
399
+
400
+ let from${forStartCounter}
401
+ let to${forStartCounter}
402
+
403
+ const forloop${forStartCounter} = (collection = [], elms, created) => {
404
+ const rawCollection = getRaw(collection)
405
+ const keys = new Set()
406
+ let l = rawCollection.length
407
+
408
+ const range = ${interpolate(range)} || {}
409
+ from${forStartCounter} = range['from'] || 0
410
+ to${forStartCounter} = 'to' in range ? range['to'] : rawCollection.length
411
+
412
+ while(l--) {
413
+ const ${item} = rawCollection[l]
414
+ `)
415
+ // push reference of index variable
416
+ if (index !== undefined) {
417
+ ctx.renderCode.push(`
418
+ const ${index} = l
419
+ `)
420
+ }
421
+ ctx.renderCode.push(`
422
+ if(l < to${forStartCounter} && l >= from${forStartCounter}) {
423
+ keys.add('' + ${interpolate(key, '') || 'l'})
424
+ }
425
+ }
426
+ `)
427
+
428
+ // keep track of the index in the render code so we can inject
429
+ // the code that takes care of destroying elements (generated later on)
430
+ // in the right spot
431
+ const indexToInjectDestroyCode = ctx.renderCode.length
432
+
433
+ ctx.renderCode.push(`
434
+ created.length = 0
435
+ const length = rawCollection.length
436
+ const effects = []
437
+ for(let __index = 0; __index < length; __index++) {
438
+ if(__index < from${forStartCounter} || __index >= to${forStartCounter}) continue
439
+ const scope = Object.create(component)
440
+ parent = ${parent}
441
+ scope['${item}'] = rawCollection[__index]
442
+ `)
443
+ // If the index variable is declared, include it in the scope object
444
+ if (index !== '') {
445
+ ctx.renderCode.push(`
446
+ scope['${index}'] = __index
447
+ `)
448
+ }
449
+ ctx.renderCode.push(`
450
+ scope['key'] = '' + ${forKey || '__index'}
451
+ `)
452
+ if ('ref' in templateObject && templateObject.ref.indexOf('$') === -1) {
453
+ // automatically map the ref for each item in the loop based on the given ref key
454
+ ctx.renderCode.push(`
455
+ scope['__ref'] = '${templateObject.ref}' + __index
456
+ `)
457
+ templateObject.ref = '$__ref'
458
+ }
459
+
460
+ ctx.renderCode.push(`
461
+ created.push(scope.key)
462
+ `)
463
+
464
+ if (
465
+ templateObject[Symbol.for('componentType')] === 'Element' ||
466
+ templateObject[Symbol.for('componentType')] === 'Slot' ||
467
+ templateObject[Symbol.for('componentType')] === 'Text'
468
+ ) {
469
+ if (templateObject[Symbol.for('componentType')] === 'Text') {
470
+ templateObject.__textnode = 'true'
471
+ }
472
+ generateElementCode.call(ctx, templateObject, parent, {
473
+ key: 'scope.key',
474
+ component: 'scope.',
475
+ forceEffect: false,
476
+ forloop: true,
477
+ })
478
+ } else {
479
+ generateComponentCode.call(ctx, templateObject, false, {
480
+ key: 'scope.key',
481
+ component: 'scope.',
482
+ forceEffect: false,
483
+ forloop: true,
484
+ })
485
+ }
486
+
487
+ // separate effects that only rely on variables in the itteration
488
+ const innerScopeEffects = ctx.effectsCode.filter(
489
+ (effect) => [...effect.matchAll(scopeRegex)].length === 0
490
+ )
491
+
492
+ // separate effects that (also) rely on variables in the outer scope
493
+ const outerScopeEffects = ctx.effectsCode.filter(
494
+ (effect) => [...effect.matchAll(scopeRegex)].length !== 0
495
+ )
496
+
497
+ if (shallow === false) {
498
+ ctx.renderCode.push(`
499
+ scope['${item}'] = null
500
+ scope['${item}'] = collection[__index]
501
+ `)
502
+ }
503
+
504
+ // inner scope variables are part of the main forloop
505
+ innerScopeEffects.forEach((effect, index) => {
506
+ const key = effect.indexOf(`scope.${index}`) > -1 ? `'${interpolate(result[2], '')}'` : null
507
+ if (effect.indexOf("Symbol.for('props')") === -1) {
508
+ ctx.renderCode.push(`
509
+ let eff${index} = () => {
510
+ ${effect}
511
+ }
512
+ effect(eff${index}, ${key})
513
+ effects.push(eff${index})
514
+ component[Symbol.for('effects')].push(eff${index})
515
+ `)
516
+ } else {
517
+ // props shouldn't be wrapped in an effect, but simply passed on
518
+ ctx.renderCode.push(`
519
+ ${effect}
520
+ `)
521
+ }
522
+ })
523
+ ctx.renderCode.push(`
524
+ }
525
+ return effects
526
+ }`)
527
+
528
+ // generate code that destroys items
529
+ const destroyCode = []
530
+ destroyCode.push(`
531
+ let i = created.length
532
+
533
+ while (i--) {
534
+ if (keys.has(created[i]) === false) {
535
+ const key = created[i]
536
+ `)
537
+ const forEndCounter = counter
538
+
539
+ for (let i = forStartCounter; i <= forEndCounter; i++) {
540
+ destroyCode.push(`
541
+ elms[${i}][key] && elms[${i}][key].destroy()
542
+ delete elms[${i}][key]
543
+ `)
544
+ }
545
+ destroyCode.push(`
546
+ }
547
+ }
548
+ `)
549
+
550
+ // inject the destroy code in the correct spot
551
+ ctx.renderCode.splice(indexToInjectDestroyCode, 0, ...destroyCode)
552
+
553
+ let effectKey = `${interpolate(result[2], '')}`
554
+
555
+ // Get the last property from nested path
556
+ if (effectKey && effectKey.includes('.')) {
557
+ effectKey = effectKey.match(/[^.]+$/)[0]
558
+ }
559
+
560
+ // get the reference to range from and to
561
+ const effectKeysRegex = /\$([^,} ]+)/g
562
+ const effectKeys = [...range.matchAll(effectKeysRegex)].map((match) => `'${match[1]}'`)
563
+
564
+ ctx.renderCode.push(`
565
+ let forEffects${forStartCounter}
566
+ const eff${forStartCounter} = () => {
567
+ component[Symbol.for('removeGlobalEffects')](forEffects${forStartCounter})
568
+ forEffects${forStartCounter} = null
569
+ forEffects${forStartCounter} = forloop${forStartCounter}(${cast(
570
+ result[2],
571
+ ':for'
572
+ )}, elms, created${forStartCounter})
573
+ }
574
+
575
+ component[Symbol.for('effects')].push(eff${forStartCounter})
576
+
577
+ effect(eff${forStartCounter}, ['${effectKey}', ${effectKeys.join(',')}] )
578
+ `)
579
+
580
+ outerScopeEffects.forEach((effect, outerScopeEffectsIndex) => {
581
+ const matches = [...effect.matchAll(scopeRegex)]
582
+
583
+ let l = matches.length
584
+ const refs = []
585
+ while (l--) {
586
+ const match = matches[l]
587
+ const ref = `component.${match[2]}`
588
+ refs.indexOf(ref) === -1 && refs.push(ref)
589
+ // don't update the effect to point to component, if we're referring to a scope item
590
+ if (match[2] !== item) {
591
+ effect =
592
+ effect.substring(0, match.index) + ref + effect.substring(match.index + match[1].length)
593
+ }
594
+ }
595
+
596
+ ctx.renderCode.push(`
597
+ const eff${forStartCounter}_${outerScopeEffectsIndex} = () => {
598
+ void ${refs.join(', ')}
599
+ for(let __index = 0; __index < ${interpolate(result[2])}.length; __index++) {
600
+ if(__index < from${forStartCounter} || __index >= to${forStartCounter}) continue
601
+ const scope = {}
602
+ scope['${index}'] = __index
603
+ scope['${item}'] = ${interpolate(result[2])}[__index]
604
+ scope['key'] = ${forKey || '__index'}
605
+ `)
606
+
607
+ ctx.renderCode.push(`
608
+ ${effect}
609
+ }
610
+ }
611
+ component[Symbol.for('effects')].push(eff${forStartCounter}_${outerScopeEffectsIndex})
612
+ effect(eff${forStartCounter}_${outerScopeEffectsIndex})
613
+ `)
614
+ })
615
+
616
+ this.renderCode.push(ctx.renderCode.join('\n'))
617
+ }
618
+
619
+ const generateCode = function (templateObject, parent = false, options = {}) {
620
+ templateObject.children &&
621
+ templateObject.children.forEach((childTemplateObject) => {
622
+ counter++
623
+
624
+ if (Object.keys(childTemplateObject).indexOf(':for') > -1) {
625
+ generateForLoopCode.call(this, childTemplateObject, parent)
626
+ } else {
627
+ if (
628
+ childTemplateObject[Symbol.for('componentType')] === 'Element' ||
629
+ childTemplateObject[Symbol.for('componentType')] === 'Slot' ||
630
+ childTemplateObject[Symbol.for('componentType')] === 'Text' ||
631
+ childTemplateObject[Symbol.for('componentType')] === 'Layout'
632
+ ) {
633
+ if (childTemplateObject[Symbol.for('componentType')] === 'Text') {
634
+ childTemplateObject.__textnode = 'true'
635
+ }
636
+ if (childTemplateObject[Symbol.for('componentType')] === 'Layout') {
637
+ childTemplateObject.__layout = 'true'
638
+ }
639
+ generateElementCode.call(this, childTemplateObject, parent, options)
640
+ } else {
641
+ generateComponentCode.call(this, childTemplateObject, parent, options)
642
+ }
643
+ }
644
+ })
645
+ }
646
+
647
+ const interpolate = (val, component = 'component.') => {
648
+ if (val === undefined) return val
649
+ const replaceString = /('.*?')+/gi
650
+ const replaceDollar = /\$(\$(?=\$)|\$?)/g
651
+ const matches = val.matchAll(replaceString)
652
+ const restore = []
653
+
654
+ let i = 0
655
+
656
+ // replace string and store in temp
657
+ for (const match of matches) {
658
+ restore.push(match[0])
659
+ val = val.replace(match[0], `[@@REPLACEMENT${i}@@]`)
660
+ i++
661
+ }
662
+
663
+ // replace dollar signs using the regex and function
664
+ val = val.replace(replaceDollar, (match, group1) => {
665
+ // single dollar sign
666
+ if (group1 === '') {
667
+ return component
668
+ }
669
+ // consecutive dollar sign, replace only the first $
670
+ else if (group1 === '$') {
671
+ return component + '$'
672
+ }
673
+ })
674
+
675
+ // restore string replacement
676
+ restore.forEach((el, idx) => {
677
+ val = val.replace(`[@@REPLACEMENT${idx}@@]`, el)
678
+ })
679
+
680
+ return val
681
+ }
682
+
683
+ const cast = (val = '', key = false, component = 'component.') => {
684
+ const dynamicArgumentRegex = /\$\w+/gi
685
+ let castedValue
686
+
687
+ // inline content
688
+ if (key === 'content') {
689
+ if (val.startsWith('$')) {
690
+ castedValue = `${component}${val.replace('$', '')}`
691
+ } else {
692
+ // unescaped single quotes must be escaped while preserving escaped backslashes
693
+ const escapedVal = val
694
+ .replace(/\\\\/g, '__DOUBLE_BACKSLASH__')
695
+ .replace(/(^|[^\\])'/g, "$1\\'")
696
+ .replace(/__DOUBLE_BACKSLASH__/g, '\\\\')
697
+ castedValue = `'${parseInlineContent(escapedVal, component)}'`
698
+ }
699
+ }
700
+ // numeric
701
+ else if (key !== 'color' && /[^0-9%\s.eE]/.test(val) === false && !isNaN(parseFloat(val))) {
702
+ castedValue = parseFloat(val)
703
+ if (val.endsWith('%')) {
704
+ const map = {
705
+ w: 'width',
706
+ width: 'width',
707
+ x: 'width',
708
+ h: 'height',
709
+ height: 'height',
710
+ y: 'height',
711
+ }
712
+ const base = map[key]
713
+ if (base) {
714
+ castedValue = `parent.node.${base} * (${castedValue} / 100)`
715
+ }
716
+ }
717
+ }
718
+ // boolean true
719
+ else if (val.toLowerCase() === 'true') {
720
+ castedValue = true
721
+ }
722
+ // boolean false
723
+ else if (val.toLowerCase() === 'false') {
724
+ castedValue = false
725
+ }
726
+ // @-listener
727
+ else if (key.startsWith('@') && val) {
728
+ const c = component.slice(0, -1)
729
+ castedValue = `${c}['${val.replace('$', '')}'] && ${c}['${val.replace('$', '')}'].bind(${c})`
730
+ }
731
+ // dynamic value
732
+ else if (val.startsWith('$')) {
733
+ castedValue = `${component}${val.replace('$', '')}`
734
+ }
735
+ // dynamic value in object
736
+ else if (dynamicArgumentRegex.exec(val)) {
737
+ const rex = /\w+\s*:\s*(?:[^\s,}]+|".*?"|'.*?')/g
738
+ const results = val.match(rex)
739
+ castedValue = {}
740
+ if (results) {
741
+ for (let i = 0; i < results.length; i++) {
742
+ const members = results[i].split(/\s*:\s*/)
743
+ if (members) {
744
+ populateFields(members[0], members[1], component, castedValue)
745
+ }
746
+ }
747
+ }
748
+ return interpolateObject(castedValue)
749
+ }
750
+ // static string
751
+ else {
752
+ castedValue = `"${val}"`
753
+ }
754
+
755
+ return castedValue
756
+ }
757
+
758
+ const isReactiveKey = (str) => str.startsWith(':')
759
+
760
+ const parseInlineContent = (val, component) => {
761
+ const dynamicParts = /\{\{\s*(\$\S+)\s*\}\}/g
762
+ const matches = [...val.matchAll(dynamicParts)]
763
+
764
+ if (matches.length) {
765
+ for (let [match, arg] of matches) {
766
+ val = val.replace(match, `${arg.replace('$', `'+${component}`)}+'`)
767
+ }
768
+ }
769
+ return val
770
+ }
771
+
772
+ // Populate value into target's prop
773
+ const populateFields = (prop, val, component, target) => {
774
+ if (val.startsWith('$')) {
775
+ target[prop] = `${component}${val.replace('$', '')}`
776
+ } else {
777
+ target[prop] = val
778
+ }
779
+ }
780
+
781
+ const interpolateObject = (input) => {
782
+ const interpolatedResults = []
783
+ Object.keys(input).forEach((key) => {
784
+ interpolatedResults.push(`${key}: ${input[key]}`)
785
+ })
786
+
787
+ return ` { ${interpolatedResults.join(', ')} }`
788
+ }