@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.
- package/.editorconfig +7 -7
- package/.eslintignore +1 -1
- package/.eslintrc.cjs +65 -65
- package/.github/workflows/comment-lint-results.yml +124 -124
- package/.github/workflows/comment-test-results.yml +108 -108
- package/.github/workflows/deploy-docs.yml +27 -27
- package/.github/workflows/run-blits-tests.yml +141 -141
- package/.github/workflows/run-lint.yml +127 -127
- package/.husky/pre-commit +1 -1
- package/.vscode/extensions.json +7 -7
- package/.vscode/settings.json +9 -9
- package/CHANGELOG.md +1019 -1012
- package/COPYING +1 -0
- package/LICENSE +202 -202
- package/NOTICE +12 -12
- package/README.md +33 -33
- package/bin/index.js +8 -8
- package/blits.d.ts +6 -6
- package/docs/README.md +19 -19
- package/docs/_sidebar.md +37 -37
- package/docs/built-in/directives.md +50 -50
- package/docs/built-in/for-loop.md +203 -203
- package/docs/built-in/layout.md +168 -168
- package/docs/components/component_state.md +90 -90
- package/docs/components/computed_properties.md +45 -45
- package/docs/components/lifecycle_events.md +42 -42
- package/docs/components/methods.md +45 -45
- package/docs/components/props.md +56 -56
- package/docs/components/user_input.md +186 -186
- package/docs/components/utility_methods.md +320 -320
- package/docs/components/watchers.md +34 -34
- package/docs/essentials/application_root.md +19 -19
- package/docs/essentials/components.md +68 -68
- package/docs/essentials/displaying_images.md +67 -67
- package/docs/essentials/displaying_text.md +135 -135
- package/docs/essentials/element_attributes.md +195 -195
- package/docs/essentials/settings.md +117 -117
- package/docs/essentials/template_syntax.md +83 -83
- package/docs/getting_started/file_structure.md +58 -58
- package/docs/getting_started/getting_started.md +45 -45
- package/docs/getting_started/intro.md +20 -20
- package/docs/index.html +47 -47
- package/docs/performance/lazy-loading.md +161 -161
- package/docs/plugins/global_app_state.md +98 -98
- package/docs/plugins/language.md +172 -172
- package/docs/plugins/text-to-speech-announcer.md +119 -119
- package/docs/plugins/theme.md +176 -176
- package/docs/router/basics.md +175 -175
- package/docs/router/hooks.md +70 -70
- package/docs/router/transitions.md +146 -146
- package/docs/sidebar.json +171 -171
- package/docs/transitions_animations/transitions.md +150 -150
- package/index.d.ts +1099 -1099
- package/index.js +28 -28
- package/jsconfig.json +12 -12
- package/package.json +56 -56
- package/public/assets/fps_sprite.base64.js +1 -1
- package/scripts/postpublish.js +59 -59
- package/scripts/prepublishOnly.js +84 -84
- package/scripts/prepush.js +78 -78
- package/src/announcer/announcer.js +162 -162
- package/src/announcer/speechSynthesis.js +107 -107
- package/src/application.js +100 -100
- package/src/component/base/announcer.js +27 -27
- package/src/component/base/events.js +69 -69
- package/src/component/base/index.js +33 -33
- package/src/component/base/methods.js +244 -244
- package/src/component/base/router.js +41 -41
- package/src/component/base/timeouts_intervals.js +99 -99
- package/src/component/base/utils.js +72 -72
- package/src/component/setup/computed.js +48 -48
- package/src/component/setup/index.js +62 -62
- package/src/component/setup/input.js +30 -30
- package/src/component/setup/methods.js +37 -37
- package/src/component/setup/props.js +62 -62
- package/src/component/setup/props.test.js +238 -238
- package/src/component/setup/routes.js +42 -42
- package/src/component/setup/state.js +59 -59
- package/src/component/setup/watch.js +35 -35
- package/src/component.js +437 -437
- package/src/component.test.js +388 -388
- package/src/components/Circle.js +73 -73
- package/src/components/Circle.orig.js +31 -31
- package/src/components/FPScounter.js +1510 -1510
- package/src/components/FPScounter.orig.js +121 -121
- package/src/components/RouterView.js +81 -81
- package/src/components/RouterView.orig.js +54 -54
- package/src/components/Sprite.js +94 -94
- package/src/components/Sprite.orig.js +58 -58
- package/src/components/index.js +28 -28
- package/src/components/index.orig.js +28 -28
- package/src/constants.js +21 -21
- package/src/engine.js +1 -1
- package/src/engines/L3/element.js +744 -744
- package/src/engines/L3/element.test.js +579 -579
- package/src/engines/L3/fontLoader.js +38 -38
- package/src/engines/L3/index.js +7 -7
- package/src/engines/L3/launch.js +123 -123
- package/src/engines/L3/shaderLoader.js +12 -12
- package/src/engines/index.js +1 -1
- package/src/focus.js +90 -90
- package/src/helpers/deepEqualArray.js +50 -50
- package/src/helpers/deepEqualArray.test.js +127 -127
- package/src/helpers/fetchJson.js +34 -34
- package/src/helpers/sequence.js +26 -26
- package/src/launch.js +123 -123
- package/src/lib/blitsfileconverter/blitsfileconverter.js +52 -52
- package/src/lib/codegenerator/generator.js +788 -788
- package/src/lib/codegenerator/generator.test.js +3801 -3801
- package/src/lib/colors/colors.js +92 -92
- package/src/lib/colors/colors.test.js +290 -290
- package/src/lib/colors/htmlColors.js +168 -168
- package/src/lib/componentId.js +49 -49
- package/src/lib/componentId.test.js +84 -84
- package/src/lib/eventListeners.js +123 -123
- package/src/lib/eventListeners.test.js +236 -236
- package/src/lib/hooks.js +59 -59
- package/src/lib/lifecycle.js +85 -85
- package/src/lib/log.js +117 -117
- package/src/lib/precompiler/precompiler.js +73 -73
- package/src/lib/reactivity/effect.js +93 -93
- package/src/lib/reactivity/effect.test.js +35 -35
- package/src/lib/reactivity/reactive.js +208 -208
- package/src/lib/reactivity/reactive.test.js +27 -27
- package/src/lib/reactivityguard/computedprops.js +218 -218
- package/src/lib/symbols.js +108 -108
- package/src/lib/templateparser/parser.js +386 -386
- package/src/lib/templateparser/parser.test.js +1411 -1411
- package/src/lib/utils.js +27 -27
- package/src/plugin.js +27 -27
- package/src/plugins/appstate.js +26 -26
- package/src/plugins/index.js +20 -20
- package/src/plugins/language.js +152 -152
- package/src/plugins/log.js +25 -25
- package/src/plugins/theme.js +94 -94
- package/src/router/router.js +514 -514
- package/src/router/router.test.js +473 -473
- package/src/router/transitions/fadeIn.js +27 -27
- package/src/router/transitions/fadeInOut.js +33 -33
- package/src/router/transitions/index.js +21 -21
- package/src/router/transitions/slideIn.js +28 -28
- package/src/router/transitions/slideInOut.js +33 -33
- package/src/settings.js +40 -40
- package/vite/blitsFileConverter.js +67 -67
- package/vite/index.js +28 -28
- package/vite/msdfGenerator.js +269 -269
- package/vite/preCompiler.js +42 -42
- package/vite/reactivityGuard.js +45 -45
- 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
|
+
}
|