@lightningjs/blits 1.30.2 → 1.31.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 (152) hide show
  1. package/CHANGELOG.md +1035 -1019
  2. package/LICENSE +202 -202
  3. package/NOTICE +12 -12
  4. package/README.md +33 -33
  5. package/bin/index.js +8 -8
  6. package/blits.d.ts +6 -6
  7. package/index.d.ts +1099 -1099
  8. package/index.js +28 -28
  9. package/package.json +57 -56
  10. package/public/assets/fps_sprite.base64.js +1 -1
  11. package/scripts/postpublish.js +59 -59
  12. package/scripts/prepublishOnly.js +84 -84
  13. package/scripts/prepush.js +78 -78
  14. package/src/announcer/announcer.js +162 -162
  15. package/src/announcer/speechSynthesis.js +107 -107
  16. package/src/application.js +100 -100
  17. package/src/component/base/announcer.js +27 -27
  18. package/src/component/base/events.js +69 -69
  19. package/src/component/base/index.js +33 -33
  20. package/src/component/base/methods.js +244 -244
  21. package/src/component/base/router.js +41 -41
  22. package/src/component/base/timeouts_intervals.js +99 -99
  23. package/src/component/base/utils.js +72 -72
  24. package/src/component/setup/computed.js +48 -48
  25. package/src/component/setup/index.js +62 -62
  26. package/src/component/setup/input.js +30 -30
  27. package/src/component/setup/methods.js +37 -37
  28. package/src/component/setup/props.js +62 -62
  29. package/src/component/setup/routes.js +42 -42
  30. package/src/component/setup/state.js +59 -59
  31. package/src/component/setup/watch.js +35 -35
  32. package/src/component.js +437 -437
  33. package/src/components/Circle.js +73 -73
  34. package/src/components/FPScounter.js +1510 -1510
  35. package/src/components/RouterView.js +81 -81
  36. package/src/components/Sprite.js +94 -94
  37. package/src/components/index.js +28 -28
  38. package/src/constants.js +21 -21
  39. package/src/engine.js +1 -1
  40. package/src/engines/L3/element.js +744 -744
  41. package/src/engines/L3/fontLoader.js +38 -38
  42. package/src/engines/L3/index.js +7 -7
  43. package/src/engines/L3/launch.js +123 -123
  44. package/src/engines/L3/shaderLoader.js +12 -12
  45. package/src/engines/index.js +1 -1
  46. package/src/focus.js +90 -90
  47. package/src/helpers/deepEqualArray.js +50 -50
  48. package/src/helpers/fetchJson.js +34 -34
  49. package/src/helpers/sequence.js +26 -26
  50. package/src/launch.js +123 -123
  51. package/src/lib/blitsfileconverter/blitsfileconverter.js +52 -52
  52. package/src/lib/codegenerator/generator.js +788 -788
  53. package/src/lib/colors/colors.js +92 -92
  54. package/src/lib/colors/htmlColors.js +168 -168
  55. package/src/lib/componentId.js +49 -49
  56. package/src/lib/eventListeners.js +123 -123
  57. package/src/lib/hooks.js +59 -59
  58. package/src/lib/lifecycle.js +85 -85
  59. package/src/lib/log.js +117 -117
  60. package/src/lib/precompiler/precompiler.js +73 -73
  61. package/src/lib/reactivity/effect.js +93 -93
  62. package/src/lib/reactivity/reactive.js +208 -208
  63. package/src/lib/reactivityguard/computedprops.js +218 -218
  64. package/src/lib/symbols.js +108 -108
  65. package/src/lib/templateparser/parser.js +386 -386
  66. package/src/lib/utils.js +27 -27
  67. package/src/plugin.js +27 -27
  68. package/src/plugins/appstate.js +26 -26
  69. package/src/plugins/index.js +20 -20
  70. package/src/plugins/language.js +152 -152
  71. package/src/plugins/log.js +25 -25
  72. package/src/plugins/theme.js +94 -94
  73. package/src/router/router.js +516 -514
  74. package/src/router/transitions/fadeIn.js +27 -27
  75. package/src/router/transitions/fadeInOut.js +33 -33
  76. package/src/router/transitions/index.js +21 -21
  77. package/src/router/transitions/slideIn.js +28 -28
  78. package/src/router/transitions/slideInOut.js +33 -33
  79. package/src/settings.js +40 -40
  80. package/vite/blitsFileConverter.js +67 -67
  81. package/vite/index.js +28 -28
  82. package/vite/msdfGenerator.js +269 -269
  83. package/vite/preCompiler.js +47 -42
  84. package/vite/reactivityGuard.js +45 -45
  85. package/vscode/data/template-attributes.json +589 -589
  86. package/.editorconfig +0 -7
  87. package/.eslintignore +0 -1
  88. package/.eslintrc.cjs +0 -65
  89. package/.github/workflows/comment-lint-results.yml +0 -125
  90. package/.github/workflows/comment-test-results.yml +0 -109
  91. package/.github/workflows/deploy-docs.yml +0 -27
  92. package/.github/workflows/run-blits-tests.yml +0 -142
  93. package/.github/workflows/run-lint.yml +0 -128
  94. package/.husky/pre-commit +0 -1
  95. package/.husky/pre-push +0 -1
  96. package/.vscode/extensions.json +0 -7
  97. package/.vscode/settings.json +0 -9
  98. package/COPYING +0 -1
  99. package/banner.png +0 -0
  100. package/docs/.nojekyll +0 -0
  101. package/docs/README.md +0 -19
  102. package/docs/_sidebar.md +0 -37
  103. package/docs/built-in/directives.md +0 -50
  104. package/docs/built-in/for-loop.md +0 -204
  105. package/docs/built-in/layout.md +0 -168
  106. package/docs/components/component_state.md +0 -90
  107. package/docs/components/computed_properties.md +0 -45
  108. package/docs/components/lifecycle_events.md +0 -42
  109. package/docs/components/methods.md +0 -45
  110. package/docs/components/props.md +0 -56
  111. package/docs/components/user_input.md +0 -186
  112. package/docs/components/utility_methods.md +0 -320
  113. package/docs/components/watchers.md +0 -34
  114. package/docs/essentials/application_root.md +0 -19
  115. package/docs/essentials/components.md +0 -68
  116. package/docs/essentials/displaying_images.md +0 -67
  117. package/docs/essentials/displaying_text.md +0 -135
  118. package/docs/essentials/element_attributes.md +0 -195
  119. package/docs/essentials/settings.md +0 -117
  120. package/docs/essentials/template_syntax.md +0 -83
  121. package/docs/getting_started/file_structure.md +0 -58
  122. package/docs/getting_started/getting_started.md +0 -45
  123. package/docs/getting_started/intro.md +0 -20
  124. package/docs/index.html +0 -47
  125. package/docs/performance/lazy-loading.md +0 -161
  126. package/docs/plugins/global_app_state.md +0 -98
  127. package/docs/plugins/language.md +0 -172
  128. package/docs/plugins/text-to-speech-announcer.md +0 -120
  129. package/docs/plugins/theme.md +0 -176
  130. package/docs/router/basics.md +0 -175
  131. package/docs/router/hooks.md +0 -70
  132. package/docs/router/transitions.md +0 -146
  133. package/docs/sidebar.json +0 -171
  134. package/docs/transitions_animations/transitions.md +0 -150
  135. package/jsconfig.json +0 -12
  136. package/src/component/setup/props.test.js +0 -238
  137. package/src/component.test.js +0 -388
  138. package/src/components/Circle.orig.js +0 -31
  139. package/src/components/FPScounter.orig.js +0 -121
  140. package/src/components/RouterView.orig.js +0 -54
  141. package/src/components/Sprite.orig.js +0 -58
  142. package/src/components/index.orig.js +0 -28
  143. package/src/engines/L3/element.test.js +0 -579
  144. package/src/helpers/deepEqualArray.test.js +0 -127
  145. package/src/lib/codegenerator/generator.test.js +0 -3801
  146. package/src/lib/colors/colors.test.js +0 -290
  147. package/src/lib/componentId.test.js +0 -84
  148. package/src/lib/eventListeners.test.js +0 -236
  149. package/src/lib/reactivity/effect.test.js +0 -35
  150. package/src/lib/reactivity/reactive.test.js +0 -27
  151. package/src/lib/templateparser/parser.test.js +0 -1411
  152. package/src/router/router.test.js +0 -473
@@ -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
+ }