@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,386 +1,386 @@
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
- import symbols from '../symbols.js'
19
- import colors from '../colors/colors.js'
20
-
21
- export default (template = '', componentName, parentComponent, filePath = null) => {
22
- let cursor = 0
23
- let prevCursor = 0
24
- let tags = []
25
- let currentTag = null
26
- let currentLevel = 0
27
-
28
- // regular expressions
29
- const tagStartRegex = /^<\/?([a-zA-Z0-9_\-.]+)\s*/
30
- const tagEndRegex = /^\s*(\/?>)\s*/
31
- const attrNameRegex = /^([A-Za-z0-9:.\-_@$]+)=\s*(["'])/
32
- const emptyTagStartRegex = /^<>\s*/
33
- const emptyTagEndRegex = /^\s*(<\/>)\s*/
34
-
35
- // main functions
36
- const parse = () => {
37
- template = clean(template)
38
- try {
39
- parseLoop(parseEmptyTagStart)
40
- return format(tags)
41
- } catch (error) {
42
- if (error.name == 'TemplateParseError' || error.name == 'TemplateStructureError') {
43
- error.message = `${error.message}\n${error.context}`
44
- }
45
- throw error
46
- }
47
- }
48
-
49
- const parseLoop = (next) => {
50
- if (cursor >= template.length) {
51
- return
52
- }
53
- next()
54
- }
55
-
56
- // utils
57
- const clean = (templateText) => {
58
- // remove all unnecessary new lines and comments
59
- return templateText
60
- .replace(/<!--.*?-->/gms, '') // remove comments
61
- .replace(/\r?\n\s*\r\n/gm, ' ') // remove empty lines
62
- .replace(/\r?\n\s*(\S)/gm, ' $1') // remove line endings & spacing
63
- .replace(/\r?\n/g, '') // remove all line endings
64
- .trim()
65
- }
66
-
67
- const moveCursorOnMatch = (regex) => {
68
- const match = template.slice(cursor).match(regex)
69
- if (match) {
70
- prevCursor = cursor
71
- cursor += match[0].length
72
- }
73
- return match
74
- }
75
-
76
- // parsers
77
- const parseEmptyTagStart = () => {
78
- const match = moveCursorOnMatch(emptyTagStartRegex)
79
- if (match) {
80
- tags.push({
81
- [Symbol.for('componentType')]: null,
82
- [symbols.type]: 'opening',
83
- [symbols.level]: currentLevel,
84
- [symbols.cursorTagStart]: prevCursor,
85
- })
86
- currentLevel++
87
- parseLoop(parseEmptyTagStart)
88
- } else {
89
- parseLoop(parseEmptyTagEnd)
90
- }
91
- }
92
-
93
- const parseEmptyTagEnd = () => {
94
- const match = moveCursorOnMatch(emptyTagEndRegex)
95
- if (match) {
96
- currentLevel--
97
- tags.push({
98
- [Symbol.for('componentType')]: null,
99
- [symbols.type]: 'closing',
100
- [symbols.level]: currentLevel,
101
- })
102
- parseLoop(parseEmptyTagStart)
103
- } else {
104
- parseLoop(parseTag)
105
- }
106
- }
107
-
108
- const parseTag = () => {
109
- const match = moveCursorOnMatch(tagStartRegex)
110
- if (match) {
111
- currentTag = {
112
- [Symbol.for('componentType')]: match[1],
113
- [symbols.level]: currentLevel,
114
- [symbols.cursorTagStart]: prevCursor,
115
- }
116
- if (match[0].startsWith('</')) {
117
- currentLevel--
118
- currentTag[symbols.type] = 'closing'
119
- currentTag[symbols.level] = currentLevel
120
- } else {
121
- currentTag[symbols.type] = 'opening'
122
- currentLevel++
123
- }
124
- parseLoop(parseTagEnd)
125
- } else {
126
- throw TemplateParseError('InvalidTag')
127
- }
128
- }
129
-
130
- const parseTagEnd = () => {
131
- const match = moveCursorOnMatch(tagEndRegex)
132
- if (match) {
133
- if (match[1] === '/>') {
134
- if (currentTag[symbols.type] === 'closing') {
135
- throw TemplateParseError('InvalidClosingTag')
136
- }
137
- currentTag[symbols.type] = 'self-closing'
138
- currentLevel-- // because it was parsed as opening tag before
139
- }
140
-
141
- // parsing content in between tags
142
- // rule: < char cannot be used in between tags even in escaped form
143
- if (currentTag[symbols.type] === 'opening') {
144
- const tagContent = template.slice(cursor, template.indexOf('<', cursor))
145
- if (tagContent) {
146
- currentTag.content = tagContent
147
- cursor += tagContent.length
148
- }
149
- }
150
-
151
- tags.push(currentTag)
152
- parseLoop(parseEmptyTagStart)
153
- } else {
154
- parseLoop(parseAttributes)
155
- }
156
- }
157
-
158
- //fixme: closing tags cannot have attributes
159
- const parseAttributes = () => {
160
- const attrNameMatch = moveCursorOnMatch(attrNameRegex)
161
- if (attrNameMatch) {
162
- if (currentTag[symbols.type] === 'closing') {
163
- throw TemplateParseError('AttributesInClosingTag')
164
- }
165
-
166
- const delimiter = attrNameMatch[2]
167
- const attrValueRegex = new RegExp(`^(.*?)${delimiter}\\s*`)
168
- const attrValueMatch = moveCursorOnMatch(attrValueRegex)
169
-
170
- if (attrValueMatch) {
171
- // maybe this should be done while code generation step
172
- const attr = formatAttribute(attrNameMatch[1], attrValueMatch[1])
173
- currentTag[attr.name] = attr.value
174
- parseLoop(parseTagEnd)
175
- } else {
176
- throw TemplateParseError('MissingOrInvalidAttributeValue')
177
- }
178
- } else {
179
- throw TemplateParseError('InvalidAttribute')
180
- }
181
- }
182
-
183
- const formatAttribute = (name, value) => {
184
- // if name contains a dot then we need to add enclosing js object to the value
185
- // example :x.transition="{v: $offset}" must be formatted as { name: ':x', value: '{ transition: {v: $offset}' }
186
- if (name.includes('.')) {
187
- const [objectName, attributeName] = name.split('.')
188
- return { name: objectName, value: `{${attributeName}: ${value}}` }
189
- }
190
-
191
- // process color values
192
- if (['color', ':color', ':effects', 'effects'].includes(name)) {
193
- return processColors(name, value)
194
- }
195
- return { name, value }
196
- }
197
-
198
- const processColors = (name, value) => {
199
- let newValue = value //copy for processing
200
- let normalized = colors.normalize(newValue, null)
201
-
202
- if (normalized === null) {
203
- const stringTokenRegex = /'([^']+)'/g
204
- let match
205
- let lastIndex = 0
206
- let result = ''
207
-
208
- while ((match = stringTokenRegex.exec(value)) !== null) {
209
- const potentialColor = match[1]
210
- const matchIndex = match.index
211
- const matchLength = match[0].length
212
-
213
- result += value.slice(lastIndex, matchIndex)
214
- normalized = colors.normalize(potentialColor, null)
215
-
216
- if (normalized === null) {
217
- result += value.slice(matchIndex, matchIndex + matchLength)
218
- } else {
219
- result += `'${normalized}'`
220
- }
221
-
222
- lastIndex = matchIndex + matchLength
223
- }
224
-
225
- result += value.slice(lastIndex)
226
- newValue = result
227
- } else {
228
- newValue = normalized
229
- }
230
-
231
- return { name, value: newValue }
232
- }
233
-
234
- /*
235
- Formatting & validation rules:
236
- #1: Every opening tag must have a corresponding closing tag at the same level. If a closing tag is encountered without
237
- a preceding opening tag at the same level, or if an opening tag is not followed by a corresponding closing tag at
238
- the same level, an error should be thrown.
239
- #2: There must be exactly one top-level element (an element at level 0). This element may either be a self-closing
240
- element or an opening tag followed by a closing tag. If more than one top-level element is encountered, an error
241
- should be thrown.
242
- But the top-level element can not have :for attribute.
243
- */
244
- const format = (parsedData) => {
245
- let stack = []
246
- let rootElementDefined = false
247
- let output = { children: [] }
248
- let currentParent = output
249
-
250
- for (let i = 0; i < parsedData.length; i++) {
251
- let element = parsedData[i]
252
-
253
- // Rule #1
254
- if (element[symbols.level] === 0 && element[symbols.type] !== 'closing') {
255
- if (rootElementDefined) {
256
- throw TemplateStructureError('MultipleTopLevelTags', element)
257
- }
258
- // Check for :for attribute on root level element
259
- if (element[':for'] !== undefined) {
260
- throw TemplateStructureError('ForAttributeOnRootElement', element)
261
- }
262
- rootElementDefined = true
263
- }
264
-
265
- // Rule #2
266
- if (element[symbols.type] === 'opening') {
267
- stack.push({
268
- [symbols.level]: element[symbols.level],
269
- [symbols.type]: element[symbols.type],
270
- [symbols.cursorTagStart]: element[symbols.cursorTagStart],
271
- [Symbol.for('componentType')]: element[Symbol.for('componentType')],
272
- parent: currentParent, // helps getting the previous parent when closing tag is encountered
273
- })
274
- } else if (element[symbols.type] === 'closing') {
275
- const isStackEmpty = stack.length === 0
276
- let isLevelMismatch = false
277
- let isTagMismatch = false
278
- if (!isStackEmpty) {
279
- isLevelMismatch = stack[stack.length - 1][symbols.level] !== element[symbols.level]
280
- isTagMismatch =
281
- stack[stack.length - 1][Symbol.for('componentType')] !==
282
- element[Symbol.for('componentType')]
283
- }
284
-
285
- if (isStackEmpty || isLevelMismatch || isTagMismatch) {
286
- throw TemplateStructureError('MismatchedClosingTag', element)
287
- }
288
-
289
- // when we remove the closing element from the stack, we should set
290
- // the current parent to the parent of the closing element
291
- const lastTag = stack.pop()
292
- currentParent = lastTag.parent
293
- }
294
-
295
- const newItem = { ...element }
296
- delete newItem[symbols.type]
297
- delete newItem[symbols.level]
298
- delete newItem[symbols.cursorTagStart]
299
-
300
- // if it is an opening tag, add children[] to it and update current parent
301
- if (element[symbols.type] === 'opening') {
302
- // make sure the current opening tag has really a child element
303
- if (i + 1 < parsedData.length && parsedData[i + 1][symbols.type] !== 'closing') {
304
- newItem.children = []
305
- }
306
- currentParent.children.push(newItem)
307
- currentParent = newItem
308
- } else if (element[symbols.type] === 'self-closing') {
309
- currentParent.children.push(newItem)
310
- }
311
- }
312
-
313
- // Check if all tags are closed (so stack should be empty)[Rule #1]
314
- if (stack.length > 0) {
315
- throw TemplateStructureError('UnclosedTags', stack)
316
- }
317
-
318
- return output
319
- }
320
-
321
- // error reporting
322
- const contextPaddingBefore = 10 // number of characters to show before the error location
323
- const contextPaddingAfter = 50 // number of characters to show after the error location
324
-
325
- const TemplateParseError = (message) => {
326
- const location = getErrorLocation()
327
- message = `${message} in ${location}`
328
-
329
- const error = new Error(message)
330
- error.name = 'TemplateParseError'
331
-
332
- const start = Math.max(0, prevCursor - contextPaddingBefore)
333
- const end = Math.min(template.length, cursor + contextPaddingAfter)
334
- const contextText = template.slice(start, end)
335
-
336
- // add ^ caret to show where the error is
337
- const caretPosition = cursor - start
338
- error.context = insertContextCaret(caretPosition, contextText)
339
-
340
- return error
341
- }
342
-
343
- const TemplateStructureError = (message, context) => {
344
- const location = getErrorLocation()
345
- message = `${message} in ${location}`
346
-
347
- const error = new Error(message)
348
- error.name = 'TemplateStructureError'
349
-
350
- // check if context is an array
351
- if (Array.isArray(context)) {
352
- error.context = context.map((tag) => generateContext(tag)).join('\n')
353
- } else {
354
- error.context = generateContext(context)
355
- }
356
-
357
- function generateContext(element) {
358
- const start = Math.max(0, element[symbols.cursorTagStart] - contextPaddingBefore)
359
- const contextText = template.slice(start, start + contextPaddingAfter)
360
- // add ^ caret to show where the error is
361
- return insertContextCaret(contextPaddingBefore, contextText)
362
- }
363
- return error
364
- }
365
-
366
- const insertContextCaret = (position, contextText) => {
367
- const caret = ' '.repeat(position) + '^'
368
- return `\n${contextText}\n${caret}\n`
369
- }
370
-
371
- const getErrorLocation = () => {
372
- if (parentComponent) {
373
- let hierarchy = componentName || ''
374
- let currentParent = parentComponent
375
-
376
- while (currentParent) {
377
- hierarchy = `${currentParent[Symbol.for('componentType')]}/${hierarchy}`
378
- currentParent = currentParent.parent
379
- }
380
- return hierarchy
381
- }
382
- return filePath ? filePath : 'Blits.Application'
383
- }
384
-
385
- return parse()
386
- }
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
+ import symbols from '../symbols.js'
19
+ import colors from '../colors/colors.js'
20
+
21
+ export default (template = '', componentName, parentComponent, filePath = null) => {
22
+ let cursor = 0
23
+ let prevCursor = 0
24
+ let tags = []
25
+ let currentTag = null
26
+ let currentLevel = 0
27
+
28
+ // regular expressions
29
+ const tagStartRegex = /^<\/?([a-zA-Z0-9_\-.]+)\s*/
30
+ const tagEndRegex = /^\s*(\/?>)\s*/
31
+ const attrNameRegex = /^([A-Za-z0-9:.\-_@$]+)=\s*(["'])/
32
+ const emptyTagStartRegex = /^<>\s*/
33
+ const emptyTagEndRegex = /^\s*(<\/>)\s*/
34
+
35
+ // main functions
36
+ const parse = () => {
37
+ template = clean(template)
38
+ try {
39
+ parseLoop(parseEmptyTagStart)
40
+ return format(tags)
41
+ } catch (error) {
42
+ if (error.name == 'TemplateParseError' || error.name == 'TemplateStructureError') {
43
+ error.message = `${error.message}\n${error.context}`
44
+ }
45
+ throw error
46
+ }
47
+ }
48
+
49
+ const parseLoop = (next) => {
50
+ if (cursor >= template.length) {
51
+ return
52
+ }
53
+ next()
54
+ }
55
+
56
+ // utils
57
+ const clean = (templateText) => {
58
+ // remove all unnecessary new lines and comments
59
+ return templateText
60
+ .replace(/<!--.*?-->/gms, '') // remove comments
61
+ .replace(/\r?\n\s*\r\n/gm, ' ') // remove empty lines
62
+ .replace(/\r?\n\s*(\S)/gm, ' $1') // remove line endings & spacing
63
+ .replace(/\r?\n/g, '') // remove all line endings
64
+ .trim()
65
+ }
66
+
67
+ const moveCursorOnMatch = (regex) => {
68
+ const match = template.slice(cursor).match(regex)
69
+ if (match) {
70
+ prevCursor = cursor
71
+ cursor += match[0].length
72
+ }
73
+ return match
74
+ }
75
+
76
+ // parsers
77
+ const parseEmptyTagStart = () => {
78
+ const match = moveCursorOnMatch(emptyTagStartRegex)
79
+ if (match) {
80
+ tags.push({
81
+ [Symbol.for('componentType')]: null,
82
+ [symbols.type]: 'opening',
83
+ [symbols.level]: currentLevel,
84
+ [symbols.cursorTagStart]: prevCursor,
85
+ })
86
+ currentLevel++
87
+ parseLoop(parseEmptyTagStart)
88
+ } else {
89
+ parseLoop(parseEmptyTagEnd)
90
+ }
91
+ }
92
+
93
+ const parseEmptyTagEnd = () => {
94
+ const match = moveCursorOnMatch(emptyTagEndRegex)
95
+ if (match) {
96
+ currentLevel--
97
+ tags.push({
98
+ [Symbol.for('componentType')]: null,
99
+ [symbols.type]: 'closing',
100
+ [symbols.level]: currentLevel,
101
+ })
102
+ parseLoop(parseEmptyTagStart)
103
+ } else {
104
+ parseLoop(parseTag)
105
+ }
106
+ }
107
+
108
+ const parseTag = () => {
109
+ const match = moveCursorOnMatch(tagStartRegex)
110
+ if (match) {
111
+ currentTag = {
112
+ [Symbol.for('componentType')]: match[1],
113
+ [symbols.level]: currentLevel,
114
+ [symbols.cursorTagStart]: prevCursor,
115
+ }
116
+ if (match[0].startsWith('</')) {
117
+ currentLevel--
118
+ currentTag[symbols.type] = 'closing'
119
+ currentTag[symbols.level] = currentLevel
120
+ } else {
121
+ currentTag[symbols.type] = 'opening'
122
+ currentLevel++
123
+ }
124
+ parseLoop(parseTagEnd)
125
+ } else {
126
+ throw TemplateParseError('InvalidTag')
127
+ }
128
+ }
129
+
130
+ const parseTagEnd = () => {
131
+ const match = moveCursorOnMatch(tagEndRegex)
132
+ if (match) {
133
+ if (match[1] === '/>') {
134
+ if (currentTag[symbols.type] === 'closing') {
135
+ throw TemplateParseError('InvalidClosingTag')
136
+ }
137
+ currentTag[symbols.type] = 'self-closing'
138
+ currentLevel-- // because it was parsed as opening tag before
139
+ }
140
+
141
+ // parsing content in between tags
142
+ // rule: < char cannot be used in between tags even in escaped form
143
+ if (currentTag[symbols.type] === 'opening') {
144
+ const tagContent = template.slice(cursor, template.indexOf('<', cursor))
145
+ if (tagContent) {
146
+ currentTag.content = tagContent
147
+ cursor += tagContent.length
148
+ }
149
+ }
150
+
151
+ tags.push(currentTag)
152
+ parseLoop(parseEmptyTagStart)
153
+ } else {
154
+ parseLoop(parseAttributes)
155
+ }
156
+ }
157
+
158
+ //fixme: closing tags cannot have attributes
159
+ const parseAttributes = () => {
160
+ const attrNameMatch = moveCursorOnMatch(attrNameRegex)
161
+ if (attrNameMatch) {
162
+ if (currentTag[symbols.type] === 'closing') {
163
+ throw TemplateParseError('AttributesInClosingTag')
164
+ }
165
+
166
+ const delimiter = attrNameMatch[2]
167
+ const attrValueRegex = new RegExp(`^(.*?)${delimiter}\\s*`)
168
+ const attrValueMatch = moveCursorOnMatch(attrValueRegex)
169
+
170
+ if (attrValueMatch) {
171
+ // maybe this should be done while code generation step
172
+ const attr = formatAttribute(attrNameMatch[1], attrValueMatch[1])
173
+ currentTag[attr.name] = attr.value
174
+ parseLoop(parseTagEnd)
175
+ } else {
176
+ throw TemplateParseError('MissingOrInvalidAttributeValue')
177
+ }
178
+ } else {
179
+ throw TemplateParseError('InvalidAttribute')
180
+ }
181
+ }
182
+
183
+ const formatAttribute = (name, value) => {
184
+ // if name contains a dot then we need to add enclosing js object to the value
185
+ // example :x.transition="{v: $offset}" must be formatted as { name: ':x', value: '{ transition: {v: $offset}' }
186
+ if (name.includes('.')) {
187
+ const [objectName, attributeName] = name.split('.')
188
+ return { name: objectName, value: `{${attributeName}: ${value}}` }
189
+ }
190
+
191
+ // process color values
192
+ if (['color', ':color', ':effects', 'effects'].includes(name)) {
193
+ return processColors(name, value)
194
+ }
195
+ return { name, value }
196
+ }
197
+
198
+ const processColors = (name, value) => {
199
+ let newValue = value //copy for processing
200
+ let normalized = colors.normalize(newValue, null)
201
+
202
+ if (normalized === null) {
203
+ const stringTokenRegex = /'([^']+)'/g
204
+ let match
205
+ let lastIndex = 0
206
+ let result = ''
207
+
208
+ while ((match = stringTokenRegex.exec(value)) !== null) {
209
+ const potentialColor = match[1]
210
+ const matchIndex = match.index
211
+ const matchLength = match[0].length
212
+
213
+ result += value.slice(lastIndex, matchIndex)
214
+ normalized = colors.normalize(potentialColor, null)
215
+
216
+ if (normalized === null) {
217
+ result += value.slice(matchIndex, matchIndex + matchLength)
218
+ } else {
219
+ result += `'${normalized}'`
220
+ }
221
+
222
+ lastIndex = matchIndex + matchLength
223
+ }
224
+
225
+ result += value.slice(lastIndex)
226
+ newValue = result
227
+ } else {
228
+ newValue = normalized
229
+ }
230
+
231
+ return { name, value: newValue }
232
+ }
233
+
234
+ /*
235
+ Formatting & validation rules:
236
+ #1: Every opening tag must have a corresponding closing tag at the same level. If a closing tag is encountered without
237
+ a preceding opening tag at the same level, or if an opening tag is not followed by a corresponding closing tag at
238
+ the same level, an error should be thrown.
239
+ #2: There must be exactly one top-level element (an element at level 0). This element may either be a self-closing
240
+ element or an opening tag followed by a closing tag. If more than one top-level element is encountered, an error
241
+ should be thrown.
242
+ But the top-level element can not have :for attribute.
243
+ */
244
+ const format = (parsedData) => {
245
+ let stack = []
246
+ let rootElementDefined = false
247
+ let output = { children: [] }
248
+ let currentParent = output
249
+
250
+ for (let i = 0; i < parsedData.length; i++) {
251
+ let element = parsedData[i]
252
+
253
+ // Rule #1
254
+ if (element[symbols.level] === 0 && element[symbols.type] !== 'closing') {
255
+ if (rootElementDefined) {
256
+ throw TemplateStructureError('MultipleTopLevelTags', element)
257
+ }
258
+ // Check for :for attribute on root level element
259
+ if (element[':for'] !== undefined) {
260
+ throw TemplateStructureError('ForAttributeOnRootElement', element)
261
+ }
262
+ rootElementDefined = true
263
+ }
264
+
265
+ // Rule #2
266
+ if (element[symbols.type] === 'opening') {
267
+ stack.push({
268
+ [symbols.level]: element[symbols.level],
269
+ [symbols.type]: element[symbols.type],
270
+ [symbols.cursorTagStart]: element[symbols.cursorTagStart],
271
+ [Symbol.for('componentType')]: element[Symbol.for('componentType')],
272
+ parent: currentParent, // helps getting the previous parent when closing tag is encountered
273
+ })
274
+ } else if (element[symbols.type] === 'closing') {
275
+ const isStackEmpty = stack.length === 0
276
+ let isLevelMismatch = false
277
+ let isTagMismatch = false
278
+ if (!isStackEmpty) {
279
+ isLevelMismatch = stack[stack.length - 1][symbols.level] !== element[symbols.level]
280
+ isTagMismatch =
281
+ stack[stack.length - 1][Symbol.for('componentType')] !==
282
+ element[Symbol.for('componentType')]
283
+ }
284
+
285
+ if (isStackEmpty || isLevelMismatch || isTagMismatch) {
286
+ throw TemplateStructureError('MismatchedClosingTag', element)
287
+ }
288
+
289
+ // when we remove the closing element from the stack, we should set
290
+ // the current parent to the parent of the closing element
291
+ const lastTag = stack.pop()
292
+ currentParent = lastTag.parent
293
+ }
294
+
295
+ const newItem = { ...element }
296
+ delete newItem[symbols.type]
297
+ delete newItem[symbols.level]
298
+ delete newItem[symbols.cursorTagStart]
299
+
300
+ // if it is an opening tag, add children[] to it and update current parent
301
+ if (element[symbols.type] === 'opening') {
302
+ // make sure the current opening tag has really a child element
303
+ if (i + 1 < parsedData.length && parsedData[i + 1][symbols.type] !== 'closing') {
304
+ newItem.children = []
305
+ }
306
+ currentParent.children.push(newItem)
307
+ currentParent = newItem
308
+ } else if (element[symbols.type] === 'self-closing') {
309
+ currentParent.children.push(newItem)
310
+ }
311
+ }
312
+
313
+ // Check if all tags are closed (so stack should be empty)[Rule #1]
314
+ if (stack.length > 0) {
315
+ throw TemplateStructureError('UnclosedTags', stack)
316
+ }
317
+
318
+ return output
319
+ }
320
+
321
+ // error reporting
322
+ const contextPaddingBefore = 10 // number of characters to show before the error location
323
+ const contextPaddingAfter = 50 // number of characters to show after the error location
324
+
325
+ const TemplateParseError = (message) => {
326
+ const location = getErrorLocation()
327
+ message = `${message} in ${location}`
328
+
329
+ const error = new Error(message)
330
+ error.name = 'TemplateParseError'
331
+
332
+ const start = Math.max(0, prevCursor - contextPaddingBefore)
333
+ const end = Math.min(template.length, cursor + contextPaddingAfter)
334
+ const contextText = template.slice(start, end)
335
+
336
+ // add ^ caret to show where the error is
337
+ const caretPosition = cursor - start
338
+ error.context = insertContextCaret(caretPosition, contextText)
339
+
340
+ return error
341
+ }
342
+
343
+ const TemplateStructureError = (message, context) => {
344
+ const location = getErrorLocation()
345
+ message = `${message} in ${location}`
346
+
347
+ const error = new Error(message)
348
+ error.name = 'TemplateStructureError'
349
+
350
+ // check if context is an array
351
+ if (Array.isArray(context)) {
352
+ error.context = context.map((tag) => generateContext(tag)).join('\n')
353
+ } else {
354
+ error.context = generateContext(context)
355
+ }
356
+
357
+ function generateContext(element) {
358
+ const start = Math.max(0, element[symbols.cursorTagStart] - contextPaddingBefore)
359
+ const contextText = template.slice(start, start + contextPaddingAfter)
360
+ // add ^ caret to show where the error is
361
+ return insertContextCaret(contextPaddingBefore, contextText)
362
+ }
363
+ return error
364
+ }
365
+
366
+ const insertContextCaret = (position, contextText) => {
367
+ const caret = ' '.repeat(position) + '^'
368
+ return `\n${contextText}\n${caret}\n`
369
+ }
370
+
371
+ const getErrorLocation = () => {
372
+ if (parentComponent) {
373
+ let hierarchy = componentName || ''
374
+ let currentParent = parentComponent
375
+
376
+ while (currentParent) {
377
+ hierarchy = `${currentParent[Symbol.for('componentType')]}/${hierarchy}`
378
+ currentParent = currentParent.parent
379
+ }
380
+ return hierarchy
381
+ }
382
+ return filePath ? filePath : 'Blits.Application'
383
+ }
384
+
385
+ return parse()
386
+ }