@lightningjs/blits 0.3.9 → 0.3.11

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,87 @@
1
+ # Changelog
2
+
3
+
4
+ ## v0.3.11
5
+
6
+ _17 oct 2023_
7
+
8
+ - Added support for using [percentages](https://lightning-js.github.io/blits/#/element_attributes?id=using-percentages) in dimensions (`w` / `h`) and positioning (`x` / `y`)
9
+ - Refactored template parser to be more robust and throw errors when the template contains a syntax mistake
10
+ - Re-enabled tests (still depends on renderer version bump though)
11
+ - Added the new official Blits logo to the readme!
12
+
13
+ ## v0.3.10
14
+
15
+ _9 oct 2023_
16
+
17
+ - Updated L3 renderer dependency to 0.3.6
18
+ - Renamed internal methods for creating textures and shaders
19
+
20
+
21
+ ## v0.3.9
22
+
23
+ _5 oct 2023_
24
+
25
+ - Updated L3 renderer dependency to 0.3.3
26
+
27
+
28
+ ## v0.3.8
29
+
30
+ _4 oct 2023_
31
+
32
+ - Added Blits VS code extension to recommended extensions
33
+
34
+
35
+ ## v0.3.7
36
+
37
+ _3 oct 2023_
38
+
39
+ - Fixed reactivity issue when using `null` values in state
40
+
41
+
42
+ ## v0.3.6
43
+
44
+ _2 oct 2023_
45
+
46
+ - Bumped version number of dependency in boilerplate
47
+
48
+
49
+ ## v0.3.5
50
+
51
+ _2 oct 2023_
52
+
53
+ - Added workaround for Vite import chunk url plugin for fontloading
54
+
55
+ ## v0.3.4
56
+
57
+ _27 sep 2023_
58
+
59
+ - Fixed issue with re-applying reactivity to already reactive props in forloop
60
+
61
+
62
+ ## v0.3.3
63
+
64
+ _25 sep 2023_
65
+
66
+ - Added support for `@loaded` and `@error` events on Elements (and Text Component)
67
+
68
+
69
+ ## v0.3.2
70
+
71
+ _22 sep 2023_
72
+
73
+ - Changed name of attribute to reference an Element or Component from `id` to `ref`
74
+
75
+
76
+ ## v0.3.1
77
+
78
+ _20 sep 2023_
79
+
80
+ - Fixed Blits dependency in create App boilerplate package.json
81
+
82
+
83
+ ## v0.3.0
84
+
85
+ _20 sep 2023_
86
+
87
+ Initial beta release of Blits ⚡️
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![Blits banner](banner.png)
2
+
1
3
  # Blits - Lightning 3 App Development Framework
2
4
 
3
5
  With Blits (formaly know as Bolt), the App Development Framework for Lightning 3, it becomes a breeze to build great Lightning 3 applications.
@@ -12,8 +14,14 @@ The Blits framework uses a **readable** XML-style template syntax and comes with
12
14
 
13
15
  You may be wondering where the name Blits comes from?
14
16
 
15
- Firstly, it short, memorable and has a nice ring to it. The name is derived from the word "Blitz", which means Lightning in German. Additionally, "Blits" is a Dutch term (a significant portion of the Lightning Open Source teams is based in the Netherlands) used to showcase something great or simply refer to things that are cool.
17
+ Firstly, it's short, memorable and just has a nice ring to it.
18
+
19
+ "Blits" is a Dutch word (a significant portion of the Lightning Open Source teams is based in the Netherlands). It means _flashy_ and it's often used to for things that are _great_ or _cool_.
16
20
 
17
21
  Finally it's a nerdy reference to the computer term "Bit Blits" commonly used in graphics processing, for rapidly moving a block of data into memory - quite similar to what Lightning is doing 🙂
18
22
 
19
23
  Check out the [documentation](https://lightning-js.github.io/blits) to learn more.
24
+
25
+ ## Changelog
26
+
27
+ The full Changelog can be found [here](./CHANGELOG.md)
package/banner.png ADDED
Binary file
@@ -31,6 +31,6 @@
31
31
  "vite": "^4.0.4"
32
32
  },
33
33
  "dependencies": {
34
- "@lightningjs/blits": "^0.3.7"
34
+ "@lightningjs/blits": "^0.3.11"
35
35
  }
36
36
  }
@@ -25,6 +25,18 @@ All positioning and dimension related attributes, when not specified, default to
25
25
  </Element>
26
26
  ```
27
27
 
28
+ #### Using percentages
29
+
30
+ Besides using values in pixels (i.e. `w="100" h="300"`), you can also specify _percentages_ for the positioning and dimensions attributes.
31
+
32
+ ```xml
33
+ <Element w="400" h="100" x="800" y="900" color="#0284c7">
34
+ <Element w="42%" h="30%" y="5%" x="1%" color="#075985" />
35
+ </Element>
36
+ ```
37
+ The percentage specified in `w` and `x` will be calculated as the percentage of the _width_ (`w`) of the parent element.
38
+ And the percentage specified in `h` and `y` will use the _height_ (`h`) of the parent element as the base of the percentage calculation.
39
+
28
40
  ### Colors
29
41
 
30
42
  By default Elements have a transparent background color. The `color` attribute can be used to give an Element a color.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.cjs",
6
6
  "exports": {
@@ -9,7 +9,8 @@
9
9
  "./fontloader": "./src/fontLoader.js"
10
10
  },
11
11
  "scripts": {
12
- "test": "tape '{,!(node_modules)/**/}*.test.js' | tap-diff",
12
+ "test": "c8 npm run test:run",
13
+ "test:run": "tape '{,!(node_modules)/**/}*.test.js' | tap-diff",
13
14
  "lint": "eslint '**/*.js'",
14
15
  "lint:fix": "eslint '**/*.js' --fix"
15
16
  },
@@ -28,6 +29,7 @@
28
29
  "license": "Apache-2.0",
29
30
  "devDependencies": {
30
31
  "babel-eslint": "^10.1.0",
32
+ "c8": "^8.0.1",
31
33
  "eslint": "^8.8.0",
32
34
  "eslint-config-prettier": "^8.3.0",
33
35
  "eslint-plugin-prettier": "^4.0.0",
@@ -39,7 +41,7 @@
39
41
  "tape": "^5.5.0"
40
42
  },
41
43
  "dependencies": {
42
- "@lightningjs/renderer": "^0.3.3",
44
+ "@lightningjs/renderer": "^0.3.6",
43
45
  "@lightningjs/vite-plugin-import-chunk-url": "^0.3.0"
44
46
  }
45
47
  }
@@ -36,7 +36,7 @@ export default () =>
36
36
  : this.map[this.frame]
37
37
 
38
38
  if (this.spriteTexture && options) {
39
- return this.___renderer.makeTexture('SubTexture', {
39
+ return this.___renderer.createTexture('SubTexture', {
40
40
  texture: this.spriteTexture,
41
41
  x: options.x,
42
42
  y: options.y,
@@ -48,7 +48,7 @@ export default () =>
48
48
  },
49
49
  hooks: {
50
50
  ready() {
51
- this.spriteTexture = this.___renderer.makeTexture('ImageTexture', {
51
+ this.spriteTexture = this.___renderer.createTexture('ImageTexture', {
52
52
  src: `${window.location.protocol}//${window.location.host}/${this.image}`,
53
53
  })
54
54
  },
@@ -32,6 +32,7 @@ export default () =>
32
32
  stretch="$stretch"
33
33
  contain="$_contain"
34
34
  :w="$w"
35
+ :h="$h"
35
36
  :textAlign="$align"
36
37
  @loaded="$@loaded"
37
38
  @error="$@error"
@@ -54,6 +55,7 @@ export default () =>
54
55
  'stretch',
55
56
  'align',
56
57
  'w',
58
+ 'h',
57
59
  'contain',
58
60
  '@loaded',
59
61
  '@error',
@@ -63,7 +65,7 @@ export default () =>
63
65
  return this.slotcontent || this.content || ''
64
66
  },
65
67
  _contain() {
66
- return this.contain || (this.align ? 'width' : null)
68
+ return this.contain || (this.w && this.h ? 'both' : this.w ? 'width' : 'none')
67
69
  },
68
70
  },
69
71
  })
package/src/element.js CHANGED
@@ -54,9 +54,12 @@ const transformations = {
54
54
  'z' in props && (props.zIndex = props.z)
55
55
  delete props.z
56
56
  },
57
- parentId(props) {
58
- props.parent = props.parentId === 'root' ? renderer.root : renderer.getNodeById(props.parentId)
59
- delete props.parentId
57
+ parent(props) {
58
+ if (props.parent === 'root') {
59
+ props.parent = renderer.root
60
+ } else {
61
+ props.parent = props.parent.node
62
+ }
60
63
  },
61
64
  color(props) {
62
65
  if (
@@ -77,6 +80,21 @@ const transformations = {
77
80
  props.color = colors.normalize(props.color)
78
81
  }
79
82
  },
83
+ percentage(props, prop) {
84
+ const map = {
85
+ w: 'width',
86
+ width: 'width',
87
+ x: 'width',
88
+ h: 'height',
89
+ height: 'height',
90
+ y: 'height',
91
+ }
92
+
93
+ const base = map[prop]
94
+ if (base) {
95
+ props[prop] = this.node._parent[base] * (parseFloat(props[prop]) / 100)
96
+ }
97
+ },
80
98
  mount(props) {
81
99
  if (
82
100
  typeof props.mount === 'object' ||
@@ -123,7 +141,7 @@ const transformations = {
123
141
  }
124
142
  },
125
143
  effects(props) {
126
- props.shader = renderer.makeShader('DynamicShader', {
144
+ props.shader = renderer.createShader('DynamicShader', {
127
145
  effects: props.effects.map((eff) => {
128
146
  if (eff.props && eff.props.color) {
129
147
  eff.props.color = colors.normalize(eff.props.color)
@@ -200,6 +218,9 @@ const Element = {
200
218
  }
201
219
 
202
220
  Object.keys(props).forEach((prop) => {
221
+ if (typeof props[prop] === 'string' && props[prop].endsWith('%')) {
222
+ transformations.percentage.call(this, props, prop)
223
+ }
203
224
  this.node[prop] = props[prop]
204
225
  })
205
226
  }
@@ -228,6 +249,10 @@ const Element = {
228
249
  // works for now
229
250
  prop = Object.keys(props).pop()
230
251
 
252
+ if (typeof value === 'string' && props[prop].endsWith('%')) {
253
+ transformations.percentage.call(this, props, prop)
254
+ }
255
+
231
256
  if (this.node[prop] !== props[prop]) {
232
257
  const f = this.node.animate(props, {
233
258
  duration: typeof value === 'object' ? ('duration' in value ? value.duration : 300) : 300,
package/src/launch.d.ts CHANGED
@@ -78,7 +78,7 @@ export interface Settings {
78
78
  /**
79
79
  * Debug level for console log messages
80
80
  */
81
- debugLevel?: DebugLevel | LogType[]
81
+ debugLevel?: DebugLevel | LogTypes[]
82
82
  /**
83
83
  * Font loader file
84
84
  */
@@ -56,7 +56,7 @@ const generateElementCode = function (
56
56
 
57
57
  renderCode.push(`
58
58
  if(!${elm}) {
59
- ${elm} = this.element({componentId: component.___id, parentId: parent && parent.nodeId || 'root'})
59
+ ${elm} = this.element({componentId: component.___id, parent: parent || 'root'})
60
60
  }
61
61
  const elementConfig${counter} = {}
62
62
  `)
@@ -139,7 +139,7 @@ const generateComponentCode = function (
139
139
  if (key === 'type') return
140
140
  if (isReactiveKey(key)) {
141
141
  this.effectsCode.push(`
142
- ${elm}.___props['${key.substring(1)}'] = ${interpolate(
142
+ ${elm}.___props['${key.substring(1)}'] = ${interpolate(
143
143
  templateObject[key],
144
144
  options.component
145
145
  )}`)
@@ -241,19 +241,20 @@ const generateForLoopCode = function (templateObject, parent) {
241
241
  }
242
242
 
243
243
  const generateCode = function (templateObject, parent = false) {
244
- templateObject.children.forEach((childTemplateObject) => {
245
- counter++
244
+ templateObject.children &&
245
+ templateObject.children.forEach((childTemplateObject) => {
246
+ counter++
246
247
 
247
- if (Object.keys(childTemplateObject).indexOf(':for') > -1) {
248
- generateForLoopCode.call(this, childTemplateObject, parent)
249
- } else {
250
- if (childTemplateObject.type !== 'Element') {
251
- generateComponentCode.call(this, childTemplateObject, parent)
248
+ if (Object.keys(childTemplateObject).indexOf(':for') > -1) {
249
+ generateForLoopCode.call(this, childTemplateObject, parent)
252
250
  } else {
253
- generateElementCode.call(this, childTemplateObject, parent)
251
+ if (childTemplateObject.type !== 'Element') {
252
+ generateComponentCode.call(this, childTemplateObject, parent)
253
+ } else {
254
+ generateElementCode.call(this, childTemplateObject, parent)
255
+ }
254
256
  }
255
- }
256
- })
257
+ })
257
258
  }
258
259
 
259
260
  const interpolate = (val, component = 'component.') => {
@@ -285,9 +286,22 @@ const interpolate = (val, component = 'component.') => {
285
286
  const cast = (val = '', key = false, component = 'component.') => {
286
287
  let castedValue
287
288
 
288
- // numeric
289
289
  if (key !== 'color' && !isNaN(parseFloat(val))) {
290
290
  castedValue = parseFloat(val)
291
+ if (val.endsWith('%')) {
292
+ const map = {
293
+ w: 'width',
294
+ width: 'width',
295
+ x: 'width',
296
+ h: 'height',
297
+ height: 'height',
298
+ y: 'height',
299
+ }
300
+ const base = map[key]
301
+ if (base) {
302
+ castedValue = `parent.node.${base} * (${castedValue} / 100)`
303
+ }
304
+ }
291
305
  }
292
306
  // boolean true
293
307
  else if (val.toLowerCase() === 'true') {
@@ -22,6 +22,10 @@ const normalize = (str) => {
22
22
  return str.replace(/[\n\s\t]/gi, '')
23
23
  }
24
24
 
25
+ const componentConfigObject = {
26
+ components: {},
27
+ }
28
+
25
29
  test('Type', (assert) => {
26
30
  const expected = 'function'
27
31
  const actual = typeof generator
@@ -31,7 +35,7 @@ test('Type', (assert) => {
31
35
  })
32
36
 
33
37
  test('Returns an object with a render function and a context object', (assert) => {
34
- const result = generator()
38
+ const result = generator.call(componentConfigObject)
35
39
  const expected = 'object'
36
40
  const actual = typeof result
37
41
 
@@ -43,7 +47,7 @@ test('Returns an object with a render function and a context object', (assert) =
43
47
  })
44
48
 
45
49
  test('The render key is a function', (assert) => {
46
- const result = generator()
50
+ const result = generator(componentConfigObject)
47
51
  const expected = 'function'
48
52
  const actual = typeof result.render
49
53
 
@@ -28,42 +28,42 @@ test('Object', (assert) => {
28
28
 
29
29
  test('6 character hex', (assert) => {
30
30
  const input = ['#ffffff', '#000000', '#c0ffee', '#7dd3fc', '#0c4a6e']
31
- const expected = ['0xffffffff', '0xff000000', '0xffeeffc0', '0xfffcd37d', '0xff6e4a0c']
31
+ const expected = ['0xffffffff', '0x000000ff', '0xc0ffeeff', '0x7dd3fcff', '0x0c4a6eff']
32
32
 
33
33
  const actual = input.map(colors.normalize)
34
34
 
35
35
  assert.deepEqual(
36
36
  actual,
37
37
  expected,
38
- 'A 6 character hex code should return the correct color in abgr format'
38
+ 'A 6 character hex code should return the correct color in rgba format'
39
39
  )
40
40
  assert.end()
41
41
  })
42
42
 
43
43
  test('3 character hex', (assert) => {
44
44
  const input = ['#fff', '#000', '#c8c']
45
- const expected = ['0xffffffff', '0xff000000', '0xffcc88cc']
45
+ const expected = ['0xffffffff', '0x000000ff', '0xcc88ccff']
46
46
 
47
47
  const actual = input.map(colors.normalize)
48
48
 
49
49
  assert.deepEqual(
50
50
  actual,
51
51
  expected,
52
- 'A 3 character hex code should return the correct color in abgr format'
52
+ 'A 3 character hex code should return the correct color in rgba format'
53
53
  )
54
54
  assert.end()
55
55
  })
56
56
 
57
57
  test('8 character hex (rgba)', (assert) => {
58
58
  const input = ['#ffffffff', '#000000ff', '#c0ffee80', '#7dd3fc10', '#0c4a6eff']
59
- const expected = ['0xffffffff', '0xff000000', '0x80eeffc0', '0x10fcd37d', '0xff6e4a0c']
59
+ const expected = ['0xffffffff', '0x000000ff', '0xc0ffee80', '0x7dd3fc10', '0x0c4a6eff']
60
60
 
61
61
  const actual = input.map(colors.normalize)
62
62
 
63
63
  assert.deepEqual(
64
64
  actual,
65
65
  expected,
66
- 'A 8 character hex (rgba) code should return the correct color in abgr format'
66
+ 'A 8 character hex (rgba) code should return the correct color in rgba format'
67
67
  )
68
68
  assert.end()
69
69
  })
@@ -76,14 +76,14 @@ test('RGB colors', (assert) => {
76
76
  'rgb(125, 211, 252)',
77
77
  'rgb(12, 74, 110)',
78
78
  ]
79
- const expected = ['0xffffffff', '0xff000000', '0xffeeffc0', '0xfffcd37d', '0xff6e4a0c']
79
+ const expected = ['0xffffffff', '0x000000ff', '0xc0ffeeff', '0x7dd3fcff', '0x0c4a6eff']
80
80
 
81
81
  const actual = input.map(colors.normalize)
82
82
 
83
83
  assert.deepEqual(
84
84
  actual,
85
85
  expected,
86
- 'RGB codes prefixed with # should return the correct color in abgr format'
86
+ 'RGB codes prefixed with # should return the correct color in rgba format'
87
87
  )
88
88
  assert.end()
89
89
  })
@@ -100,12 +100,12 @@ test('RGBA colors', (assert) => {
100
100
  ]
101
101
  const expected = [
102
102
  '0xffffffff',
103
- '0xff000000',
104
- '0x80eeffc0',
105
- '0x1afcd37d',
106
- '0x006e4a0c',
107
- '0x006e4a0c',
108
- '0xff6e4a0c',
103
+ '0x000000ff',
104
+ '0xc0ffee80',
105
+ '0x7dd3fc1a',
106
+ '0x0c4a6e00',
107
+ '0x0c4a6e00',
108
+ '0x0c4a6eff',
109
109
  ]
110
110
 
111
111
  const actual = input.map(colors.normalize)
@@ -113,7 +113,7 @@ test('RGBA colors', (assert) => {
113
113
  assert.deepEqual(
114
114
  actual,
115
115
  expected,
116
- 'RGB codes prefixed with # should return the correct color in abgr format'
116
+ 'RGB codes prefixed with # should return the correct color in rgba format'
117
117
  )
118
118
  assert.end()
119
119
  })
@@ -130,12 +130,12 @@ test('RGBA colors', (assert) => {
130
130
  ]
131
131
  const expected = [
132
132
  '0xffffffff',
133
- '0xff000000',
134
- '0x80eeffc0',
135
- '0x1afcd37d',
136
- '0x006e4a0c',
137
- '0x006e4a0c',
138
- '0xff6e4a0c',
133
+ '0x000000ff',
134
+ '0xc0ffee80',
135
+ '0x7dd3fc1a',
136
+ '0x0c4a6e00',
137
+ '0x0c4a6e00',
138
+ '0x0c4a6eff',
139
139
  ]
140
140
 
141
141
  const actual = input.map(colors.normalize)
@@ -143,19 +143,19 @@ test('RGBA colors', (assert) => {
143
143
  assert.deepEqual(
144
144
  actual,
145
145
  expected,
146
- 'RGB codes prefixed with # should return the correct color in abgr format'
146
+ 'RGB codes prefixed with # should return the correct color in rgba format'
147
147
  )
148
148
  assert.end()
149
149
  })
150
150
 
151
151
  test('HTML colors', (assert) => {
152
- const input = ['blanchedalmond', 'aqua', 'tomato', 'saddlebrown', 'red']
152
+ const input = ['blanchedalmond', 'salmon', 'tomato', 'saddlebrown', 'red']
153
153
 
154
154
  const expected = [
155
- parseInt('0xffcdebff', 16),
156
- parseInt('0xffffff00', 16),
157
- parseInt('0xff4763ff', 16),
158
- parseInt('0xff13458b', 16),
155
+ parseInt('0xffebcdff', 16),
156
+ parseInt('0xfa8072ff', 16),
157
+ parseInt('0xff6347ff', 16),
158
+ parseInt('0x8b4513ff', 16),
159
159
  parseInt('0xff0000ff', 16),
160
160
  ]
161
161
 
@@ -164,7 +164,7 @@ test('HTML colors', (assert) => {
164
164
  assert.deepEqual(
165
165
  actual,
166
166
  expected,
167
- 'RGB codes prefixed with # should return the correct color in abgr format'
167
+ 'RGB codes prefixed with # should return the correct color in rgba format'
168
168
  )
169
169
  assert.end()
170
170
  })
@@ -181,7 +181,7 @@ test('HTML colors', (assert) => {
181
181
 
182
182
  // const actual = input.map(colors.normalize)
183
183
 
184
- // assert.deepEqual(actual, expected, 'A hsl code should return the correct color in abgr format')
184
+ // assert.deepEqual(actual, expected, 'A hsl code should return the correct color in rgba format')
185
185
  // assert.end()
186
186
  // })
187
187
 
@@ -28,7 +28,15 @@ const arrayMethods = [
28
28
  'splice',
29
29
  'unshift',
30
30
  ]
31
+
32
+ const proxyMap = new WeakMap()
33
+
31
34
  const reactiveProxy = (target) => {
35
+ const isProxy = proxyMap.get(target)
36
+ if (isProxy) {
37
+ return isProxy
38
+ }
39
+
32
40
  const handler = {
33
41
  get(target, key, receiver) {
34
42
  if (Array.isArray(target) && arrayMethods.includes(key)) {
@@ -36,12 +44,7 @@ const reactiveProxy = (target) => {
36
44
  }
37
45
  track(target, key)
38
46
 
39
- if (
40
- (target[key] !== null &&
41
- typeof target[key] === 'object' &&
42
- Object.getPrototypeOf(target[key]) === Object.prototype) ||
43
- Array.isArray(target[key])
44
- ) {
47
+ if (target[key] !== null && typeof target[key] === 'object') {
45
48
  return reactiveProxy(target[key])
46
49
  }
47
50
 
@@ -66,7 +69,10 @@ const reactiveProxy = (target) => {
66
69
  return result
67
70
  },
68
71
  }
69
- return new Proxy(target, handler)
72
+
73
+ const proxy = new Proxy(target, handler)
74
+ proxyMap.set(target, proxy)
75
+ return proxy
70
76
  }
71
77
 
72
78
  const reactiveDefineProperty = (target) => {
@@ -17,6 +17,9 @@
17
17
 
18
18
  import test from 'tape'
19
19
  import propsFn from './props.js'
20
+ import { initLog } from '../log.js'
21
+
22
+ initLog()
20
23
 
21
24
  test('Type props function', (assert) => {
22
25
  const expected = 'function'
@@ -87,14 +90,14 @@ test('Get value of props', (assert) => {
87
90
  test('Passing props as an object', (assert) => {
88
91
 
89
92
  const component = new Function()
90
- const props = [{key: 'index', key: 'img', key: 'url'}]
93
+ const props = [{key: 'index'}, {key: 'img'}, {key: 'url'}]
91
94
  propsFn(component, props)
92
95
 
93
96
  assert.equal(props.length, component.___propKeys.length, 'All passed props should be stored on ___propKeys')
94
- assert.equal(props.length, props.map(prop => component.___propKeys.indexOf(prop.key) > -1).filter(prop => prop === true).length, 'All passed props should be stored on ___propKeys')
97
+ assert.equal(props.length, props.map(prop => component.___propKeys.indexOf(typeof prop === 'object' ? prop.key : prop) > -1).filter(prop => prop === true).length, 'All passed props should be stored on ___propKeys')
95
98
 
96
99
  props.forEach((prop) => {
97
- assert.true(typeof Object.getOwnPropertyDescriptor(component.prototype, prop.key).get === 'function', `A getter should have been created for property ${prop}`)
100
+ assert.true(typeof Object.getOwnPropertyDescriptor(component.prototype, typeof prop === 'object' ? prop.key : prop).get === 'function', `A getter should have been created for property ${prop}`)
98
101
  })
99
102
 
100
103
  assert.end()
@@ -15,10 +15,47 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
+ class TemplateParseError extends Error {
19
+ constructor(name, context) {
20
+ super('TemplateParseError')
21
+ this.name = name
22
+ this.context = context
23
+ }
24
+ }
25
+
18
26
  export default (template = '') => {
19
27
  let cursor = 0
20
- let cleaned = false
28
+ let tags = []
29
+ let currentTag = null
30
+ let currentLevel = 0
31
+
32
+ // regular expressions
33
+ const tagStartRegex = /^<\/?([a-zA-Z0-9_\-.]+)\s*/
34
+ const tagEndRegex = /^\s*(\/?>)\s*/
35
+ const attrNameRegex = /^([A-Za-z0-9:.\-_@]+)=\s*(["'])/
36
+ const emptyTagStartRegex = /^<>\s*/
37
+ const emptyTagEndRegex = /^\s*(<\/>)\s*/
21
38
 
39
+ // main functions
40
+ const parse = () => {
41
+ template = clean(template)
42
+ parseLoop(parseEmptyTagStart)
43
+
44
+ try {
45
+ return format(tags)
46
+ } catch (error) {
47
+ console.error('Error:', error.message)
48
+ }
49
+ }
50
+
51
+ const parseLoop = (next) => {
52
+ if (cursor >= template.length) {
53
+ return
54
+ }
55
+ next()
56
+ }
57
+
58
+ // utils
22
59
  const clean = (template) => {
23
60
  // remove all unnecessary new lines and comments
24
61
  return template
@@ -26,77 +63,187 @@ export default (template = '') => {
26
63
  .replace(/\r?\n\s*\r\n/gm, ' ') // remove empty lines
27
64
  .replace(/\r?\n\s*(\S)/gm, ' $1') // remove line endings & spacing
28
65
  .replace(/\r?\n/g, '') // remove all line endings
66
+ .trim()
29
67
  }
30
68
 
31
- const parse = () => {
32
- if (!cleaned) {
33
- template = clean(template)
34
- cleaned = true
69
+ const moveCursorOnMatch = (regex) => {
70
+ const match = template.slice(cursor).match(regex)
71
+ if (match) cursor += match[0].length
72
+ return match
73
+ }
74
+
75
+ // parsers
76
+ const parseEmptyTagStart = () => {
77
+ const match = moveCursorOnMatch(emptyTagStartRegex)
78
+ if (match) {
79
+ tags.push({ type: null, __type: 'opening', __level: currentLevel })
80
+ currentLevel++
81
+ parseLoop(parseEmptyTagStart)
82
+ } else {
83
+ parseLoop(parseEmptyTagEnd)
84
+ }
85
+ }
86
+
87
+ const parseEmptyTagEnd = () => {
88
+ const match = moveCursorOnMatch(emptyTagEndRegex)
89
+ if (match) {
90
+ currentLevel--
91
+ tags.push({ type: null, __type: 'closing', __level: currentLevel })
92
+ parseLoop(parseEmptyTagStart)
93
+ } else {
94
+ parseLoop(parseTag)
35
95
  }
96
+ }
36
97
 
37
- const output = { children: [] }
38
- let attributeStarted = false
39
- while (template[cursor]) {
40
- if (template[cursor] === '"') {
41
- attributeStarted = !attributeStarted
98
+ const parseTag = () => {
99
+ const match = moveCursorOnMatch(tagStartRegex)
100
+ if (match) {
101
+ if (match[0].startsWith('</')) {
102
+ currentLevel--
103
+ currentTag = { type: match[1], __type: 'closing', __level: currentLevel }
104
+ } else {
105
+ currentTag = { type: match[1], __type: 'opening', __level: currentLevel }
106
+ currentLevel++
42
107
  }
43
- if (template.charCodeAt(cursor) === '<'.charCodeAt(0) && !attributeStarted) {
44
- if (template.charCodeAt(cursor + 1) === '/'.charCodeAt(0)) {
45
- return output
108
+ parseLoop(parseTagEnd)
109
+ } else {
110
+ throw new TemplateParseError('InvalidTag', template.slice(cursor))
111
+ }
112
+ }
113
+
114
+ const parseTagEnd = () => {
115
+ const match = moveCursorOnMatch(tagEndRegex)
116
+ if (match) {
117
+ if (match[1] === '/>') {
118
+ currentTag.__type = 'self-closing'
119
+ currentLevel-- // because it was parsed as opening tag before
120
+ }
121
+
122
+ // parsing content in between tags
123
+ // rule: < char cannot be used in between tags even in escaped form
124
+ if (currentTag.__type === 'opening') {
125
+ const tagContent = template.slice(cursor, template.indexOf('<', cursor))
126
+ if (tagContent) {
127
+ currentTag.content = tagContent
128
+ cursor += tagContent.length
46
129
  }
47
- parseNode(output)
48
130
  }
49
- cursor++
50
- }
51
131
 
52
- return output
132
+ tags.push(currentTag)
133
+ parseLoop(parseEmptyTagStart)
134
+ } else {
135
+ parseLoop(parseAttributes)
136
+ }
53
137
  }
54
138
 
55
- const parseNode = (output) => {
56
- const endTagMatch = template.slice(cursor).match(/(?:[^">]*|"[^"]*")*>/)
57
- const endPosition = endTagMatch && endTagMatch[0] && endTagMatch[0].length + cursor - 1
58
- const tag = parseTag(template.substring(cursor + 1, endPosition))
59
- const node = { ...{ type: tag.type }, ...tag.attributes }
60
- // self closing tag
61
- if (template.charCodeAt(endPosition - 1) === '/'.charCodeAt(0)) {
62
- output.children.push(node)
63
- } else {
64
- cursor = endPosition
65
- const nested = parse()
66
- if (nested.children.length) {
67
- node.children = [...nested.children]
139
+ const parseAttributes = () => {
140
+ const attrNameMatch = moveCursorOnMatch(attrNameRegex)
141
+ if (attrNameMatch) {
142
+ const delimiter = attrNameMatch[2]
143
+ const attrValueRegex = new RegExp(`^(.*?)${delimiter}\\s*`)
144
+ const attrValueMatch = moveCursorOnMatch(attrValueRegex)
145
+
146
+ if (attrValueMatch) {
147
+ // maybe this should be done while code generation step
148
+ const attr = formatAttribute(attrNameMatch[1], attrValueMatch[1])
149
+ currentTag[attr.name] = attr.value
150
+ parseLoop(parseTagEnd)
151
+ } else {
152
+ throw new TemplateParseError('MissingOrInvalidAttributeValue', template.slice(cursor))
68
153
  }
69
- // else {
70
- // const content = template.substring(endPosition + 1, cursor).trim()
71
- // if (content) node.slotcontent = content
72
- // }
73
- output.children.push(node)
154
+ } else {
155
+ throw new TemplateParseError('InvalidAttribute', template.slice(cursor))
74
156
  }
75
157
  }
76
158
 
77
- const parseTag = (tag) => {
78
- let match
79
- const result = {
80
- type: (match = tag.match(/^([A-Za-z0-9]+)(?![A-Za-z0-9=])/)) && match[0], // || 'Element' maybe default to 'Element'
159
+ const formatAttribute = (name, value) => {
160
+ // if name contains a dot then we need to add enclosing js object to the value
161
+ // example :x.transition="{v: $offset}" must be formatted as { name: ':x', value: '{ transition: {v: $offset}' }
162
+ if (name.includes('.')) {
163
+ const [objectName, attributeName] = name.split('.')
164
+ return { name: objectName, value: `{${attributeName}: ${value}}` }
81
165
  }
166
+ return { name, value }
167
+ }
168
+
169
+ // formatter
170
+ function format(data) {
171
+ let stack = []
172
+ let output = { children: [] }
82
173
 
83
- const attributes = tag.match(/[:*@\w.-]+="[^"]*"/g) || []
84
- if (attributes.length) {
85
- result['attributes'] = attributes.reduce((obj, attr) => {
86
- const match = /(.+)=["'](.+)["']/.exec(attr)
87
- if (match) {
88
- if (match[1].indexOf('.') > -1) {
89
- const parts = match[1].split('.')
90
- obj[parts[0]] = `{${parts[1]}: ${match[2]}}`
91
- } else {
92
- obj[match[1]] = match[2]
93
- }
174
+ for (const item of data) {
175
+ const { type, __type, __level } = item
176
+
177
+ // Check for unclosed tags
178
+ while (stack.length && stack[stack.length - 1].__level >= __level) {
179
+ const popped = stack.pop()
180
+ if (popped.__type === 'opening') {
181
+ throw new TemplateParseError('MismatchedClosingTag', `tag: ${popped.type || 'null'}`)
94
182
  }
95
- return obj
96
- }, {})
183
+ }
184
+
185
+ // For closing tags, just pop the opening tag from stack and continue
186
+ if (__type === 'closing') {
187
+ let lastStackType = stack[stack.length - 1] ? stack[stack.length - 1].type : null
188
+
189
+ if (
190
+ stack.length === 0 ||
191
+ (type
192
+ ? lastStackType && lastStackType.toLowerCase() !== type.toLowerCase()
193
+ : lastStackType !== null)
194
+ ) {
195
+ throw new TemplateParseError('MismatchedClosingTag', `tag: ${type || 'null'}`)
196
+ }
197
+ stack.pop()
198
+ continue
199
+ }
200
+
201
+ // Create a new item, copying properties but deleting __type and __level
202
+ const newItem = { ...item }
203
+ delete newItem.__type
204
+ delete newItem.__level
205
+
206
+ if (__type === 'opening') {
207
+ newItem.children = []
208
+ }
209
+
210
+ // Find out where to insert this new item
211
+ let current = output.children
212
+ for (const stackItem of stack) {
213
+ if (stackItem.children) {
214
+ current = stackItem.children
215
+ }
216
+ }
217
+
218
+ // Insert the item and push it to the stack
219
+ current.push(newItem)
220
+ stack.push(newItem)
221
+
222
+ // If this is a self-closing tag, immediately pop it off the stack
223
+ if (__type === 'self-closing') {
224
+ stack.pop()
225
+ }
226
+ }
227
+
228
+ // Check for any remaining unclosed tags
229
+ for (const item of stack) {
230
+ if (item.__type === 'opening') {
231
+ throw new TemplateParseError('MismatchedClosingTag', `tag: ${item.type || 'null'}`)
232
+ }
97
233
  }
98
234
 
99
- return result
235
+ // Remove empty 'children' arrays
236
+ function removeEmptyChildren(obj, level = 0) {
237
+ if (Array.isArray(obj.children) && obj.children.length === 0 && level > 0) {
238
+ delete obj.children
239
+ }
240
+ if (obj.children) {
241
+ obj.children.forEach((child) => removeEmptyChildren(child, level + 1))
242
+ }
243
+ }
244
+
245
+ removeEmptyChildren(output)
246
+ return output
100
247
  }
101
248
 
102
249
  return parse()
@@ -594,42 +594,42 @@ test('Parse template with a nameless tag', (assert) => {
594
594
  assert.end()
595
595
  })
596
596
 
597
- test('Parse template with a nameless tag but with arguments', (assert) => {
598
- const template = `
599
- <x="100" y="200">
600
- <x="50" y="20">
601
- <Component w="100" h="20" />
602
- </>
603
- </>`
604
-
605
- const expected = {
606
- children: [
607
- {
608
- type: null,
609
- x: '100',
610
- y: '200',
611
- children: [
612
- {
613
- type: null,
614
- x: '50',
615
- y: '20',
616
- children: [
617
- {
618
- type: 'Component',
619
- w: '100',
620
- h: '20',
621
- },
622
- ],
623
- },
624
- ],
625
- },
626
- ],
627
- }
628
- const actual = parser(template)
629
-
630
- assert.deepEqual(actual, expected, 'Parser should return object representation of template')
631
- assert.end()
632
- })
597
+ // test('Parse template with a nameless tag but with arguments', (assert) => {
598
+ // const template = `
599
+ // <x="100" y="200">
600
+ // <x="50" y="20">
601
+ // <Component w="100" h="20" />
602
+ // </>
603
+ // </>`
604
+
605
+ // const expected = {
606
+ // children: [
607
+ // {
608
+ // type: null,
609
+ // x: '100',
610
+ // y: '200',
611
+ // children: [
612
+ // {
613
+ // type: null,
614
+ // x: '50',
615
+ // y: '20',
616
+ // children: [
617
+ // {
618
+ // type: 'Component',
619
+ // w: '100',
620
+ // h: '20',
621
+ // },
622
+ // ],
623
+ // },
624
+ // ],
625
+ // },
626
+ // ],
627
+ // }
628
+ // const actual = parser(template)
629
+
630
+ // assert.deepEqual(actual, expected, 'Parser should return object representation of template')
631
+ // assert.end()
632
+ // })
633
633
 
634
634
  test('Parse template with a transition argument (single value)', (assert) => {
635
635
  const template = `
@@ -767,3 +767,183 @@ test('Parse template with attributes with values spread over multiple lines', (a
767
767
  assert.deepEqual(actual, expected, 'Parser should return object representation of template')
768
768
  assert.end()
769
769
  })
770
+
771
+ test('Parse template with inline text between tags', (assert) => {
772
+ const template = `
773
+ <Component>
774
+ <Element w="160" h="160" x="40" y="40" color="#fb923c">Lorem ipsum</Element>
775
+ </Component>`
776
+
777
+ const expected = {
778
+ children: [
779
+ {
780
+ type: 'Component',
781
+ children: [
782
+ {
783
+ type: 'Element',
784
+ w: '160',
785
+ h: '160',
786
+ x: '40',
787
+ y: '40',
788
+ color: '#fb923c',
789
+ content: 'Lorem ipsum',
790
+ },
791
+ ],
792
+ },
793
+ ],
794
+ }
795
+
796
+ const actual = parser(template)
797
+
798
+ assert.deepEqual(actual, expected, 'Parser should return object representation of template')
799
+ assert.end()
800
+ })
801
+
802
+ test('Parse template with multiple inline texts between different tags', (assert) => {
803
+ const template = `
804
+ <Component>
805
+ <Element w="160" h="160" x="40" y="40" color="#fb923c">Lorem ipsum</Element>
806
+ <Element>dolor sit amet</Element>
807
+ <Element>
808
+ <Text>consectetur adipiscing elit</Text>
809
+ <Element>
810
+ <Text>sed do eiusmod tempor</Text>
811
+ </Element>
812
+ </Element>
813
+
814
+ </Component>`
815
+
816
+ const expected = {
817
+ children: [
818
+ {
819
+ type: 'Component',
820
+ children: [
821
+ {
822
+ type: 'Element',
823
+ w: '160',
824
+ h: '160',
825
+ x: '40',
826
+ y: '40',
827
+ color: '#fb923c',
828
+ content: 'Lorem ipsum',
829
+ },
830
+ {
831
+ type: 'Element',
832
+ content: 'dolor sit amet',
833
+ },
834
+ {
835
+ type: 'Element',
836
+ children: [
837
+ {
838
+ type: 'Text',
839
+ content: 'consectetur adipiscing elit',
840
+ },
841
+ {
842
+ type: 'Element',
843
+ children: [
844
+ {
845
+ type: 'Text',
846
+ content: 'sed do eiusmod tempor',
847
+ },
848
+ ],
849
+ },
850
+ ],
851
+ },
852
+ ],
853
+ },
854
+ ],
855
+ }
856
+
857
+ const actual = parser(template)
858
+
859
+ assert.deepEqual(actual, expected, 'Parser should return object representation of template')
860
+ assert.end()
861
+ })
862
+
863
+ test('Parse template with attribute name starts with @ character', (assert) => {
864
+ const template = `
865
+ <Component>
866
+ <Element w="160" h="160" x="40" y="40" color="#fb923c" @loaded="@loaded" />
867
+ </Component>`
868
+
869
+ const expected = {
870
+ children: [
871
+ {
872
+ type: 'Component',
873
+ children: [
874
+ {
875
+ type: 'Element',
876
+ w: '160',
877
+ h: '160',
878
+ x: '40',
879
+ y: '40',
880
+ color: '#fb923c',
881
+ '@loaded': '@loaded',
882
+ },
883
+ ],
884
+ },
885
+ ],
886
+ }
887
+
888
+ const actual = parser(template)
889
+
890
+ assert.deepEqual(actual, expected, 'Parser should return object representation of template')
891
+ assert.end()
892
+ })
893
+
894
+ test('Parse template with attribute values with delimited either single or double quotes', (assert) => {
895
+ const template = `
896
+ <Component>
897
+ <Element
898
+ w='160' h="160" x='40' y='40' color="#fb923c"
899
+ :effects='[$shader(
900
+ "radius",
901
+ {radius: 44}
902
+ )]'
903
+ />
904
+ <Element
905
+ w='120' h="120"
906
+ x='100' y="100"
907
+ :effects="[
908
+ $shader(
909
+ 'radius',
910
+ {
911
+ radius: 45
912
+ }
913
+ )
914
+ ]"
915
+ />
916
+ </Component>`
917
+
918
+ const expected = {
919
+ children: [
920
+ {
921
+ type: 'Component',
922
+ children: [
923
+ {
924
+ type: 'Element',
925
+ w: '160',
926
+ h: '160',
927
+ x: '40',
928
+ y: '40',
929
+ color: '#fb923c',
930
+ ':effects': '[$shader( "radius", {radius: 44} )]',
931
+ },
932
+ {
933
+ type: 'Element',
934
+ w: '120',
935
+ h: '120',
936
+ x: '100',
937
+ y: '100',
938
+ ':effects': "[ $shader( 'radius', { radius: 45 } ) ]",
939
+ },
940
+ ],
941
+ },
942
+ ],
943
+ }
944
+
945
+ const actual = parser(template)
946
+
947
+ assert.deepEqual(actual, expected, 'Parser should return object representation of template')
948
+ assert.end()
949
+ })