@lightningjs/blits
Advanced tools
Comparing version 0.4.1 to 0.4.2
@@ -34,4 +34,4 @@ { | ||
"dependencies": { | ||
"@lightningjs/blits": "^0.4.1" | ||
"@lightningjs/blits": "^0.4.2" | ||
} | ||
} |
# Changelog | ||
# v0.4.2 | ||
_22 nov 2023_ | ||
- Improved parser and added more template validation (i.e. one single root element in a template) | ||
- Fixed typo in documentation | ||
# v0.4.1 | ||
@@ -4,0 +11,0 @@ |
@@ -28,3 +28,3 @@ # Blits - Lightning 3 App Development Framework | ||
}, | ||
watchers: { | ||
watch: { | ||
alpha(value, oldValue) { | ||
@@ -31,0 +31,0 @@ if(value > oldValue) { |
{ | ||
"name": "@lightningjs/blits", | ||
"version": "0.4.1", | ||
"version": "0.4.2", | ||
"description": "Blits: The Lightning 3 App Development Framework", | ||
@@ -5,0 +5,0 @@ "bin": "bin/index.cjs", |
@@ -22,3 +22,3 @@ /* | ||
constructor(message, name, context) { | ||
super(`TemplateParseError ${message}`) | ||
super(`TemplateParseError: ${message}`) | ||
this.name = name | ||
@@ -45,5 +45,4 @@ this.context = context | ||
template = clean(template) | ||
parseLoop(parseEmptyTagStart) | ||
try { | ||
parseLoop(parseEmptyTagStart) | ||
return format(tags) | ||
@@ -128,2 +127,6 @@ } catch (error) { | ||
if (match[1] === '/>') { | ||
if (currentTag[symbols.type] === 'closing') { | ||
// 10 is arbitrary, just to show some context by moving the cursor back a bit | ||
throw new TemplateParseError('InvalidClosingTag', template.slice(cursor - 10)) | ||
} | ||
currentTag[symbols.type] = 'self-closing' | ||
@@ -180,79 +183,90 @@ currentLevel-- // because it was parsed as opening tag before | ||
// formatter | ||
function format(data) { | ||
// formating and validation | ||
/* | ||
validation rules: | ||
#1: Every opening tag must have a corresponding closing tag at the same level. If a closing tag is encountered without | ||
a preceding opening tag at the same level, or if an opening tag is not followed by a corresponding closing tag at | ||
the same level, an error should be thrown. | ||
#2: There must be exactly one top-level element (an element at level 0). This element may either be a self-closing | ||
element or an opening tag followed by a closing tag. If more than one top-level element is encountered, an error | ||
should be thrown. | ||
*/ | ||
const format = (parsedData) => { | ||
let stack = [] | ||
let rootElementDefined = false | ||
let output = { children: [] } | ||
let currentParent = output | ||
for (const item of data) { | ||
const { type, [symbols.type]: __type, [symbols.level]: __level } = item | ||
for (let i = 0; i < parsedData.length; i++) { | ||
let element = parsedData[i] | ||
// Check for unclosed tags | ||
while (stack.length && stack[stack.length - 1][symbols.level] >= __level) { | ||
const popped = stack.pop() | ||
if (popped[symbols.type] === 'opening') { | ||
throw new TemplateParseError('MismatchedClosingTag', `tag: ${popped.type || 'null'}`) | ||
// Rule #1 | ||
if (element[symbols.level] === 0 && element[symbols.type] !== 'closing') { | ||
if (rootElementDefined) { | ||
throw new TemplateParseError('MultipleTopLevelTags', formatErrorContext(element)) | ||
} | ||
rootElementDefined = true | ||
} | ||
// For closing tags, just pop the opening tag from stack and continue | ||
if (__type === 'closing') { | ||
let lastStackType = stack[stack.length - 1] ? stack[stack.length - 1].type : null | ||
// Rule #2 | ||
if (element[symbols.type] === 'opening') { | ||
stack.push({ | ||
[symbols.level]: element[symbols.level], | ||
[symbols.type]: element[symbols.type], | ||
type: element.type, | ||
parent: currentParent, // helps getting the previous parent when closing tag is encountered | ||
}) | ||
} else if (element[symbols.type] === 'closing') { | ||
const isStackEmpty = stack.length === 0 | ||
let isLevelMismatch = false | ||
let isTagMismatch = false | ||
if (!isStackEmpty) { | ||
isLevelMismatch = stack[stack.length - 1][symbols.level] !== element[symbols.level] | ||
isTagMismatch = stack[stack.length - 1].type !== element.type | ||
} | ||
if ( | ||
stack.length === 0 || | ||
(type | ||
? lastStackType && lastStackType.toLowerCase() !== type.toLowerCase() | ||
: lastStackType !== null) | ||
) { | ||
throw new TemplateParseError('MismatchedClosingTag', `tag: ${type || 'null'}`) | ||
if (isStackEmpty || isLevelMismatch || isTagMismatch) { | ||
throw new TemplateParseError('MismatchedClosingTag', formatErrorContext(element)) | ||
} | ||
stack.pop() | ||
continue | ||
// when we remove the closing element from the stack, we should set | ||
// the current parent to the parent of the closing element | ||
const lastTag = stack.pop() | ||
currentParent = lastTag.parent | ||
} | ||
// Create a new item, copying properties but deleting __type and __level | ||
const newItem = { ...item } | ||
const newItem = { ...element } | ||
delete newItem[symbols.type] | ||
delete newItem[symbols.level] | ||
if (__type === 'opening') { | ||
newItem.children = [] | ||
} | ||
// Find out where to insert this new item | ||
let current = output.children | ||
for (const stackItem of stack) { | ||
if (stackItem.children) { | ||
current = stackItem.children | ||
// if it is an opening tag, add children[] to it and update current parent | ||
if (element[symbols.type] === 'opening') { | ||
// make sure the current opening tag has really a child element | ||
if (i + 1 < parsedData.length && parsedData[i + 1][symbols.type] !== 'closing') { | ||
newItem.children = [] | ||
} | ||
currentParent.children.push(newItem) | ||
currentParent = newItem | ||
} else if (element[symbols.type] === 'self-closing') { | ||
currentParent.children.push(newItem) | ||
} | ||
// Insert the item and push it to the stack | ||
current.push(newItem) | ||
stack.push(newItem) | ||
// If this is a self-closing tag, immediately pop it off the stack | ||
if (__type === 'self-closing') { | ||
stack.pop() | ||
} | ||
} | ||
// Check for any remaining unclosed tags | ||
for (const item of stack) { | ||
if (item.__type === 'opening') { | ||
throw new TemplateParseError('MismatchedClosingTag', `tag: ${item.type || 'null'}`) | ||
} | ||
// Check if all tags are closed (so stack should be empty)[Rule #1] | ||
if (stack.length > 0) { | ||
const unclosedTags = stack | ||
.map((item) => { | ||
return formatErrorContext(item) | ||
}) | ||
.join(', ') | ||
throw new TemplateParseError('UnclosedTags', unclosedTags) | ||
} | ||
// Remove empty 'children' arrays | ||
function removeEmptyChildren(obj, level = 0) { | ||
if (Array.isArray(obj.children) && obj.children.length === 0 && level > 0) { | ||
delete obj.children | ||
} | ||
if (obj.children) { | ||
obj.children.forEach((child) => removeEmptyChildren(child, level + 1)) | ||
} | ||
function formatErrorContext(element) { | ||
return `${element.type || 'empty-tag'}[${element[symbols.type]}] at level ${ | ||
element[symbols.level] | ||
}` | ||
} | ||
removeEmptyChildren(output) | ||
return output | ||
@@ -259,0 +273,0 @@ } |
@@ -950,1 +950,100 @@ /* | ||
}) | ||
test('Parse template with multiple top level elements and parsing should fail', (assert) => { | ||
const template = ` | ||
<Component> | ||
<Element | ||
w='160' h="160" x='40' y='40' color="#fb923c" | ||
:effects='[$shader( | ||
"radius", | ||
{radius: 44} | ||
)]' | ||
/> | ||
</Component> | ||
<Component> | ||
<Element | ||
w='120' h="120" | ||
x='100' y="100" | ||
:effects="[ | ||
$shader( | ||
'radius', | ||
{ | ||
radius: 45 | ||
} | ||
) | ||
]" | ||
/> | ||
</Component>` | ||
const actual = parser(template) | ||
assert.equal(actual, null, 'Parser should throw an error') | ||
assert.end() | ||
}) | ||
test('Parse template with unclosed tag and parsing should fail', (assert) => { | ||
const template = ` | ||
<Component> | ||
<Element> | ||
</Component> | ||
` | ||
const actual = parser(template) | ||
assert.equal(actual, null, 'Parser should throw an error') | ||
assert.end() | ||
}) | ||
test('Parse template with multiple unclosed tags and parsing should fail', (assert) => { | ||
const template = ` | ||
<Component> | ||
<Element> | ||
<Button /> | ||
<Text>Lorem Ipsum</Text> | ||
<Element> | ||
<Button /> | ||
<Text>Lorem Ipsum</Text> | ||
</Component> | ||
` | ||
const actual = parser(template) | ||
assert.equal(actual, null, 'Parser should throw an error') | ||
assert.end() | ||
}) | ||
test('Parse template with an invalid closing tag and parsing should fail', (assert) => { | ||
const template = ` | ||
<Component> | ||
<Element> | ||
</Element/> | ||
</Component> | ||
` | ||
const actual = parser(template) | ||
assert.equal(actual, null, 'Parser should throw an error') | ||
assert.end() | ||
}) | ||
test('Parse template with multiple self-closing tags at the top level and parsing should fail', (assert) => { | ||
const template = ` | ||
<Component/> | ||
<Element/> | ||
` | ||
const actual = parser(template) | ||
assert.equal(actual, null, 'Parser should throw an error') | ||
assert.end() | ||
}) | ||
test('Parse template with a closing tag at the beginning and parsing should fail', (assert) => { | ||
const template = ` | ||
</Element> | ||
<Component> | ||
<Element/> | ||
</Component> | ||
` | ||
const actual = parser(template) | ||
assert.equal(actual, null, 'Parser should throw an error') | ||
assert.end() | ||
}) |
904228
22136