@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
package/boilerplate/package.json
CHANGED
package/docs/watchers.md
CHANGED
package/package.json
CHANGED
|
@@ -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
|
-
//
|
|
178
|
-
|
|
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 (
|
|
183
|
-
|
|
197
|
+
for (let i = 0; i < parsedData.length; i++) {
|
|
198
|
+
let element = parsedData[i]
|
|
184
199
|
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
//
|
|
194
|
-
if (
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
const newItem = { ...item }
|
|
235
|
+
const newItem = { ...element }
|
|
211
236
|
delete newItem[symbols.type]
|
|
212
237
|
delete newItem[symbols.level]
|
|
213
238
|
|
|
214
|
-
if
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
})
|