@lightningjs/blits 1.2.0 → 1.3.1

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 (38) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/NOTICE +9 -0
  3. package/docs/built-in/directives.md +4 -6
  4. package/docs/built-in/for-loop.md +6 -8
  5. package/docs/components/component_state.md +3 -5
  6. package/docs/components/computed_properties.md +3 -5
  7. package/docs/components/lifecycle_events.md +2 -4
  8. package/docs/components/methods.md +1 -3
  9. package/docs/components/props.md +3 -5
  10. package/docs/components/user_input.md +6 -8
  11. package/docs/components/utilities.md +1 -2
  12. package/docs/components/watchers.md +2 -4
  13. package/docs/essentials/application_root.md +1 -3
  14. package/docs/essentials/components.md +2 -4
  15. package/docs/essentials/displaying_images.md +4 -6
  16. package/docs/essentials/displaying_text.md +4 -6
  17. package/docs/essentials/element_attributes.md +10 -12
  18. package/docs/essentials/template_syntax.md +5 -7
  19. package/docs/getting_started/file_structure.md +9 -11
  20. package/docs/getting_started/getting_started.md +5 -7
  21. package/docs/plugins/language.md +8 -10
  22. package/docs/plugins/text-to-speech-announcer.md +2 -4
  23. package/docs/transitions_animations/transitions.md +5 -7
  24. package/package.json +1 -1
  25. package/src/component/base/index.js +2 -3
  26. package/src/component/base/methods.js +1 -2
  27. package/src/component/setup/state.js +9 -13
  28. package/src/component.js +12 -17
  29. package/src/components/FPScounter.js +56 -12
  30. package/src/engines/L3/element.js +55 -65
  31. package/src/lib/codegenerator/generator.js +91 -24
  32. package/src/lib/codegenerator/generator.test.js +132 -0
  33. package/src/lib/colors/colors.js +6 -7
  34. package/src/lib/reactivity/reactive.js +3 -5
  35. package/src/lib/symbols.js +1 -1
  36. package/src/lib/templateparser/parser.js +1 -1
  37. package/src/plugin.js +6 -1
  38. package/src/{component/base → plugins}/log.js +4 -6
@@ -23,7 +23,7 @@ import symbols from '../../lib/symbols.js'
23
23
  import Settings from '../../settings.js'
24
24
 
25
25
  const isTransition = (value) => {
26
- return value !== null && typeof value === 'object' && 'transition' in value
26
+ return value !== null && typeof value === 'object' && 'transition' in value === true
27
27
  }
28
28
 
29
29
  const isObjectString = (str) => {
@@ -48,13 +48,12 @@ const parsePercentage = function (v, base) {
48
48
  }
49
49
 
50
50
  const unpackTransition = (v) => {
51
- if (v === null) return v
52
- if (typeof v === 'string') return v
53
- else if (typeof v === 'number') return v
54
- else if (typeof v === 'object' && v.constructor === Object) {
55
- if ('value' in v) {
51
+ if (typeof v !== 'object' || v === null) return v
52
+ if (v.constructor === Object) {
53
+ if ('value' in v === true) {
56
54
  return v.value
57
- } else if ('transition' in v) {
55
+ }
56
+ if ('transition' in v === true) {
58
57
  return unpackTransition(v.transition)
59
58
  }
60
59
  }
@@ -68,7 +67,7 @@ const colorMap = {
68
67
  right: 'colorRight',
69
68
  }
70
69
 
71
- let textDefaults
70
+ let textDefaults = null
72
71
 
73
72
  const propsTransformer = {
74
73
  set parent(v) {
@@ -102,9 +101,9 @@ const propsTransformer = {
102
101
  this.props['zIndex'] = v
103
102
  },
104
103
  set color(v) {
105
- if (typeof v === 'string' && v.indexOf('{') === -1) {
104
+ if (typeof v === 'string' && v.startsWith('{') === false) {
106
105
  this.props['color'] = colors.normalize(v)
107
- } else if (typeof v === 'object' || (isObjectString(v) && (v = parseToObject(v)))) {
106
+ } else if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
108
107
  this.props['color'] = 0
109
108
  Object.entries(v).forEach((color) => {
110
109
  this.props[colorMap[color[0]]] = colors.normalize(color[1])
@@ -113,31 +112,31 @@ const propsTransformer = {
113
112
  },
114
113
  set src(v) {
115
114
  this.props['src'] = v
116
- if (this.raw.get('color') === undefined) {
115
+ if (this.raw['color'] === undefined) {
117
116
  this.props['color'] = 0xffffffff
118
117
  }
119
118
  },
120
119
  set texture(v) {
121
120
  this.props['texture'] = v
122
121
 
123
- if (this.raw.get('color') === undefined && (v === null || v === undefined)) {
122
+ if (this.raw['color'] === undefined && (v === null || v === undefined)) {
124
123
  this.props['color'] = 0x00000000
125
- } else if (this.raw.get('color') === undefined) {
124
+ } else if (this.raw['color'] === undefined) {
126
125
  this.props['color'] = 0xffffffff
127
126
  }
128
127
  },
129
128
  set rtt(v) {
130
129
  this.props['rtt'] = v
131
- if (v === true && this.raw.get('color') === undefined) {
130
+ if (v === true && this.raw['color'] === undefined) {
132
131
  this.props['color'] = 0xffffffff
133
132
  }
134
133
  },
135
134
  set mount(v) {
136
- if (typeof v === 'object' || (isObjectString(v) && (v = parseToObject(v)))) {
137
- if ('x' in v) {
135
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
136
+ if ('x' in v === true) {
138
137
  this.props['mountX'] = v.x
139
138
  }
140
- if ('y' in v) {
139
+ if ('y' in v === true) {
141
140
  this.props['mountY'] = v.y
142
141
  }
143
142
  } else {
@@ -146,11 +145,11 @@ const propsTransformer = {
146
145
  }
147
146
  },
148
147
  set pivot(v) {
149
- if (typeof v === 'object' || (isObjectString(v) && (v = parseToObject(v)))) {
150
- if ('x' in v) {
148
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
149
+ if ('x' in v === true) {
151
150
  this.props['pivotX'] = v.x
152
151
  }
153
- if ('y' in v) {
152
+ if ('y' in v === true) {
154
153
  this.props['pivotY'] = v.y
155
154
  }
156
155
  } else {
@@ -159,11 +158,11 @@ const propsTransformer = {
159
158
  }
160
159
  },
161
160
  set scale(v) {
162
- if (typeof v === 'object' || (isObjectString(v) && (v = parseToObject(v)))) {
163
- if ('x' in v) {
161
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
162
+ if ('x' in v === true) {
164
163
  this.props['scaleX'] = v.x
165
164
  }
166
- if ('y' in v) {
165
+ if ('y' in v === true) {
167
166
  this.props['scaleY'] = v.y
168
167
  }
169
168
  } else {
@@ -185,12 +184,7 @@ const propsTransformer = {
185
184
  },
186
185
  set effects(v) {
187
186
  this.props['shader'] = renderer.createShader('DynamicShader', {
188
- effects: v.map((eff) => {
189
- if (eff.props && eff.props.color) {
190
- eff.props.color = colors.normalize(eff.props.color)
191
- }
192
- return eff
193
- }),
187
+ effects: v,
194
188
  })
195
189
  },
196
190
  set clipping(v) {
@@ -238,24 +232,22 @@ const propsTransformer = {
238
232
 
239
233
  const Element = {
240
234
  populate(data) {
241
- const props = {
242
- ...this.config,
243
- ...data,
244
- }
235
+ const props = data
236
+ props['node'] = this.config.node
245
237
 
246
- if (props[symbols.isSlot]) {
238
+ if (props[symbols.isSlot] === true) {
247
239
  this[symbols.isSlot] = true
248
240
  }
249
241
 
250
242
  this.props.element = this
251
243
 
252
- const propKeys = Object.keys(props)
253
- const length = propKeys.length
254
-
255
- this.props['parent'] = props.parent
244
+ this.props['parent'] = this.config.parent
256
245
  delete props.parent
257
246
 
258
- this.props.raw = new Map(Object.entries(props))
247
+ this.props.raw = data
248
+
249
+ const propKeys = Object.keys(props)
250
+ const length = propKeys.length
259
251
 
260
252
  for (let i = 0; i < length; i++) {
261
253
  const key = propKeys[i]
@@ -266,7 +258,7 @@ const Element = {
266
258
  }
267
259
 
268
260
  // correct for default white nodes (but not for text nodes)
269
- if (this.props.props['color'] === undefined && !('__textnode' in props)) {
261
+ if (this.props.props['color'] === undefined && '__textnode' in props === false) {
270
262
  this.props.props['color'] = 0
271
263
  }
272
264
 
@@ -274,57 +266,55 @@ const Element = {
274
266
  ? renderer.createTextNode({ ...textDefaults, ...this.props.props })
275
267
  : renderer.createNode(this.props.props)
276
268
 
277
- if (props['@loaded']) {
269
+ if (props['@loaded'] !== undefined && typeof props['@loaded'] === 'function') {
278
270
  this.node.on('loaded', (el, { type, dimensions }) => {
279
271
  props['@loaded']({ w: dimensions.width, h: dimensions.height, type }, this)
280
272
  })
281
273
  }
282
274
 
283
- if (props['@error']) {
275
+ if (props['@error'] !== undefined && typeof props['@error'] === 'function') {
284
276
  this.node.on('failed', (el, error) => {
285
277
  props['@error'](error, this)
286
278
  })
287
279
  }
288
280
 
289
- if (this.component && this.component.___layout) {
290
- this.node.on('loaded', () => {
291
- this.component.___layout()
292
- })
293
- }
281
+ // if (this.component && this.component.___layout) {
282
+ // this.node.on('loaded', () => {
283
+ // this.component.___layout()
284
+ // })
285
+ // }
294
286
  },
295
287
  set(prop, value) {
296
288
  if (value === undefined) return
297
- if (this.props.raw.get(prop) === value) return
298
-
299
- this.props.raw.set(prop, value)
289
+ if (this.props.raw[prop] === value) return
290
+ this.props.raw[prop] = value
300
291
 
301
292
  this.props.props = {}
302
293
  this.props[prop] = unpackTransition(value)
303
- const props = Object.entries(this.props.props)
304
294
 
305
- if (props.length === 1) {
306
- const [p, v] = props[0]
307
- if (isTransition(value)) {
308
- return this.animate(p, v, value.transition)
295
+ const propsKeys = Object.keys(this.props.props)
296
+
297
+ if (propsKeys.length === 1) {
298
+ if (isTransition(value) === true) {
299
+ return this.animate(propsKeys[0], this.props.props[propsKeys[0]], value.transition)
309
300
  }
310
301
  // set the prop to the value on the node
311
- this.node[p] = v
302
+ this.node[propsKeys[0]] = this.props.props[propsKeys[0]]
312
303
  } else {
313
- for (let i = 0; i < props.length; i++) {
304
+ for (let i = 0; i < propsKeys.length; i++) {
314
305
  // todo: fix code duplication
315
- const [p, v] = props[i]
316
- if (isTransition(value)) {
317
- return this.animate(p, v, value.transition)
306
+ if (isTransition(value) === true) {
307
+ return this.animate(propsKeys[i], this.props.props[propsKeys[i]], value.transition)
318
308
  }
319
309
  // set the prop to the value on the node
320
- this.node[p] = v
310
+ this.node[propsKeys[i]] = this.props.props[propsKeys[i]]
321
311
  }
322
312
  }
323
313
 
324
314
  // todo: review naming
325
- if (this.component && this.component.___layout) {
326
- this.component.___layout()
327
- }
315
+ // if (this.component && this.component.___layout) {
316
+ // this.component.___layout()
317
+ // }
328
318
  },
329
319
  animate(prop, value, transition) {
330
320
  // if current value is the same as the value to animate to, instantly resolve
@@ -409,7 +399,7 @@ const Element = {
409
399
  }
410
400
 
411
401
  export default (config, component) => {
412
- if (!textDefaults) {
402
+ if (textDefaults === null) {
413
403
  textDefaults = {
414
404
  fontSize: 32,
415
405
  fontFamily: Settings.get('defaultFont', 'sans-serif'),
@@ -70,7 +70,7 @@ const generateElementCode = function (
70
70
  renderCode.push(`const elementConfig${counter} = {}`)
71
71
 
72
72
  if (options.forloop) {
73
- renderCode.push(`if(!${elm}) {`)
73
+ renderCode.push(`if(${elm} === undefined) {`)
74
74
  }
75
75
 
76
76
  renderCode.push(`
@@ -99,9 +99,20 @@ const generateElementCode = function (
99
99
 
100
100
  if (isReactiveKey(key)) {
101
101
  if (options.holder && key === ':color') return
102
- this.effectsCode.push(
103
- `${elm}.set('${key.substring(1)}', ${interpolate(templateObject[key], options.component)})`
104
- )
102
+ if (options.holder) {
103
+ this.effectsCode.push(`
104
+ if(typeof skip${counter} === 'undefined' ||
105
+ skip${counter}.indexOf('${key.substring(1)}') === -1)
106
+ ${elm}.set('${key.substring(1)}', ${interpolate(templateObject[key], options.component)})
107
+ `)
108
+ } else {
109
+ this.effectsCode.push(`
110
+ ${elm}.set('${key.substring(1)}', ${interpolate(
111
+ templateObject[key],
112
+ options.component
113
+ )})
114
+ `)
115
+ }
105
116
  renderCode.push(
106
117
  `elementConfig${counter}['${key.substring(1)}'] = ${interpolate(
107
118
  templateObject[key],
@@ -117,16 +128,18 @@ const generateElementCode = function (
117
128
 
118
129
  if (options.holder === true) {
119
130
  renderCode.push(`
131
+ const skip${counter} = []
120
132
  if(typeof cmp${counter} !== 'undefined') {
121
133
  for(let key in cmp${counter}.config.props) {
122
134
  delete elementConfig${counter}[cmp${counter}.config.props[key]]
135
+ skip${counter}.push(cmp${counter}.config.props[key])
123
136
  }
124
137
  }
125
138
  `)
126
139
  }
127
140
 
128
141
  if (options.forloop) {
129
- renderCode.push(`if(!${elm}.nodeId) {`)
142
+ renderCode.push(`if(${elm}.nodeId === undefined) {`)
130
143
  }
131
144
 
132
145
  renderCode.push(`${elm}.populate(elementConfig${counter})`)
@@ -187,6 +200,11 @@ const generateComponentCode = function (
187
200
  }
188
201
 
189
202
  renderCode.push(`const props${counter} = {}`)
203
+
204
+ if (options.forloop) {
205
+ renderCode.push(`if(${elm} === undefined) {`)
206
+ }
207
+
190
208
  Object.keys(templateObject).forEach((key) => {
191
209
  if (isReactiveKey(key)) {
192
210
  this.effectsCode.push(`
@@ -207,10 +225,6 @@ const generateComponentCode = function (
207
225
  }
208
226
  })
209
227
 
210
- if (options.forloop) {
211
- renderCode.push(`if(!${elm}) {`)
212
- }
213
-
214
228
  renderCode.push(`
215
229
  componentType = props${counter}['is'] || '${templateObject[Symbol.for('componentType')]}'
216
230
 
@@ -256,6 +270,11 @@ const generateForLoopCode = function (templateObject, parent) {
256
270
  const key = templateObject['key']
257
271
  const forKey = interpolate(key, 'scope.')
258
272
 
273
+ const shallow = !!!(
274
+ templateObject['$shallow'] && templateObject['$shallow'].toLowerCase() === 'false'
275
+ )
276
+ delete templateObject['$shallow']
277
+
259
278
  delete templateObject['key']
260
279
  const regex = /(.+)\s+in\s+(.+)/gi
261
280
  // const regex = /(:?\(*)(.+)\s+in\s+(.+)/gi
@@ -287,7 +306,12 @@ const generateForLoopCode = function (templateObject, parent) {
287
306
  const created${forStartCounter} = []
288
307
  const forloop${forStartCounter} = (collection = [], elms, created) => {
289
308
  const rawCollection = getRaw(collection)
290
- const keys = new Set(rawCollection.map((${item}) => '' + ${interpolate(key, '')}))
309
+ const keys = new Set()
310
+ let l = rawCollection.length
311
+ while(l--) {
312
+ const ${item} = rawCollection[l]
313
+ keys.add('' + ${interpolate(key, '') || 'l'})
314
+ }
291
315
  `)
292
316
 
293
317
  // keep track of the index in the render code so we can inject
@@ -297,13 +321,13 @@ const generateForLoopCode = function (templateObject, parent) {
297
321
 
298
322
  ctx.renderCode.push(`
299
323
  created.length = 0
300
- const length = collection.length
324
+ const length = rawCollection.length
301
325
  for(let __index = 0; __index < length; __index++) {
302
326
  const scope = Object.create(component)
303
327
  parent = ${parent}
304
328
  scope['${index}'] = __index
305
329
  scope['${item}'] = rawCollection[__index]
306
- scope['key'] = ${forKey || '__index'}
330
+ scope['key'] = '' + ${forKey || '__index'}
307
331
  `)
308
332
  if ('ref' in templateObject && templateObject.ref.indexOf('$') === -1) {
309
333
  // automatically map the ref for each item in the loop based on the given ref key
@@ -347,24 +371,33 @@ const generateForLoopCode = function (templateObject, parent) {
347
371
  (effect) => [...effect.matchAll(scopeRegex)].length !== 0
348
372
  )
349
373
 
350
- ctx.renderCode.push(`
351
- scope['${item}'] = collection[__index]
374
+ if (shallow === false) {
375
+ ctx.renderCode.push(`
376
+ scope['${item}'] = null
377
+ scope['${item}'] = collection[__index]
352
378
  `)
379
+ }
353
380
 
354
381
  // inner scope variables are part of the main forloop
355
382
  innerScopeEffects.forEach((effect) => {
356
383
  const key = effect.indexOf(`scope.${index}`) > -1 ? `'${interpolate(result[2], '')}'` : null
357
- ctx.renderCode.push(`
358
- effect(() => {
384
+ if (effect.indexOf("Symbol.for('props')") === -1) {
385
+ ctx.renderCode.push(`
386
+ effect(() => {
387
+ ${effect}
388
+ }, ${key})
389
+ `)
390
+ } else {
391
+ // props shouldn't be wrapped in an effect, but simply passed on
392
+ ctx.renderCode.push(`
359
393
  ${effect}
360
- }, ${key})
361
- `)
394
+ `)
395
+ }
362
396
  })
363
-
397
+ // if(elms[${forStartCounter}][0] && elms[${forStartCounter}][0].forComponent && elms[${forStartCounter}][0].forComponent.___layout) {
398
+ // elms[${forStartCounter}][0].forComponent.___layout()
399
+ // }
364
400
  ctx.renderCode.push(`
365
- if(elms[${forStartCounter}][0] && elms[${forStartCounter}][0].forComponent && elms[${forStartCounter}][0].forComponent.___layout) {
366
- elms[${forStartCounter}][0].forComponent.___layout()
367
- }
368
401
  }
369
402
  }`)
370
403
 
@@ -374,8 +407,8 @@ const generateForLoopCode = function (templateObject, parent) {
374
407
  let i = created.length
375
408
 
376
409
  while (i--) {
377
- const key = created[i]
378
- if (!keys.has(key)) {
410
+ if (keys.has(created[i]) === false) {
411
+ const key = created[i]
379
412
  `)
380
413
  const forEndCounter = counter
381
414
 
@@ -492,6 +525,7 @@ const interpolate = (val, component = 'component.') => {
492
525
  }
493
526
 
494
527
  const cast = (val = '', key = false, component = 'component.') => {
528
+ const dynamicArgumentRegex = /\$\w+/gi
495
529
  let castedValue
496
530
 
497
531
  // inline content
@@ -542,6 +576,21 @@ const cast = (val = '', key = false, component = 'component.') => {
542
576
  else if (val.startsWith('$')) {
543
577
  castedValue = `${component}${val.replace('$', '')}`
544
578
  }
579
+ // dynamic value in object
580
+ else if (dynamicArgumentRegex.exec(val)) {
581
+ const rex = /\w+\s*:\s*(?:[^\s,}]+|".*?"|'.*?')/g
582
+ const results = val.match(rex)
583
+ castedValue = {}
584
+ if (results) {
585
+ for (let i = 0; i < results.length; i++) {
586
+ const members = results[i].split(/\s*:\s*/)
587
+ if (members) {
588
+ populateFields(members[0], members[1], component, castedValue)
589
+ }
590
+ }
591
+ }
592
+ return interpolateObject(castedValue)
593
+ }
545
594
  // static string
546
595
  else {
547
596
  castedValue = `"${val}"`
@@ -563,3 +612,21 @@ const parseInlineContent = (val, component) => {
563
612
  }
564
613
  return val
565
614
  }
615
+
616
+ // Populate value into target's prop
617
+ const populateFields = (prop, val, component, target) => {
618
+ if (val.startsWith('$')) {
619
+ target[prop] = `${component}${val.replace('$', '')}`
620
+ } else {
621
+ target[prop] = val
622
+ }
623
+ }
624
+
625
+ const interpolateObject = (input) => {
626
+ const interpolatedResults = []
627
+ Object.keys(input).forEach((key) => {
628
+ interpolatedResults.push(`${key}: ${input[key]}`)
629
+ })
630
+
631
+ return ` { ${interpolatedResults.join(', ')} }`
632
+ }
@@ -684,6 +684,138 @@ test('Generate code for a template with dynamic attributes with code to be evalu
684
684
  assert.end()
685
685
  })
686
686
 
687
+ test('Generate code for a template with attribute (object)', (assert) => {
688
+ const templateObject = {
689
+ children: [
690
+ {
691
+ [Symbol.for('componentType')]: 'Element',
692
+ w: '100',
693
+ h: '100',
694
+ color: '{top: 0xff0000ff, bottom: 0x008000ff, left: 0xffff00ff }',
695
+ },
696
+ ],
697
+ }
698
+
699
+ const expectedRender = `
700
+ function anonymous(parent,component,context,components,effect,getRaw) {
701
+ const elms = []
702
+ let componentType
703
+ const rootComponent = component
704
+ const elementConfig0 = {}
705
+
706
+ elms[0] = this.element({parent: parent || 'root'}, component)
707
+ elementConfig0['w'] = 100
708
+ elementConfig0['h'] = 100
709
+ elementConfig0['color'] = "{top: 0xff0000ff, bottom: 0x008000ff, left: 0xffff00ff}"
710
+ elms[0].populate(elementConfig0)
711
+
712
+ return elms
713
+ }
714
+ `
715
+
716
+ const actual = generator.call(scope, templateObject)
717
+ console.log(actual.render.toString())
718
+ assert.equal(
719
+ normalize(actual.render.toString()),
720
+ normalize(expectedRender),
721
+ 'Generator should return a render function with the correct code'
722
+ )
723
+ assert.ok(
724
+ Array.isArray(actual.effects) && actual.effects.length === 0,
725
+ 'Generator should return an empty effects array'
726
+ )
727
+
728
+ assert.end()
729
+ })
730
+
731
+ test('Generate code for a template with dynamic attribute (object)', (assert) => {
732
+ const templateObject = {
733
+ children: [
734
+ {
735
+ [Symbol.for('componentType')]: 'Element',
736
+ w: '100',
737
+ h: '100',
738
+ color: '{top: $colorT, bottom: $colorB, left:$colorL}',
739
+ },
740
+ ],
741
+ }
742
+
743
+ const expectedRender = `
744
+ function anonymous(parent,component,context,components,effect,getRaw) {
745
+ const elms = []
746
+ let componentType
747
+ const rootComponent = component
748
+ const elementConfig0 = {}
749
+
750
+ elms[0] = this.element({parent: parent || 'root'}, component)
751
+ elementConfig0['w'] = 100
752
+ elementConfig0['h'] = 100
753
+ elementConfig0['color'] = { top: component.colorT, bottom: component.colorB, left: component.colorL }
754
+ elms[0].populate(elementConfig0)
755
+
756
+ return elms
757
+ }
758
+ `
759
+
760
+ const actual = generator.call(scope, templateObject)
761
+
762
+ assert.equal(
763
+ normalize(actual.render.toString()),
764
+ normalize(expectedRender),
765
+ 'Generator should return a render function with the correct code'
766
+ )
767
+ assert.ok(
768
+ Array.isArray(actual.effects) && actual.effects.length === 0,
769
+ 'Generator should return an empty effects array'
770
+ )
771
+
772
+ assert.end()
773
+ })
774
+
775
+ test('Generate code for a template with attribute (object) with mixed dynamic & static value', (assert) => {
776
+ const templateObject = {
777
+ children: [
778
+ {
779
+ [Symbol.for('componentType')]: 'Element',
780
+ w: '100',
781
+ h: '100',
782
+ color: '{top: 0xff0000ff, bottom: $colorB, left: $colorL}',
783
+ },
784
+ ],
785
+ }
786
+
787
+ const expectedRender = `
788
+ function anonymous(parent,component,context,components,effect,getRaw) {
789
+ const elms = []
790
+ let componentType
791
+ const rootComponent = component
792
+ const elementConfig0 = {}
793
+
794
+ elms[0] = this.element({parent: parent || 'root'}, component)
795
+ elementConfig0['w'] = 100
796
+ elementConfig0['h'] = 100
797
+ elementConfig0['color'] = { top: 0xff0000ff, bottom: component.colorB, left: component.colorL }
798
+ elms[0].populate(elementConfig0)
799
+
800
+ return elms
801
+ }
802
+ `
803
+
804
+ const actual = generator.call(scope, templateObject)
805
+
806
+ assert.equal(
807
+ normalize(actual.render.toString()),
808
+ normalize(expectedRender),
809
+ 'Generator should return a render function with the correct code'
810
+ )
811
+ assert.ok(
812
+ Array.isArray(actual.effects) && actual.effects.length === 0,
813
+ 'Generator should return an empty effects array'
814
+ )
815
+
816
+ assert.end()
817
+ })
818
+
687
819
  test('Generate code for a template with @-listeners', (assert) => {
688
820
  const templateObject = {
689
821
  children: [
@@ -39,17 +39,16 @@ export default {
39
39
  normalize: (color = '', defaultColor = '0xffffffff') => {
40
40
  color = color.toString()
41
41
 
42
- // its mappable, map it
43
- if (colorsMap[color] !== undefined) {
44
- return colorsMap[color]
45
- }
46
-
47
- // its a 0xRRGGBBAA color, store it and return it
42
+ // it is a 0xRRGGBBAA color, return it
48
43
  if (color.startsWith('0x') && color.length === 10) {
49
- colorsMap[color] = color
50
44
  return color
51
45
  }
52
46
 
47
+ // it is a previously mapped color, return it
48
+ if (colorsMap[color] !== undefined) {
49
+ return colorsMap[color]
50
+ }
51
+
53
52
  // check for hex color
54
53
  if (hex.test(color)) {
55
54
  color = color.replace('#', '').toLowerCase()
@@ -30,11 +30,9 @@ export const getRaw = (value) => {
30
30
  const reactiveProxy = (original, _parent = null, _key) => {
31
31
  // don't create a proxy when a Blits component or an Image Texture
32
32
  // is assigned to a state variable
33
- if (
34
- (typeof original === 'object' && original[symbols.id]) ||
35
- original.constructor.name === '_ImageTexture'
36
- ) {
37
- return original
33
+ if (typeof original === 'object') {
34
+ if (original[symbols.id] !== undefined) return original
35
+ if (original.constructor.name === '_ImageTexture') return original
38
36
  }
39
37
 
40
38
  // if original object is already a proxy, don't create a new one but return the existing one instead
@@ -11,10 +11,10 @@ export default {
11
11
  init: Symbol('init'),
12
12
  inputEvents: Symbol('inputEvents'),
13
13
  intervals: Symbol('intervals'),
14
+ launched: Symbol('launched'),
14
15
  level: Symbol('level'),
15
16
  methodKeys: Symbol('methodKeys'),
16
17
  originalState: Symbol('originalState'),
17
- pluginsRegistered: Symbol('pluginsRegistered'),
18
18
  propKeys: Symbol('propKeys'),
19
19
  raw: Symbol('raw'),
20
20
  ready: Symbol('ready'),
@@ -28,7 +28,7 @@ export default (template = '', componentName, parentComponent, filePath = null)
28
28
  // regular expressions
29
29
  const tagStartRegex = /^<\/?([a-zA-Z0-9_\-.]+)\s*/
30
30
  const tagEndRegex = /^\s*(\/?>)\s*/
31
- const attrNameRegex = /^([A-Za-z0-9:.\-_@]+)=\s*(["'])/
31
+ const attrNameRegex = /^([A-Za-z0-9:.\-_@$]+)=\s*(["'])/
32
32
  const emptyTagStartRegex = /^<>\s*/
33
33
  const emptyTagEndRegex = /^\s*(<\/>)\s*/
34
34