@canvasengine/compiler 2.0.0-beta.16 → 2.0.0-beta.18

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 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
- const parsedTemplate = parser.parse(template);
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 \n const parsedTemplate = parser.parse(template);\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":[]}
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
- / comment
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
- error("Mismatched opening and closing tags");
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
285
+ forLoop "for loop"
257
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,7 +311,7 @@ eventName
282
311
  variableName
283
312
  = [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
284
313
 
285
- iterable
314
+ iterable "iterable expression"
286
315
  = id:identifier "(" _ args:functionArgs? _ ")" { // Direct function call
287
316
  return `${id}(${args || ''})`;
288
317
  }
@@ -301,11 +330,30 @@ dotFunctionChain
301
330
  return `${segment}${restStr}`;
302
331
  }
303
332
 
304
- condition
333
+ condition "condition expression"
305
334
  = functionCall
306
- / $([^)]*) { return text().trim(); }
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
+ }
307
355
 
308
- functionCall
356
+ functionCall "function call"
309
357
  = name:identifier "(" args:functionArgs? ")" {
310
358
  return `${name}(${args || ''})`;
311
359
  }
@@ -344,4 +392,31 @@ comment
344
392
  singleComment
345
393
  = "<!--" _ content:((!("-->") .)* "-->") _ {
346
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
+ );
347
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
- const parsedTemplate = parser.parse(template);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canvasengine/compiler",
3
- "version": "2.0.0-beta.16",
3
+ "version": "2.0.0-beta.18",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -407,6 +407,46 @@ describe("Loop", () => {
407
407
  });
408
408
 
409
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
+
410
450
  test("should compile condition when sprite is visible", () => {
411
451
  const input = `
412
452
  @if (sprite.visible) {