@canvasengine/compiler 2.0.0-beta.15 → 2.0.0-beta.17
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/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/grammar.pegjs +119 -28
- package/index.ts +37 -2
- package/package.json +1 -1
- package/tests/compiler.spec.ts +100 -0
package/dist/index.js
CHANGED
|
@@ -8,6 +8,20 @@ import * as ts from "typescript";
|
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
var { generate } = pkg;
|
|
10
10
|
var DEV_SRC = "../../src";
|
|
11
|
+
function showErrorMessage(template, error) {
|
|
12
|
+
if (!error.location) {
|
|
13
|
+
return `Syntax error: ${error.message}`;
|
|
14
|
+
}
|
|
15
|
+
const lines = template.split("\n");
|
|
16
|
+
const { line, column } = error.location.start;
|
|
17
|
+
const errorLine = lines[line - 1] || "";
|
|
18
|
+
const pointer = " ".repeat(column - 1) + "^";
|
|
19
|
+
return `Syntax error at line ${line}, column ${column}: ${error.message}
|
|
20
|
+
|
|
21
|
+
${errorLine}
|
|
22
|
+
${pointer}
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
11
25
|
function canvasengine() {
|
|
12
26
|
const filter = createFilter("**/*.ce");
|
|
13
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -46,7 +60,14 @@ function canvasengine() {
|
|
|
46
60
|
template = template.replace(/<svg>([\s\S]*?)<\/svg>/g, (match, content) => {
|
|
47
61
|
return `<Svg content="${content.trim()}" />`;
|
|
48
62
|
});
|
|
49
|
-
|
|
63
|
+
let parsedTemplate;
|
|
64
|
+
try {
|
|
65
|
+
parsedTemplate = parser.parse(template);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const errorMsg = showErrorMessage(template, error);
|
|
68
|
+
throw new Error(`Error parsing template in file ${id}:
|
|
69
|
+
${errorMsg}`);
|
|
70
|
+
}
|
|
50
71
|
scriptContent += FLAG_COMMENT + parsedTemplate;
|
|
51
72
|
let transpiledCode = ts.transpileModule(scriptContent, {
|
|
52
73
|
compilerOptions: {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts"],"sourcesContent":["import { createFilter } from \"vite\";\nimport { parse } from \"acorn\";\nimport fs from \"fs\";\nimport pkg from \"peggy\";\nimport path from \"path\";\nimport * as ts from \"typescript\";\nimport { fileURLToPath } from 'url';\n\nconst { generate } = pkg;\n\nconst DEV_SRC = \"../../src\"\n\nexport default function canvasengine() {\n const filter = createFilter(\"**/*.ce\");\n\n // Convert import.meta.url to a file path\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = path.dirname(__filename);\n\n const grammar = fs.readFileSync(\n path.join(__dirname, \"grammar.pegjs\").replace(\"dist/grammar.pegjs\", \"grammar.pegjs\"), \n \"utf8\");\n const parser = generate(grammar);\n const isDev = process.env.NODE_ENV === \"dev\";\n const FLAG_COMMENT = \"/*--[TPL]--*/\";\n\n const PRIMITIVE_COMPONENTS = [\n \"Canvas\",\n \"Sprite\",\n \"Text\",\n \"Viewport\",\n \"Graphics\",\n \"Container\",\n \"ImageMap\",\n \"NineSliceSprite\",\n \"Rect\",\n \"Circle\",\n \"Ellipse\",\n \"Triangle\",\n \"TilingSprite\",\n \"svg\",\n \"Video\"\n ];\n\n return {\n name: \"vite-plugin-ce\",\n transform(code: string, id: string) {\n if (!filter(id)) return;\n\n // Extract the script content\n const scriptMatch = code.match(/<script>([\\s\\S]*?)<\\/script>/);\n let scriptContent = scriptMatch ? scriptMatch[1].trim() : \"\";\n \n // Transform SVG tags to Svg components\n let template = code.replace(/<script>[\\s\\S]*?<\\/script>/, \"\")\n .replace(/^\\s+|\\s+$/g, '');\n \n // Add SVG transformation\n template = template.replace(/<svg>([\\s\\S]*?)<\\/svg>/g, (match, content) => {\n return `<Svg content=\"${content.trim()}\" />`;\n });\n
|
|
1
|
+
{"version":3,"sources":["../index.ts"],"sourcesContent":["import { createFilter } from \"vite\";\nimport { parse } from \"acorn\";\nimport fs from \"fs\";\nimport pkg from \"peggy\";\nimport path from \"path\";\nimport * as ts from \"typescript\";\nimport { fileURLToPath } from 'url';\n\nconst { generate } = pkg;\n\nconst DEV_SRC = \"../../src\"\n\n/**\n * Formats a syntax error message with visual pointer to the error location\n * \n * @param {string} template - The template content that failed to parse\n * @param {object} error - The error object with location information\n * @returns {string} - Formatted error message with a visual pointer\n * \n * @example\n * ```\n * const errorMessage = showErrorMessage(\"<Canvas>test(d)</Canvas>\", syntaxError);\n * // Returns a formatted error message with an arrow pointing to 'd'\n * ```\n */\nfunction showErrorMessage(template: string, error: any): string {\n if (!error.location) {\n return `Syntax error: ${error.message}`;\n }\n\n const lines = template.split('\\n');\n const { line, column } = error.location.start;\n const errorLine = lines[line - 1] || '';\n \n // Create a visual pointer with an arrow\n const pointer = ' '.repeat(column - 1) + '^';\n \n return `Syntax error at line ${line}, column ${column}: ${error.message}\\n\\n` +\n `${errorLine}\\n${pointer}\\n`;\n}\n\nexport default function canvasengine() {\n const filter = createFilter(\"**/*.ce\");\n\n // Convert import.meta.url to a file path\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = path.dirname(__filename);\n\n const grammar = fs.readFileSync(\n path.join(__dirname, \"grammar.pegjs\").replace(\"dist/grammar.pegjs\", \"grammar.pegjs\"), \n \"utf8\");\n const parser = generate(grammar);\n const isDev = process.env.NODE_ENV === \"dev\";\n const FLAG_COMMENT = \"/*--[TPL]--*/\";\n\n const PRIMITIVE_COMPONENTS = [\n \"Canvas\",\n \"Sprite\",\n \"Text\",\n \"Viewport\",\n \"Graphics\",\n \"Container\",\n \"ImageMap\",\n \"NineSliceSprite\",\n \"Rect\",\n \"Circle\",\n \"Ellipse\",\n \"Triangle\",\n \"TilingSprite\",\n \"svg\",\n \"Video\"\n ];\n\n return {\n name: \"vite-plugin-ce\",\n transform(code: string, id: string) {\n if (!filter(id)) return;\n\n // Extract the script content\n const scriptMatch = code.match(/<script>([\\s\\S]*?)<\\/script>/);\n let scriptContent = scriptMatch ? scriptMatch[1].trim() : \"\";\n \n // Transform SVG tags to Svg components\n let template = code.replace(/<script>[\\s\\S]*?<\\/script>/, \"\")\n .replace(/^\\s+|\\s+$/g, '');\n \n // Add SVG transformation\n template = template.replace(/<svg>([\\s\\S]*?)<\\/svg>/g, (match, content) => {\n return `<Svg content=\"${content.trim()}\" />`;\n });\n\n let parsedTemplate;\n try {\n parsedTemplate = parser.parse(template);\n } catch (error) {\n const errorMsg = showErrorMessage(template, error);\n throw new Error(`Error parsing template in file ${id}:\\n${errorMsg}`);\n }\n\n // trick to avoid typescript remove imports in scriptContent\n scriptContent += FLAG_COMMENT + parsedTemplate\n\n let transpiledCode = ts.transpileModule(scriptContent, {\n compilerOptions: {\n module: ts.ModuleKind.Preserve,\n },\n }).outputText;\n\n // remove code after /*---*/\n transpiledCode = transpiledCode.split(FLAG_COMMENT)[0]\n\n // Use Acorn to parse the script content\n const parsed = parse(transpiledCode, {\n sourceType: \"module\",\n ecmaVersion: 2020,\n });\n\n // Extract imports\n const imports = parsed.body.filter(\n (node) => node.type === \"ImportDeclaration\"\n );\n\n // Extract non-import statements from scriptContent\n const nonImportCode = parsed.body\n .filter((node) => node.type !== \"ImportDeclaration\")\n .map((node) => transpiledCode.slice(node.start, node.end))\n .join(\"\\n\");\n\n let importsCode = imports\n .map((imp) => {\n let importCode = transpiledCode.slice(imp.start, imp.end);\n if (isDev && importCode.includes(\"from 'canvasengine'\")) {\n importCode = importCode.replace(\n \"from 'canvasengine'\",\n `from '${DEV_SRC}'`\n );\n }\n return importCode;\n })\n .join(\"\\n\");\n\n // Define an array for required imports\n const requiredImports = [\"h\", \"computed\", \"cond\", \"loop\"];\n\n // Check for missing imports\n const missingImports = requiredImports.filter(\n (importName) =>\n !imports.some(\n (imp) =>\n imp.specifiers &&\n imp.specifiers.some(\n (spec) =>\n spec.type === \"ImportSpecifier\" &&\n spec.imported && \n 'name' in spec.imported &&\n spec.imported.name === importName\n )\n )\n );\n\n // Add missing imports\n if (missingImports.length > 0) {\n const additionalImportCode = `import { ${missingImports.join(\n \", \"\n )} } from ${isDev ? `'${DEV_SRC}'` : \"'canvasengine'\"};`;\n importsCode = `${additionalImportCode}\\n${importsCode}`;\n }\n\n // Check for primitive components in parsedTemplate\n const primitiveImports = PRIMITIVE_COMPONENTS.filter((component) =>\n parsedTemplate.includes(`h(${component}`)\n );\n\n // Add missing imports for primitive components\n primitiveImports.forEach((component) => {\n const importStatement = `import { ${component} } from ${\n isDev ? `'${DEV_SRC}'` : \"'canvasengine'\"\n };`;\n if (!importsCode.includes(importStatement)) {\n importsCode = `${importStatement}\\n${importsCode}`;\n }\n });\n\n // Generate the output\n const output = String.raw`\n ${importsCode}\n import { useProps, useDefineProps } from ${isDev ? `'${DEV_SRC}'` : \"'canvasengine'\"}\n\n export default function component($$props) {\n const $props = useProps($$props)\n const defineProps = useDefineProps($$props)\n ${nonImportCode}\n let $this = ${parsedTemplate}\n return $this\n }\n `;\n\n return {\n code: output,\n map: null,\n };\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/grammar.pegjs
CHANGED
|
@@ -6,6 +6,29 @@
|
|
|
6
6
|
throw new Error(errorMessage);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/*——— Custom error handler for syntax errors ———*/
|
|
10
|
+
function parseError(error) {
|
|
11
|
+
// error.expected : array of { type, description }
|
|
12
|
+
// error.found : string | null
|
|
13
|
+
// error.location : { start, end }
|
|
14
|
+
const { expected, found, location } = error;
|
|
15
|
+
|
|
16
|
+
// Group expected items by description to avoid duplicates
|
|
17
|
+
const uniqueExpected = [...new Set(expected.map(e => e.description))];
|
|
18
|
+
|
|
19
|
+
// Format the expected values in a more readable way
|
|
20
|
+
const expectedDesc = uniqueExpected
|
|
21
|
+
.map(desc => `'${desc}'`)
|
|
22
|
+
.join(' or ');
|
|
23
|
+
|
|
24
|
+
const foundDesc = found === null ? 'end of input' : `'${found}'`;
|
|
25
|
+
|
|
26
|
+
generateError(
|
|
27
|
+
`Syntax error: expected ${expectedDesc} but found ${foundDesc}`,
|
|
28
|
+
location
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
function formatAttributes(attributes) {
|
|
10
33
|
if (attributes.length === 0) {
|
|
11
34
|
return null;
|
|
@@ -43,23 +66,27 @@ start
|
|
|
43
66
|
return `[${elements.join(',')}]`;
|
|
44
67
|
}
|
|
45
68
|
|
|
46
|
-
element
|
|
69
|
+
element "component or control structure"
|
|
47
70
|
= forLoop
|
|
48
71
|
/ ifCondition
|
|
49
72
|
/ selfClosingElement
|
|
50
73
|
/ openCloseElement
|
|
51
|
-
/
|
|
74
|
+
/ openUnclosedTag
|
|
75
|
+
/ comment
|
|
52
76
|
|
|
53
|
-
selfClosingElement
|
|
77
|
+
selfClosingElement "self-closing component tag"
|
|
54
78
|
= _ "<" _ tagName:tagName _ attributes:attributes _ "/>" _ {
|
|
55
79
|
const attrsString = formatAttributes(attributes);
|
|
56
80
|
return attrsString ? `h(${tagName}, ${attrsString})` : `h(${tagName})`;
|
|
57
81
|
}
|
|
58
82
|
|
|
59
|
-
openCloseElement
|
|
83
|
+
openCloseElement "component with content"
|
|
60
84
|
= "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ "</" _ closingTagName:tagName _ ">" _ {
|
|
61
85
|
if (tagName !== closingTagName) {
|
|
62
|
-
|
|
86
|
+
generateError(
|
|
87
|
+
`Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
|
|
88
|
+
location()
|
|
89
|
+
);
|
|
63
90
|
}
|
|
64
91
|
const attrsString = formatAttributes(attributes);
|
|
65
92
|
const children = content ? content : null;
|
|
@@ -74,35 +101,37 @@ openCloseElement
|
|
|
74
101
|
}
|
|
75
102
|
}
|
|
76
103
|
|
|
77
|
-
attributes
|
|
104
|
+
attributes "component attributes"
|
|
78
105
|
= attrs:(attribute (_ attribute)*)? {
|
|
79
106
|
return attrs
|
|
80
107
|
? [attrs[0]].concat(attrs[1].map(a => a[1]))
|
|
81
108
|
: [];
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
attribute
|
|
111
|
+
attribute "attribute"
|
|
85
112
|
= staticAttribute
|
|
86
113
|
/ dynamicAttribute
|
|
87
114
|
/ eventHandler
|
|
88
115
|
/ spreadAttribute
|
|
116
|
+
/ unclosedQuote
|
|
117
|
+
/ unclosedBrace
|
|
89
118
|
|
|
90
|
-
spreadAttribute
|
|
119
|
+
spreadAttribute "spread attribute"
|
|
91
120
|
= "..." expr:(functionCallExpr / dotNotation) {
|
|
92
121
|
return "..." + expr;
|
|
93
122
|
}
|
|
94
123
|
|
|
95
|
-
functionCallExpr
|
|
124
|
+
functionCallExpr "function call"
|
|
96
125
|
= name:dotNotation "(" args:functionArgs? ")" {
|
|
97
126
|
return `${name}(${args || ''})`;
|
|
98
127
|
}
|
|
99
128
|
|
|
100
|
-
dotNotation
|
|
129
|
+
dotNotation "property access"
|
|
101
130
|
= first:identifier rest:("." identifier)* {
|
|
102
131
|
return text();
|
|
103
132
|
}
|
|
104
133
|
|
|
105
|
-
eventHandler
|
|
134
|
+
eventHandler "event handler"
|
|
106
135
|
= "@" eventName:identifier _ "=" _ "{" _ handlerName:attributeValue _ "}" {
|
|
107
136
|
const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
|
|
108
137
|
const formattedName = needsQuotes ? `'${eventName}'` : eventName;
|
|
@@ -113,7 +142,7 @@ eventHandler
|
|
|
113
142
|
return needsQuotes ? `'${eventName}'` : eventName;
|
|
114
143
|
}
|
|
115
144
|
|
|
116
|
-
dynamicAttribute
|
|
145
|
+
dynamicAttribute "dynamic attribute"
|
|
117
146
|
= attributeName:attributeName _ "=" _ "{" _ attributeValue:attributeValue _ "}" {
|
|
118
147
|
// Check if attributeName needs to be quoted (contains dash or other invalid JS identifier chars)
|
|
119
148
|
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
@@ -149,7 +178,7 @@ dynamicAttribute
|
|
|
149
178
|
return needsQuotes ? `'${attributeName}'` : attributeName;
|
|
150
179
|
}
|
|
151
180
|
|
|
152
|
-
attributeValue
|
|
181
|
+
attributeValue "attribute value"
|
|
153
182
|
= element
|
|
154
183
|
/ functionWithElement
|
|
155
184
|
/ objectLiteral
|
|
@@ -161,7 +190,7 @@ attributeValue
|
|
|
161
190
|
return t
|
|
162
191
|
}
|
|
163
192
|
|
|
164
|
-
objectLiteral
|
|
193
|
+
objectLiteral "object literal"
|
|
165
194
|
= "{" _ objContent:objectContent _ "}" {
|
|
166
195
|
return `{ ${objContent} }`;
|
|
167
196
|
}
|
|
@@ -196,7 +225,7 @@ stringLiteral
|
|
|
196
225
|
= '"' chars:[^"]* '"' { return text(); }
|
|
197
226
|
/ "'" chars:[^']* "'" { return text(); }
|
|
198
227
|
|
|
199
|
-
functionWithElement
|
|
228
|
+
functionWithElement "function expression"
|
|
200
229
|
= "(" _ params:functionParams? _ ")" _ "=>" _ elem:element {
|
|
201
230
|
return `${params ? `(${params}) =>` : '() =>'} ${elem}`;
|
|
202
231
|
}
|
|
@@ -215,7 +244,7 @@ simpleParams
|
|
|
215
244
|
return [param].concat(rest.map(r => r[3])).join(', ');
|
|
216
245
|
}
|
|
217
246
|
|
|
218
|
-
staticAttribute
|
|
247
|
+
staticAttribute "static attribute"
|
|
219
248
|
= attributeName:attributeName _ "=" _ "\"" attributeValue:staticValue "\"" {
|
|
220
249
|
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
221
250
|
const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
|
|
@@ -233,7 +262,7 @@ staticValue
|
|
|
233
262
|
return `'${val}'`
|
|
234
263
|
}
|
|
235
264
|
|
|
236
|
-
content
|
|
265
|
+
content "component content"
|
|
237
266
|
= elements:(element)* {
|
|
238
267
|
const filteredElements = elements.filter(el => el !== null);
|
|
239
268
|
if (filteredElements.length === 0) return null;
|
|
@@ -253,27 +282,27 @@ textElement
|
|
|
253
282
|
return trimmed ? JSON.stringify(trimmed) : null;
|
|
254
283
|
}
|
|
255
284
|
|
|
256
|
-
forLoop
|
|
257
|
-
= _ "@for" _ "(" _ variableName:(tupleDestructuring / identifier) _ "of" _ iterable:
|
|
285
|
+
forLoop "for loop"
|
|
286
|
+
= _ "@for" _ "(" _ variableName:(tupleDestructuring / identifier) _ "of" _ iterable:iterable _ ")" _ "{" _ content:content _ "}" _ {
|
|
258
287
|
return `loop(${iterable}, ${variableName} => ${content})`;
|
|
259
288
|
}
|
|
260
289
|
|
|
261
|
-
tupleDestructuring
|
|
290
|
+
tupleDestructuring "destructuring pattern"
|
|
262
291
|
= "(" _ first:identifier _ "," _ second:identifier _ ")" {
|
|
263
292
|
return `(${first}, ${second})`;
|
|
264
293
|
}
|
|
265
294
|
|
|
266
|
-
ifCondition
|
|
295
|
+
ifCondition "if condition"
|
|
267
296
|
= _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
|
|
268
297
|
return `cond(${condition}, () => ${content})`;
|
|
269
298
|
}
|
|
270
299
|
|
|
271
|
-
tagName
|
|
300
|
+
tagName "tag name"
|
|
272
301
|
= segments:([a-zA-Z][a-zA-Z0-9]* ("." [a-zA-Z][a-zA-Z0-9]*)*) {
|
|
273
302
|
return text();
|
|
274
303
|
}
|
|
275
304
|
|
|
276
|
-
attributeName
|
|
305
|
+
attributeName "attribute name"
|
|
277
306
|
= [a-zA-Z][a-zA-Z0-9-]* { return text(); }
|
|
278
307
|
|
|
279
308
|
eventName
|
|
@@ -282,14 +311,49 @@ eventName
|
|
|
282
311
|
variableName
|
|
283
312
|
= [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
|
|
284
313
|
|
|
285
|
-
iterable
|
|
286
|
-
=
|
|
314
|
+
iterable "iterable expression"
|
|
315
|
+
= id:identifier "(" _ args:functionArgs? _ ")" { // Direct function call
|
|
316
|
+
return `${id}(${args || ''})`;
|
|
317
|
+
}
|
|
318
|
+
/ first:identifier "." rest:dotFunctionChain { // Dot notation possibly with function call
|
|
319
|
+
return `${first}.${rest}`;
|
|
320
|
+
}
|
|
321
|
+
/ id:identifier { return id; }
|
|
322
|
+
|
|
323
|
+
dotFunctionChain
|
|
324
|
+
= segment:identifier "(" _ args:functionArgs? _ ")" rest:("." dotFunctionChain)? {
|
|
325
|
+
const restStr = rest ? `.${rest[1]}` : '';
|
|
326
|
+
return `${segment}(${args || ''})${restStr}`;
|
|
327
|
+
}
|
|
328
|
+
/ segment:identifier rest:("." dotFunctionChain)? {
|
|
329
|
+
const restStr = rest ? `.${rest[1]}` : '';
|
|
330
|
+
return `${segment}${restStr}`;
|
|
331
|
+
}
|
|
287
332
|
|
|
288
|
-
condition
|
|
333
|
+
condition "condition expression"
|
|
289
334
|
= functionCall
|
|
290
|
-
/
|
|
335
|
+
/ text_condition:$([^)]*) {
|
|
336
|
+
const originalText = text_condition.trim();
|
|
337
|
+
|
|
338
|
+
// Transform simple identifiers to function calls like "foo" to "foo()"
|
|
339
|
+
// This regex matches identifiers not followed by an opening parenthesis.
|
|
340
|
+
// This transformation should only apply if we are wrapping in 'computed'.
|
|
341
|
+
if (originalText.includes('!') || originalText.includes('&&') || originalText.includes('||')) {
|
|
342
|
+
const transformedText = originalText.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match) => {
|
|
343
|
+
// Do not transform keywords (true, false, null) or numeric literals
|
|
344
|
+
if (['true', 'false', 'null'].includes(match) || /^\d+(\.\d+)?$/.test(match)) {
|
|
345
|
+
return match;
|
|
346
|
+
}
|
|
347
|
+
return `${match}()`;
|
|
348
|
+
});
|
|
349
|
+
return `computed(() => ${transformedText})`;
|
|
350
|
+
}
|
|
351
|
+
// For simple conditions (no !, &&, ||), return the original text as is.
|
|
352
|
+
// Cases like `myFunction()` are handled by the `functionCall` rule.
|
|
353
|
+
return originalText;
|
|
354
|
+
}
|
|
291
355
|
|
|
292
|
-
functionCall
|
|
356
|
+
functionCall "function call"
|
|
293
357
|
= name:identifier "(" args:functionArgs? ")" {
|
|
294
358
|
return `${name}(${args || ''})`;
|
|
295
359
|
}
|
|
@@ -328,4 +392,31 @@ comment
|
|
|
328
392
|
singleComment
|
|
329
393
|
= "<!--" _ content:((!("-->") .)* "-->") _ {
|
|
330
394
|
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Add a special error detection rule for unclosed tags
|
|
398
|
+
openUnclosedTag "unclosed tag"
|
|
399
|
+
= "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ !("</" _ closingTagName:tagName _ ">") {
|
|
400
|
+
generateError(
|
|
401
|
+
`Unclosed tag: <${tagName}> is missing its closing tag`,
|
|
402
|
+
location()
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Add error detection for unclosed quotes in static attributes
|
|
407
|
+
unclosedQuote "unclosed string"
|
|
408
|
+
= attributeName:attributeName _ "=" _ "\"" [^"]* !("\"") {
|
|
409
|
+
generateError(
|
|
410
|
+
`Missing closing quote in attribute '${attributeName}'`,
|
|
411
|
+
location()
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Add error detection for unclosed braces in dynamic attributes
|
|
416
|
+
unclosedBrace "unclosed brace"
|
|
417
|
+
= attributeName:attributeName _ "=" _ "{" !("}" / _ "}") [^{}]* {
|
|
418
|
+
generateError(
|
|
419
|
+
`Missing closing brace in dynamic attribute '${attributeName}'`,
|
|
420
|
+
location()
|
|
421
|
+
);
|
|
331
422
|
}
|
package/index.ts
CHANGED
|
@@ -10,6 +10,35 @@ const { generate } = pkg;
|
|
|
10
10
|
|
|
11
11
|
const DEV_SRC = "../../src"
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Formats a syntax error message with visual pointer to the error location
|
|
15
|
+
*
|
|
16
|
+
* @param {string} template - The template content that failed to parse
|
|
17
|
+
* @param {object} error - The error object with location information
|
|
18
|
+
* @returns {string} - Formatted error message with a visual pointer
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```
|
|
22
|
+
* const errorMessage = showErrorMessage("<Canvas>test(d)</Canvas>", syntaxError);
|
|
23
|
+
* // Returns a formatted error message with an arrow pointing to 'd'
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function showErrorMessage(template: string, error: any): string {
|
|
27
|
+
if (!error.location) {
|
|
28
|
+
return `Syntax error: ${error.message}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const lines = template.split('\n');
|
|
32
|
+
const { line, column } = error.location.start;
|
|
33
|
+
const errorLine = lines[line - 1] || '';
|
|
34
|
+
|
|
35
|
+
// Create a visual pointer with an arrow
|
|
36
|
+
const pointer = ' '.repeat(column - 1) + '^';
|
|
37
|
+
|
|
38
|
+
return `Syntax error at line ${line}, column ${column}: ${error.message}\n\n` +
|
|
39
|
+
`${errorLine}\n${pointer}\n`;
|
|
40
|
+
}
|
|
41
|
+
|
|
13
42
|
export default function canvasengine() {
|
|
14
43
|
const filter = createFilter("**/*.ce");
|
|
15
44
|
|
|
@@ -59,8 +88,14 @@ export default function canvasengine() {
|
|
|
59
88
|
template = template.replace(/<svg>([\s\S]*?)<\/svg>/g, (match, content) => {
|
|
60
89
|
return `<Svg content="${content.trim()}" />`;
|
|
61
90
|
});
|
|
62
|
-
|
|
63
|
-
|
|
91
|
+
|
|
92
|
+
let parsedTemplate;
|
|
93
|
+
try {
|
|
94
|
+
parsedTemplate = parser.parse(template);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const errorMsg = showErrorMessage(template, error);
|
|
97
|
+
throw new Error(`Error parsing template in file ${id}:\n${errorMsg}`);
|
|
98
|
+
}
|
|
64
99
|
|
|
65
100
|
// trick to avoid typescript remove imports in scriptContent
|
|
66
101
|
scriptContent += FLAG_COMMENT + parsedTemplate
|
package/package.json
CHANGED
package/tests/compiler.spec.ts
CHANGED
|
@@ -316,6 +316,66 @@ describe("Loop", () => {
|
|
|
316
316
|
);
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
+
test("should compile loop with object", () => {
|
|
320
|
+
const input = `
|
|
321
|
+
@for (sprite of sprites.items) {
|
|
322
|
+
<Sprite />
|
|
323
|
+
}
|
|
324
|
+
`;
|
|
325
|
+
const output = parser.parse(input);
|
|
326
|
+
expect(output.replace(/\s+/g, "")).toBe(
|
|
327
|
+
`loop(sprites.items,sprite=>h(Sprite))`.replace(/\s+/g, "")
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("should compile loop with deep object", () => {
|
|
332
|
+
const input = `
|
|
333
|
+
@for (sprite of sprites.items.items) {
|
|
334
|
+
<Sprite />
|
|
335
|
+
}
|
|
336
|
+
`;
|
|
337
|
+
const output = parser.parse(input);
|
|
338
|
+
expect(output.replace(/\s+/g, "")).toBe(
|
|
339
|
+
`loop(sprites.items.items,sprite=>h(Sprite))`.replace(/\s+/g, "")
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("should compile loop with function", () => {
|
|
344
|
+
const input = `
|
|
345
|
+
@for (sprite of sprites()) {
|
|
346
|
+
<Sprite />
|
|
347
|
+
}
|
|
348
|
+
`;
|
|
349
|
+
const output = parser.parse(input);
|
|
350
|
+
expect(output.replace(/\s+/g, "")).toBe(
|
|
351
|
+
`loop(sprites(),sprite=>h(Sprite))`.replace(/\s+/g, "")
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("should compile loop with function and params", () => {
|
|
356
|
+
const input = `
|
|
357
|
+
@for (sprite of sprites(x, y)) {
|
|
358
|
+
<Sprite />
|
|
359
|
+
}
|
|
360
|
+
`;
|
|
361
|
+
const output = parser.parse(input);
|
|
362
|
+
expect(output.replace(/\s+/g, "")).toBe(
|
|
363
|
+
`loop(sprites(x,y),sprite=>h(Sprite))`.replace(/\s+/g, "")
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("should compile loop with object and function and params", () => {
|
|
368
|
+
const input = `
|
|
369
|
+
@for (sprite of sprites.items(x, y)) {
|
|
370
|
+
<Sprite />
|
|
371
|
+
}
|
|
372
|
+
`;
|
|
373
|
+
const output = parser.parse(input);
|
|
374
|
+
expect(output.replace(/\s+/g, "")).toBe(
|
|
375
|
+
`loop(sprites.items(x,y),sprite=>h(Sprite))`.replace(/\s+/g, "")
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
319
379
|
test("should compile loop with destructuring", () => {
|
|
320
380
|
const input = `
|
|
321
381
|
@for ((sprite, index) of sprites) {
|
|
@@ -347,6 +407,46 @@ describe("Loop", () => {
|
|
|
347
407
|
});
|
|
348
408
|
|
|
349
409
|
describe("Condition", () => {
|
|
410
|
+
test("should compile condition", () => {
|
|
411
|
+
const input = `
|
|
412
|
+
@if (sprite) {
|
|
413
|
+
<Sprite />
|
|
414
|
+
}
|
|
415
|
+
`;
|
|
416
|
+
const output = parser.parse(input);
|
|
417
|
+
expect(output).toBe(`cond(sprite, () => h(Sprite))`);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("should compile negative condition", () => {
|
|
421
|
+
const input = `
|
|
422
|
+
@if (!sprite) {
|
|
423
|
+
<Sprite />
|
|
424
|
+
}
|
|
425
|
+
`;
|
|
426
|
+
const output = parser.parse(input);
|
|
427
|
+
expect(output).toBe(`cond(computed(() => !sprite()), () => h(Sprite))`);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("should compile negative condition with multiple condition", () => {
|
|
431
|
+
const input = `
|
|
432
|
+
@if (!sprite && other) {
|
|
433
|
+
<Sprite />
|
|
434
|
+
}
|
|
435
|
+
`;
|
|
436
|
+
const output = parser.parse(input);
|
|
437
|
+
expect(output).toBe(`cond(computed(() => !sprite() && other()), () => h(Sprite))`);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("should compile negative condition with multiple condition (or)", () => {
|
|
441
|
+
const input = `
|
|
442
|
+
@if (!sprite || other) {
|
|
443
|
+
<Sprite />
|
|
444
|
+
}
|
|
445
|
+
`;
|
|
446
|
+
const output = parser.parse(input);
|
|
447
|
+
expect(output).toBe(`cond(computed(() => !sprite() || other()), () => h(Sprite))`);
|
|
448
|
+
});
|
|
449
|
+
|
|
350
450
|
test("should compile condition when sprite is visible", () => {
|
|
351
451
|
const input = `
|
|
352
452
|
@if (sprite.visible) {
|