@lightningjs/blits 1.30.1 → 1.30.2

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 (149) hide show
  1. package/.editorconfig +7 -7
  2. package/.eslintignore +1 -1
  3. package/.eslintrc.cjs +65 -65
  4. package/.github/workflows/comment-lint-results.yml +124 -124
  5. package/.github/workflows/comment-test-results.yml +108 -108
  6. package/.github/workflows/deploy-docs.yml +27 -27
  7. package/.github/workflows/run-blits-tests.yml +141 -141
  8. package/.github/workflows/run-lint.yml +127 -127
  9. package/.husky/pre-commit +1 -1
  10. package/.vscode/extensions.json +7 -7
  11. package/.vscode/settings.json +9 -9
  12. package/CHANGELOG.md +1019 -1012
  13. package/COPYING +1 -0
  14. package/LICENSE +202 -202
  15. package/NOTICE +12 -12
  16. package/README.md +33 -33
  17. package/bin/index.js +8 -8
  18. package/blits.d.ts +6 -6
  19. package/docs/README.md +19 -19
  20. package/docs/_sidebar.md +37 -37
  21. package/docs/built-in/directives.md +50 -50
  22. package/docs/built-in/for-loop.md +203 -203
  23. package/docs/built-in/layout.md +168 -168
  24. package/docs/components/component_state.md +90 -90
  25. package/docs/components/computed_properties.md +45 -45
  26. package/docs/components/lifecycle_events.md +42 -42
  27. package/docs/components/methods.md +45 -45
  28. package/docs/components/props.md +56 -56
  29. package/docs/components/user_input.md +186 -186
  30. package/docs/components/utility_methods.md +320 -320
  31. package/docs/components/watchers.md +34 -34
  32. package/docs/essentials/application_root.md +19 -19
  33. package/docs/essentials/components.md +68 -68
  34. package/docs/essentials/displaying_images.md +67 -67
  35. package/docs/essentials/displaying_text.md +135 -135
  36. package/docs/essentials/element_attributes.md +195 -195
  37. package/docs/essentials/settings.md +117 -117
  38. package/docs/essentials/template_syntax.md +83 -83
  39. package/docs/getting_started/file_structure.md +58 -58
  40. package/docs/getting_started/getting_started.md +45 -45
  41. package/docs/getting_started/intro.md +20 -20
  42. package/docs/index.html +47 -47
  43. package/docs/performance/lazy-loading.md +161 -161
  44. package/docs/plugins/global_app_state.md +98 -98
  45. package/docs/plugins/language.md +172 -172
  46. package/docs/plugins/text-to-speech-announcer.md +119 -119
  47. package/docs/plugins/theme.md +176 -176
  48. package/docs/router/basics.md +175 -175
  49. package/docs/router/hooks.md +70 -70
  50. package/docs/router/transitions.md +146 -146
  51. package/docs/sidebar.json +171 -171
  52. package/docs/transitions_animations/transitions.md +150 -150
  53. package/index.d.ts +1099 -1099
  54. package/index.js +28 -28
  55. package/jsconfig.json +12 -12
  56. package/package.json +56 -56
  57. package/public/assets/fps_sprite.base64.js +1 -1
  58. package/scripts/postpublish.js +59 -59
  59. package/scripts/prepublishOnly.js +84 -84
  60. package/scripts/prepush.js +78 -78
  61. package/src/announcer/announcer.js +162 -162
  62. package/src/announcer/speechSynthesis.js +107 -107
  63. package/src/application.js +100 -100
  64. package/src/component/base/announcer.js +27 -27
  65. package/src/component/base/events.js +69 -69
  66. package/src/component/base/index.js +33 -33
  67. package/src/component/base/methods.js +244 -244
  68. package/src/component/base/router.js +41 -41
  69. package/src/component/base/timeouts_intervals.js +99 -99
  70. package/src/component/base/utils.js +72 -72
  71. package/src/component/setup/computed.js +48 -48
  72. package/src/component/setup/index.js +62 -62
  73. package/src/component/setup/input.js +30 -30
  74. package/src/component/setup/methods.js +37 -37
  75. package/src/component/setup/props.js +62 -62
  76. package/src/component/setup/props.test.js +238 -238
  77. package/src/component/setup/routes.js +42 -42
  78. package/src/component/setup/state.js +59 -59
  79. package/src/component/setup/watch.js +35 -35
  80. package/src/component.js +437 -437
  81. package/src/component.test.js +388 -388
  82. package/src/components/Circle.js +73 -73
  83. package/src/components/Circle.orig.js +31 -31
  84. package/src/components/FPScounter.js +1510 -1510
  85. package/src/components/FPScounter.orig.js +121 -121
  86. package/src/components/RouterView.js +81 -81
  87. package/src/components/RouterView.orig.js +54 -54
  88. package/src/components/Sprite.js +94 -94
  89. package/src/components/Sprite.orig.js +58 -58
  90. package/src/components/index.js +28 -28
  91. package/src/components/index.orig.js +28 -28
  92. package/src/constants.js +21 -21
  93. package/src/engine.js +1 -1
  94. package/src/engines/L3/element.js +744 -744
  95. package/src/engines/L3/element.test.js +579 -579
  96. package/src/engines/L3/fontLoader.js +38 -38
  97. package/src/engines/L3/index.js +7 -7
  98. package/src/engines/L3/launch.js +123 -123
  99. package/src/engines/L3/shaderLoader.js +12 -12
  100. package/src/engines/index.js +1 -1
  101. package/src/focus.js +90 -90
  102. package/src/helpers/deepEqualArray.js +50 -50
  103. package/src/helpers/deepEqualArray.test.js +127 -127
  104. package/src/helpers/fetchJson.js +34 -34
  105. package/src/helpers/sequence.js +26 -26
  106. package/src/launch.js +123 -123
  107. package/src/lib/blitsfileconverter/blitsfileconverter.js +52 -52
  108. package/src/lib/codegenerator/generator.js +788 -788
  109. package/src/lib/codegenerator/generator.test.js +3801 -3801
  110. package/src/lib/colors/colors.js +92 -92
  111. package/src/lib/colors/colors.test.js +290 -290
  112. package/src/lib/colors/htmlColors.js +168 -168
  113. package/src/lib/componentId.js +49 -49
  114. package/src/lib/componentId.test.js +84 -84
  115. package/src/lib/eventListeners.js +123 -123
  116. package/src/lib/eventListeners.test.js +236 -236
  117. package/src/lib/hooks.js +59 -59
  118. package/src/lib/lifecycle.js +85 -85
  119. package/src/lib/log.js +117 -117
  120. package/src/lib/precompiler/precompiler.js +73 -73
  121. package/src/lib/reactivity/effect.js +93 -93
  122. package/src/lib/reactivity/effect.test.js +35 -35
  123. package/src/lib/reactivity/reactive.js +208 -208
  124. package/src/lib/reactivity/reactive.test.js +27 -27
  125. package/src/lib/reactivityguard/computedprops.js +218 -218
  126. package/src/lib/symbols.js +108 -108
  127. package/src/lib/templateparser/parser.js +386 -386
  128. package/src/lib/templateparser/parser.test.js +1411 -1411
  129. package/src/lib/utils.js +27 -27
  130. package/src/plugin.js +27 -27
  131. package/src/plugins/appstate.js +26 -26
  132. package/src/plugins/index.js +20 -20
  133. package/src/plugins/language.js +152 -152
  134. package/src/plugins/log.js +25 -25
  135. package/src/plugins/theme.js +94 -94
  136. package/src/router/router.js +514 -514
  137. package/src/router/router.test.js +473 -473
  138. package/src/router/transitions/fadeIn.js +27 -27
  139. package/src/router/transitions/fadeInOut.js +33 -33
  140. package/src/router/transitions/index.js +21 -21
  141. package/src/router/transitions/slideIn.js +28 -28
  142. package/src/router/transitions/slideInOut.js +33 -33
  143. package/src/settings.js +40 -40
  144. package/vite/blitsFileConverter.js +67 -67
  145. package/vite/index.js +28 -28
  146. package/vite/msdfGenerator.js +269 -269
  147. package/vite/preCompiler.js +42 -42
  148. package/vite/reactivityGuard.js +45 -45
  149. package/vscode/data/template-attributes.json +589 -589
@@ -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
+ }