@lightningjs/blits 0.4.1 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ # v0.4.2
4
+
5
+ _22 nov 2023_
6
+
7
+ - Improved parser and added more template validation (i.e. one single root element in a template)
8
+ - Fixed typo in documentation
9
+
3
10
  # v0.4.1
4
11
 
5
12
  _13 nov 2023_
@@ -31,6 +31,6 @@
31
31
  "vite": "^4.0.4"
32
32
  },
33
33
  "dependencies": {
34
- "@lightningjs/blits": "^0.4.1"
34
+ "@lightningjs/blits": "^0.4.2"
35
35
  }
36
36
  }
package/docs/watchers.md CHANGED
@@ -25,7 +25,7 @@ The watcher function receives two arguments: the new value and the old value of
25
25
  alpha: 0.2
26
26
  }
27
27
  },
28
- watchers: {
28
+ watch: {
29
29
  alpha(value, oldValue) {
30
30
  if(value > oldValue) {
31
31
  // Execute some logic when the 'alpha' value increases
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.cjs",
6
6
  "exports": {
@@ -19,7 +19,7 @@ import symbols from '../symbols.js'
19
19
 
20
20
  class TemplateParseError extends Error {
21
21
  constructor(message, name, context) {
22
- super(`TemplateParseError ${message}`)
22
+ super(`TemplateParseError: ${message}`)
23
23
  this.name = name
24
24
  this.context = context
25
25
  }
@@ -41,9 +41,8 @@ export default (template = '') => {
41
41
  // main functions
42
42
  const parse = () => {
43
43
  template = clean(template)
44
- parseLoop(parseEmptyTagStart)
45
-
46
44
  try {
45
+ parseLoop(parseEmptyTagStart)
47
46
  return format(tags)
48
47
  } catch (error) {
49
48
  if (error instanceof TemplateParseError) {
@@ -123,6 +122,10 @@ export default (template = '') => {
123
122
  const match = moveCursorOnMatch(tagEndRegex)
124
123
  if (match) {
125
124
  if (match[1] === '/>') {
125
+ if (currentTag[symbols.type] === 'closing') {
126
+ // 10 is arbitrary, just to show some context by moving the cursor back a bit
127
+ throw new TemplateParseError('InvalidClosingTag', template.slice(cursor - 10))
128
+ }
126
129
  currentTag[symbols.type] = 'self-closing'
127
130
  currentLevel-- // because it was parsed as opening tag before
128
131
  }
@@ -174,83 +177,94 @@ export default (template = '') => {
174
177
  return { name, value }
175
178
  }
176
179
 
177
- // formatter
178
- function format(data) {
180
+ // formating and validation
181
+
182
+ /*
183
+ validation rules:
184
+ #1: Every opening tag must have a corresponding closing tag at the same level. If a closing tag is encountered without
185
+ a preceding opening tag at the same level, or if an opening tag is not followed by a corresponding closing tag at
186
+ the same level, an error should be thrown.
187
+ #2: There must be exactly one top-level element (an element at level 0). This element may either be a self-closing
188
+ element or an opening tag followed by a closing tag. If more than one top-level element is encountered, an error
189
+ should be thrown.
190
+ */
191
+ const format = (parsedData) => {
179
192
  let stack = []
193
+ let rootElementDefined = false
180
194
  let output = { children: [] }
195
+ let currentParent = output
181
196
 
182
- for (const item of data) {
183
- const { type, [symbols.type]: __type, [symbols.level]: __level } = item
197
+ for (let i = 0; i < parsedData.length; i++) {
198
+ let element = parsedData[i]
184
199
 
185
- // Check for unclosed tags
186
- while (stack.length && stack[stack.length - 1][symbols.level] >= __level) {
187
- const popped = stack.pop()
188
- if (popped[symbols.type] === 'opening') {
189
- throw new TemplateParseError('MismatchedClosingTag', `tag: ${popped.type || 'null'}`)
200
+ // Rule #1
201
+ if (element[symbols.level] === 0 && element[symbols.type] !== 'closing') {
202
+ if (rootElementDefined) {
203
+ throw new TemplateParseError('MultipleTopLevelTags', formatErrorContext(element))
190
204
  }
205
+ rootElementDefined = true
191
206
  }
192
207
 
193
- // For closing tags, just pop the opening tag from stack and continue
194
- if (__type === 'closing') {
195
- let lastStackType = stack[stack.length - 1] ? stack[stack.length - 1].type : null
208
+ // Rule #2
209
+ if (element[symbols.type] === 'opening') {
210
+ stack.push({
211
+ [symbols.level]: element[symbols.level],
212
+ [symbols.type]: element[symbols.type],
213
+ type: element.type,
214
+ parent: currentParent, // helps getting the previous parent when closing tag is encountered
215
+ })
216
+ } else if (element[symbols.type] === 'closing') {
217
+ const isStackEmpty = stack.length === 0
218
+ let isLevelMismatch = false
219
+ let isTagMismatch = false
220
+ if (!isStackEmpty) {
221
+ isLevelMismatch = stack[stack.length - 1][symbols.level] !== element[symbols.level]
222
+ isTagMismatch = stack[stack.length - 1].type !== element.type
223
+ }
196
224
 
197
- if (
198
- stack.length === 0 ||
199
- (type
200
- ? lastStackType && lastStackType.toLowerCase() !== type.toLowerCase()
201
- : lastStackType !== null)
202
- ) {
203
- throw new TemplateParseError('MismatchedClosingTag', `tag: ${type || 'null'}`)
225
+ if (isStackEmpty || isLevelMismatch || isTagMismatch) {
226
+ throw new TemplateParseError('MismatchedClosingTag', formatErrorContext(element))
204
227
  }
205
- stack.pop()
206
- continue
228
+
229
+ // when we remove the closing element from the stack, we should set
230
+ // the current parent to the parent of the closing element
231
+ const lastTag = stack.pop()
232
+ currentParent = lastTag.parent
207
233
  }
208
234
 
209
- // Create a new item, copying properties but deleting __type and __level
210
- const newItem = { ...item }
235
+ const newItem = { ...element }
211
236
  delete newItem[symbols.type]
212
237
  delete newItem[symbols.level]
213
238
 
214
- if (__type === 'opening') {
215
- newItem.children = []
216
- }
217
-
218
- // Find out where to insert this new item
219
- let current = output.children
220
- for (const stackItem of stack) {
221
- if (stackItem.children) {
222
- current = stackItem.children
239
+ // if it is an opening tag, add children[] to it and update current parent
240
+ if (element[symbols.type] === 'opening') {
241
+ // make sure the current opening tag has really a child element
242
+ if (i + 1 < parsedData.length && parsedData[i + 1][symbols.type] !== 'closing') {
243
+ newItem.children = []
223
244
  }
224
- }
225
-
226
- // Insert the item and push it to the stack
227
- current.push(newItem)
228
- stack.push(newItem)
229
-
230
- // If this is a self-closing tag, immediately pop it off the stack
231
- if (__type === 'self-closing') {
232
- stack.pop()
245
+ currentParent.children.push(newItem)
246
+ currentParent = newItem
247
+ } else if (element[symbols.type] === 'self-closing') {
248
+ currentParent.children.push(newItem)
233
249
  }
234
250
  }
235
251
 
236
- // Check for any remaining unclosed tags
237
- for (const item of stack) {
238
- if (item.__type === 'opening') {
239
- throw new TemplateParseError('MismatchedClosingTag', `tag: ${item.type || 'null'}`)
240
- }
252
+ // Check if all tags are closed (so stack should be empty)[Rule #1]
253
+ if (stack.length > 0) {
254
+ const unclosedTags = stack
255
+ .map((item) => {
256
+ return formatErrorContext(item)
257
+ })
258
+ .join(', ')
259
+ throw new TemplateParseError('UnclosedTags', unclosedTags)
241
260
  }
242
261
 
243
- // Remove empty 'children' arrays
244
- function removeEmptyChildren(obj, level = 0) {
245
- if (Array.isArray(obj.children) && obj.children.length === 0 && level > 0) {
246
- delete obj.children
247
- }
248
- if (obj.children) {
249
- obj.children.forEach((child) => removeEmptyChildren(child, level + 1))
250
- }
262
+ function formatErrorContext(element) {
263
+ return `${element.type || 'empty-tag'}[${element[symbols.type]}] at level ${
264
+ element[symbols.level]
265
+ }`
251
266
  }
252
267
 
253
- removeEmptyChildren(output)
254
268
  return output
255
269
  }
256
270
 
@@ -947,3 +947,102 @@ test('Parse template with attribute values with delimited either single or doubl
947
947
  assert.deepEqual(actual, expected, 'Parser should return object representation of template')
948
948
  assert.end()
949
949
  })
950
+
951
+ test('Parse template with multiple top level elements and parsing should fail', (assert) => {
952
+ const template = `
953
+ <Component>
954
+ <Element
955
+ w='160' h="160" x='40' y='40' color="#fb923c"
956
+ :effects='[$shader(
957
+ "radius",
958
+ {radius: 44}
959
+ )]'
960
+ />
961
+ </Component>
962
+ <Component>
963
+ <Element
964
+ w='120' h="120"
965
+ x='100' y="100"
966
+ :effects="[
967
+ $shader(
968
+ 'radius',
969
+ {
970
+ radius: 45
971
+ }
972
+ )
973
+ ]"
974
+ />
975
+ </Component>`
976
+
977
+ const actual = parser(template)
978
+ assert.equal(actual, null, 'Parser should throw an error')
979
+
980
+ assert.end()
981
+ })
982
+
983
+ test('Parse template with unclosed tag and parsing should fail', (assert) => {
984
+ const template = `
985
+ <Component>
986
+ <Element>
987
+ </Component>
988
+ `
989
+
990
+ const actual = parser(template)
991
+ assert.equal(actual, null, 'Parser should throw an error')
992
+ assert.end()
993
+ })
994
+
995
+ test('Parse template with multiple unclosed tags and parsing should fail', (assert) => {
996
+ const template = `
997
+ <Component>
998
+ <Element>
999
+ <Button />
1000
+ <Text>Lorem Ipsum</Text>
1001
+ <Element>
1002
+ <Button />
1003
+ <Text>Lorem Ipsum</Text>
1004
+ </Component>
1005
+ `
1006
+
1007
+ const actual = parser(template)
1008
+ assert.equal(actual, null, 'Parser should throw an error')
1009
+ assert.end()
1010
+ })
1011
+
1012
+ test('Parse template with an invalid closing tag and parsing should fail', (assert) => {
1013
+ const template = `
1014
+ <Component>
1015
+ <Element>
1016
+ </Element/>
1017
+ </Component>
1018
+ `
1019
+
1020
+ const actual = parser(template)
1021
+ assert.equal(actual, null, 'Parser should throw an error')
1022
+
1023
+ assert.end()
1024
+ })
1025
+
1026
+ test('Parse template with multiple self-closing tags at the top level and parsing should fail', (assert) => {
1027
+ const template = `
1028
+ <Component/>
1029
+ <Element/>
1030
+ `
1031
+
1032
+ const actual = parser(template)
1033
+ assert.equal(actual, null, 'Parser should throw an error')
1034
+ assert.end()
1035
+ })
1036
+
1037
+ test('Parse template with a closing tag at the beginning and parsing should fail', (assert) => {
1038
+ const template = `
1039
+ </Element>
1040
+ <Component>
1041
+ <Element/>
1042
+ </Component>
1043
+ `
1044
+
1045
+ const actual = parser(template)
1046
+ assert.equal(actual, null, 'Parser should throw an error')
1047
+ assert.end()
1048
+ })