@canvasengine/compiler 2.0.0-beta.26 → 2.0.0-beta.28

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
@@ -66,7 +66,8 @@ function canvasengine() {
66
66
  "Mesh",
67
67
  "Svg",
68
68
  "DOMContainer",
69
- "DOMElement"
69
+ "DOMElement",
70
+ "Button"
70
71
  ];
71
72
  return {
72
73
  name: "vite-plugin-ce",
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\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\n/**\n * Vite plugin to load shader files (.frag, .vert, .wgsl) as text strings\n * \n * This plugin allows importing shader files directly as string literals in your code.\n * It supports fragment shaders (.frag), vertex shaders (.vert), and WebGPU shaders (.wgsl).\n * The content is loaded as a raw string and can be used directly with graphics APIs.\n * \n * @returns {object} - Vite plugin configuration object\n * \n * @example\n * ```typescript\n * // In your vite.config.ts\n * import { shaderLoader } from './path/to/compiler'\n * \n * export default defineConfig({\n * plugins: [shaderLoader()]\n * })\n * \n * // In your code\n * import fragmentShader from './shader.frag'\n * import vertexShader from './shader.vert'\n * import computeShader from './shader.wgsl'\n * \n * console.log(fragmentShader) // Raw shader code as string\n * ```\n */\nexport function shaderLoader() {\n const filter = createFilter(/\\.(frag|vert|wgsl)$/);\n\n return {\n name: \"vite-plugin-shader-loader\",\n transform(code: string, id: string) {\n if (!filter(id)) return;\n\n // Escape the shader code to be safely embedded in a JavaScript string\n const escapedCode = code\n .replace(/\\\\/g, '\\\\\\\\') // Escape backslashes\n .replace(/`/g, '\\\\`') // Escape backticks\n .replace(/\\$/g, '\\\\$'); // Escape dollar signs\n\n // Return the shader content as a default export string\n return {\n code: `export default \\`${escapedCode}\\`;`,\n map: null,\n };\n },\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 \"Mesh\",\n \"Svg\",\n \"DOMContainer\",\n \"DOMElement\"\n ];\n\n return {\n name: \"vite-plugin-ce\",\n transform(code: string, id: string) {\n if (!filter(id)) return null;\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 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":[]}
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\n/**\n * Vite plugin to load shader files (.frag, .vert, .wgsl) as text strings\n * \n * This plugin allows importing shader files directly as string literals in your code.\n * It supports fragment shaders (.frag), vertex shaders (.vert), and WebGPU shaders (.wgsl).\n * The content is loaded as a raw string and can be used directly with graphics APIs.\n * \n * @returns {object} - Vite plugin configuration object\n * \n * @example\n * ```typescript\n * // In your vite.config.ts\n * import { shaderLoader } from './path/to/compiler'\n * \n * export default defineConfig({\n * plugins: [shaderLoader()]\n * })\n * \n * // In your code\n * import fragmentShader from './shader.frag'\n * import vertexShader from './shader.vert'\n * import computeShader from './shader.wgsl'\n * \n * console.log(fragmentShader) // Raw shader code as string\n * ```\n */\nexport function shaderLoader() {\n const filter = createFilter(/\\.(frag|vert|wgsl)$/);\n\n return {\n name: \"vite-plugin-shader-loader\",\n transform(code: string, id: string) {\n if (!filter(id)) return;\n\n // Escape the shader code to be safely embedded in a JavaScript string\n const escapedCode = code\n .replace(/\\\\/g, '\\\\\\\\') // Escape backslashes\n .replace(/`/g, '\\\\`') // Escape backticks\n .replace(/\\$/g, '\\\\$'); // Escape dollar signs\n\n // Return the shader content as a default export string\n return {\n code: `export default \\`${escapedCode}\\`;`,\n map: null,\n };\n },\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 \"Mesh\",\n \"Svg\",\n \"DOMContainer\",\n \"DOMElement\",\n \"Button\"\n ];\n\n return {\n name: \"vite-plugin-ce\",\n transform(code: string, id: string) {\n if (!filter(id)) return null;\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 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
@@ -36,7 +36,7 @@
36
36
 
37
37
  // Framework components that should NOT be transformed to DOM elements
38
38
  const frameworkComponents = new Set([
39
- 'Canvas', 'Container', 'Sprite', 'Text', 'DOMElement', 'Svg'
39
+ 'Canvas', 'Container', 'Sprite', 'Text', 'DOMElement', 'Svg', 'Button'
40
40
  ]);
41
41
 
42
42
  // DisplayObject special attributes that should not be in attrs
@@ -248,8 +248,49 @@ simpleTextPart "simple text part"
248
248
  }
249
249
 
250
250
  simpleDynamicPart "simple dynamic part"
251
- = "{" _ expr:attributeValue _ "}" {
252
- // Handle dynamic expressions like {item.name} or {@text}
251
+ = "{{" _ expr:attributeValue _ "}}" {
252
+ // Handle double brace expressions like {{ object.x }} or {{ @object.x }} or {{ @object.@x }}
253
+ if (expr.trim().match(/^(@?[a-zA-Z_][a-zA-Z0-9_]*)(\.@?[a-zA-Z_][a-zA-Z0-9_]*)*$/)) {
254
+ let foundSignal = false;
255
+ let hasLiterals = false;
256
+
257
+ // Split by dots to handle each part separately
258
+ const parts = expr.split('.');
259
+ const allLiterals = parts.every(part => part.trim().startsWith('@'));
260
+
261
+ let computedValue;
262
+
263
+ if (allLiterals) {
264
+ // All parts are literals, just remove @ prefixes
265
+ computedValue = parts.map(part => part.replace('@', '')).join('.');
266
+ hasLiterals = true;
267
+ } else {
268
+ // Transform each part individually
269
+ computedValue = parts.map(part => {
270
+ const trimmedPart = part.trim();
271
+ if (trimmedPart.startsWith('@')) {
272
+ hasLiterals = true;
273
+ return trimmedPart.substring(1); // Remove @ prefix for literals
274
+ } else {
275
+ // Don't transform keywords
276
+ if (['true', 'false', 'null'].includes(trimmedPart)) {
277
+ return trimmedPart;
278
+ }
279
+ foundSignal = true;
280
+ return `${trimmedPart}()`;
281
+ }
282
+ }).join('.');
283
+ }
284
+
285
+ if (foundSignal && !allLiterals) {
286
+ return `computed(() => ${computedValue})`;
287
+ }
288
+ return computedValue;
289
+ }
290
+ return expr;
291
+ }
292
+ / "{" _ expr:attributeValue _ "}" {
293
+ // Handle single brace expressions like {item.name} or {@text}
253
294
  if (expr.trim().match(/^@?[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
254
295
  let foundSignal = false;
255
296
  const computedValue = expr.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => {
@@ -396,14 +437,50 @@ dynamicAttribute "dynamic attribute"
396
437
  const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
397
438
  const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
398
439
 
399
- // If it's a complex object literal starting with curly braces, preserve it as is
400
- if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) {
401
- return `${formattedName}: ${attributeValue}`;
402
- }
403
440
 
404
- // If it's a template string, preserve it as is
441
+ // If it's a complex object with strings, preserve it as is
442
+ if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}') &&
443
+ (attributeValue.includes('"') || attributeValue.includes("'"))) {
444
+ return `${formattedName}: ${attributeValue}`;
445
+ }
446
+
447
+ // If it's a template string, transform expressions inside ${}
405
448
  if (attributeValue.trim().startsWith('`') && attributeValue.trim().endsWith('`')) {
406
- return `${formattedName}: ${attributeValue}`;
449
+ // Transform expressions inside ${} in template strings
450
+ let transformedTemplate = attributeValue;
451
+
452
+ // Find and replace ${expression} patterns
453
+ let startIndex = 0;
454
+ while (true) {
455
+ const dollarIndex = transformedTemplate.indexOf('${', startIndex);
456
+ if (dollarIndex === -1) break;
457
+
458
+ const braceIndex = transformedTemplate.indexOf('}', dollarIndex);
459
+ if (braceIndex === -1) break;
460
+
461
+ const expr = transformedTemplate.substring(dollarIndex + 2, braceIndex);
462
+ const trimmedExpr = expr.trim();
463
+
464
+ let replacement;
465
+ if (trimmedExpr.startsWith('@')) {
466
+ // Remove @ prefix for literals
467
+ replacement = '${' + trimmedExpr.substring(1) + '}';
468
+ } else if (trimmedExpr.match(/^[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
469
+ // Transform identifiers to signals
470
+ replacement = '${' + trimmedExpr + '()}';
471
+ } else {
472
+ // Keep as is for complex expressions
473
+ replacement = '${' + expr + '}';
474
+ }
475
+
476
+ transformedTemplate = transformedTemplate.substring(0, dollarIndex) +
477
+ replacement +
478
+ transformedTemplate.substring(braceIndex + 1);
479
+
480
+ startIndex = dollarIndex + replacement.length;
481
+ }
482
+
483
+ return formattedName + ': ' + transformedTemplate;
407
484
  }
408
485
 
409
486
  // Handle other types of values
@@ -412,17 +489,68 @@ dynamicAttribute "dynamic attribute"
412
489
  } else if (attributeValue.trim().match(/^[a-zA-Z_]\w*$/)) {
413
490
  return `${formattedName}: ${attributeValue}`;
414
491
  } else {
492
+ // Check if this is an object or array literal
493
+ const isObjectLiteral = attributeValue.trim().startsWith('{ ') && attributeValue.trim().endsWith(' }');
494
+ const isArrayLiteral = attributeValue.trim().startsWith('[') && attributeValue.trim().endsWith(']');
495
+
415
496
  let foundSignal = false;
416
- const computedValue = attributeValue.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => {
417
- if (match.startsWith('@')) {
418
- return match.substring(1);
497
+ let hasLiterals = false;
498
+ let computedValue = attributeValue;
499
+
500
+ // For simple object and array literals (like {x: x, y: 20} or [x, 20]),
501
+ // don't use computed() at all and don't transform identifiers
502
+ if ((isObjectLiteral || isArrayLiteral) && !attributeValue.includes('()')) {
503
+ // Don't transform anything, return as is
504
+ foundSignal = false;
505
+ computedValue = attributeValue;
506
+ } else {
507
+ // Apply signal transformation for other values
508
+ computedValue = attributeValue.replace(/@?([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*:)/g, (match, p1, offset) => {
509
+ // Don't transform keywords, numbers, or if we're inside quotes
510
+ if (['true', 'false', 'null'].includes(p1) || /^\d+(\.\d+)?$/.test(p1)) {
511
+ return match;
512
+ }
513
+
514
+ // Check if we're inside a string literal
515
+ const beforeMatch = attributeValue.substring(0, offset);
516
+ const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
517
+ const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
518
+
519
+ // If we're inside quotes, don't transform
520
+ if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
521
+ return match;
522
+ }
523
+
524
+ if (match.startsWith('@')) {
525
+ hasLiterals = true;
526
+ return p1; // Remove @ prefix
527
+ }
528
+ foundSignal = true;
529
+ return `${p1}()`;
530
+ });
531
+
532
+ // Check if any values already contain signals (ending with ())
533
+ if (attributeValue.includes('()')) {
534
+ foundSignal = true;
419
535
  }
420
- foundSignal = true;
421
- return `${match}()`;
422
- });
536
+ }
537
+
423
538
  if (foundSignal) {
539
+ // For objects, wrap in parentheses
540
+ if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) {
541
+ // Remove spaces for objects in parentheses
542
+ const cleanedObject = computedValue.replace(/{ /g, '{').replace(/ }/g, '}');
543
+ return `${formattedName}: computed(() => (${cleanedObject}))`;
544
+ }
424
545
  return `${formattedName}: computed(() => ${computedValue})`;
425
546
  }
547
+
548
+ // If only literals (all @), don't use computed
549
+ if (hasLiterals && !foundSignal) {
550
+ return `${formattedName}: ${computedValue}`;
551
+ }
552
+
553
+ // For static objects and arrays, return as is without parentheses
426
554
  return `${formattedName}: ${computedValue}`;
427
555
  }
428
556
  }
@@ -436,11 +564,7 @@ attributeValue "attribute value"
436
564
  / functionWithElement
437
565
  / objectLiteral
438
566
  / $([^{}]* ("{" [^{}]* "}" [^{}]*)*) {
439
- const t = text().trim()
440
- if (t.startsWith("{") && t.endsWith("}")) {
441
- return `(${t})`;
442
- }
443
- return t
567
+ return text().trim()
444
568
  }
445
569
 
446
570
  objectLiteral "object literal"
@@ -467,6 +591,7 @@ propertyValue
467
591
  / element
468
592
  / functionWithElement
469
593
  / stringLiteral
594
+ / number
470
595
  / identifier
471
596
 
472
597
  nestedObject
@@ -548,8 +673,31 @@ tupleDestructuring "destructuring pattern"
548
673
  }
549
674
 
550
675
  ifCondition "if condition"
551
- = _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
552
- return `cond(${condition}, () => ${content})`;
676
+ = _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ elseIfs:elseIfClause* elseClause:elseClause? _ {
677
+ let result = `cond(${condition}, () => ${content}`;
678
+
679
+ // Add else if clauses
680
+ elseIfs.forEach(elseIf => {
681
+ result += `, [${elseIf.condition}, () => ${elseIf.content}]`;
682
+ });
683
+
684
+ // Add else clause if present
685
+ if (elseClause) {
686
+ result += `, () => ${elseClause}`;
687
+ }
688
+
689
+ result += ')';
690
+ return result;
691
+ }
692
+
693
+ elseIfClause "else if clause"
694
+ = _ "@else" _ "if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
695
+ return { condition, content };
696
+ }
697
+
698
+ elseClause "else clause"
699
+ = _ "@else" _ "{" _ content:content _ "}" _ {
700
+ return content;
553
701
  }
554
702
 
555
703
  tagName "tag name"
@@ -601,12 +749,26 @@ condition "condition expression"
601
749
  // Transform simple identifiers to function calls like "foo" to "foo()"
602
750
  // This regex matches identifiers not followed by an opening parenthesis.
603
751
  // This transformation should only apply if we are wrapping in 'computed'.
604
- if (originalText.includes('!') || originalText.includes('&&') || originalText.includes('||')) {
605
- const transformedText = originalText.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match) => {
752
+ if (originalText.includes('!') || originalText.includes('&&') || originalText.includes('||') ||
753
+ originalText.includes('>=') || originalText.includes('<=') || originalText.includes('===') ||
754
+ originalText.includes('!==') || originalText.includes('==') || originalText.includes('!=') ||
755
+ originalText.includes('>') || originalText.includes('<')) {
756
+ const transformedText = originalText.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match, p1, offset) => {
606
757
  // Do not transform keywords (true, false, null) or numeric literals
607
758
  if (['true', 'false', 'null'].includes(match) || /^\d+(\.\d+)?$/.test(match)) {
608
759
  return match;
609
760
  }
761
+ // Check if the match is inside quotes
762
+ const beforeMatch = originalText.substring(0, offset);
763
+ const afterMatch = originalText.substring(offset + match.length);
764
+ const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
765
+ const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
766
+
767
+ // If we're inside quotes, don't transform
768
+ if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
769
+ return match;
770
+ }
771
+
610
772
  return `${match}()`;
611
773
  });
612
774
  return `computed(() => ${transformedText})`;
package/index.ts CHANGED
@@ -121,7 +121,8 @@ export default function canvasengine() {
121
121
  "Mesh",
122
122
  "Svg",
123
123
  "DOMContainer",
124
- "DOMElement"
124
+ "DOMElement",
125
+ "Button"
125
126
  ];
126
127
 
127
128
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canvasengine/compiler",
3
- "version": "2.0.0-beta.26",
3
+ "version": "2.0.0-beta.28",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -131,23 +131,178 @@ describe("Compiler", () => {
131
131
  expect(output).toBe(`h(Canvas, { width: 20 })`);
132
132
  });
133
133
 
134
- // TODO
135
- // test("should compile component with templating string", () => {
136
- // const input = `<Canvas width={\`direction: \${direction}\`} />`;
137
- // const output = parser.parse(input);
138
- // expect(output).toBe(`h(Canvas, { width: \`direction: \${direction()}\` })`);
139
- // });
140
-
141
- // test("should compile component with templating string with @ (literal)", () => {
142
- // const input = `<Canvas width={\`direction: \${@direction}\`} />`;
143
- // const output = parser.parse(input);
144
- // expect(output).toBe(`h(Canvas, { width: \`direction: \${direction}\` })`);
145
- // });
134
+ test("should compile if/else condition", () => {
135
+ const input = `
136
+ @if (sprite) {
137
+ <Sprite />
138
+ }
139
+ @else {
140
+ <Text text="No sprite" />
141
+ }
142
+ `;
143
+ const output = parser.parse(input);
144
+ expect(output).toBe(`cond(sprite, () => h(Sprite), () => h(Text, { text: 'No sprite' }))`);
145
+ });
146
+
147
+ test("should compile if/else if condition", () => {
148
+ const input = `
149
+ @if (score >= 90) {
150
+ <Text text="A+" />
151
+ }
152
+ @else if (score >= 80) {
153
+ <Text text="A" />
154
+ }
155
+ `;
156
+ const output = parser.parse(input);
157
+ expect(output).toBe(`cond(computed(() => score() >= 90), () => h(Text, { text: 'A+' }), [computed(() => score() >= 80), () => h(Text, { text: 'A' })])`);
158
+ });
159
+
160
+ test("should compile if/else if/else condition", () => {
161
+ const input = `
162
+ @if (score >= 90) {
163
+ <Text text="A+" />
164
+ }
165
+ @else if (score >= 80) {
166
+ <Text text="A" />
167
+ }
168
+ @else {
169
+ <Text text="F" />
170
+ }
171
+ `;
172
+ const output = parser.parse(input);
173
+ expect(output).toBe(`cond(computed(() => score() >= 90), () => h(Text, { text: 'A+' }), [computed(() => score() >= 80), () => h(Text, { text: 'A' })], () => h(Text, { text: 'F' }))`);
174
+ });
175
+
176
+ test("should compile if/else if/else condition within canvas", () => {
177
+ const input = `<Canvas>
178
+ <Container>
179
+ @if (score >= 90) {
180
+ <Text text="Grade: A+" x={100} y={100} color="gold" size={24} />
181
+ <Text text="Excellent work!" x={100} y={130} color="gold" size={16} />
182
+ }
183
+ </Container>
184
+ </Canvas>
185
+ `;
186
+
187
+ const output = parser.parse(input);
188
+ expect(output).toBe(`h(Canvas, null, h(Container, null, cond(computed(() => score() >= 90), () => [h(Text, { text: 'Grade: A+', x: 100, y: 100, color: 'gold', size: 24 }), h(Text, { text: 'Excellent work!', x: 100, y: 130, color: 'gold', size: 16 })])))`);
189
+ });
190
+
191
+ test("should compile multiple else if conditions", () => {
192
+ const input = `
193
+ @if (score >= 90) {
194
+ <Text text="A+" />
195
+ }
196
+ @else if (score >= 80) {
197
+ <Text text="A" />
198
+ }
199
+ @else if (score >= 70) {
200
+ <Text text="B" />
201
+ }
202
+ @else if (score >= 60) {
203
+ <Text text="C" />
204
+ }
205
+ @else {
206
+ <Text text="F" />
207
+ }
208
+ `;
209
+ const output = parser.parse(input);
210
+ expect(output).toBe(`cond(computed(() => score() >= 90), () => h(Text, { text: 'A+' }), [computed(() => score() >= 80), () => h(Text, { text: 'A' })], [computed(() => score() >= 70), () => h(Text, { text: 'B' })], [computed(() => score() >= 60), () => h(Text, { text: 'C' })], () => h(Text, { text: 'F' }))`);
211
+ });
212
+
213
+ test("should compile if/else with multiple elements", () => {
214
+ const input = `
215
+ @if (user.role === 'admin') {
216
+ <Text text="Admin Panel" />
217
+ <Text text="Settings" />
218
+ }
219
+ @else {
220
+ <Text text="Please log in" />
221
+ }
222
+ `;
223
+ const output = parser.parse(input);
224
+ expect(output).toBe(`cond(computed(() => user().role() === 'admin'), () => [h(Text, { text: 'Admin Panel' }), h(Text, { text: 'Settings' })], () => h(Text, { text: 'Please log in' }))`);
225
+ });
226
+
227
+ test("should compile nested if/else conditions", () => {
228
+ const input = `
229
+ @if (user) {
230
+ @if (user.isActive) {
231
+ <Text text="Active user" />
232
+ }
233
+ @else {
234
+ <Text text="Inactive user" />
235
+ }
236
+ }
237
+ @else {
238
+ <Text text="No user" />
239
+ }
240
+ `;
241
+ const output = parser.parse(input);
242
+ expect(output).toBe(`cond(user, () => cond(user.isActive, () => h(Text, { text: 'Active user' }), () => h(Text, { text: 'Inactive user' })), () => h(Text, { text: 'No user' }))`);
243
+ });
244
+
245
+ test("should compile if/else if with simple conditions", () => {
246
+ const input = `
247
+ @if (theme === 'dark') {
248
+ <Text text="Dark mode" />
249
+ }
250
+ @else if (theme === 'light') {
251
+ <Text text="Light mode" />
252
+ }
253
+ @else {
254
+ <Text text="Auto mode" />
255
+ }
256
+ `;
257
+ const output = parser.parse(input);
258
+ expect(output).toBe(`cond(computed(() => theme() === 'dark'), () => h(Text, { text: 'Dark mode' }), [computed(() => theme() === 'light'), () => h(Text, { text: 'Light mode' })], () => h(Text, { text: 'Auto mode' }))`);
259
+ });
260
+
261
+ test("should compile if/else with function conditions", () => {
262
+ const input = `
263
+ @if (isVisible()) {
264
+ <Sprite />
265
+ }
266
+ @else {
267
+ <Text text="Hidden" />
268
+ }
269
+ `;
270
+ const output = parser.parse(input);
271
+ expect(output).toBe(`cond(isVisible(), () => h(Sprite), () => h(Text, { text: 'Hidden' }))`);
272
+ });
273
+
274
+ test("should compile if/else if with object property conditions", () => {
275
+ const input = `
276
+ @if (sprite.visible) {
277
+ <Sprite />
278
+ }
279
+ @else if (sprite.loading) {
280
+ <Text text="Loading..." />
281
+ }
282
+ @else {
283
+ <Text text="Not available" />
284
+ }
285
+ `;
286
+ const output = parser.parse(input);
287
+ expect(output).toBe(`cond(sprite.visible, () => h(Sprite), [sprite.loading, () => h(Text, { text: 'Loading...' })], () => h(Text, { text: 'Not available' }))`);
288
+ });
289
+
290
+ test("should compile component with templating string", () => {
291
+ const input = `<Canvas width={\`direction: \${direction}\`} />`;
292
+ const output = parser.parse(input);
293
+ expect(output).toBe(`h(Canvas, { width: \`direction: \${direction()}\` })`);
294
+ });
295
+
296
+ test("should compile component with templating string with @ (literal)", () => {
297
+ const input = `<Canvas width={\`direction: \${@direction}\`} />`;
298
+ const output = parser.parse(input);
299
+ expect(output).toBe(`h(Canvas, { width: \`direction: \${direction}\` })`);
300
+ });
146
301
 
147
302
  test("should compile component with object attribute", () => {
148
303
  const input = `<Canvas width={ {x: 10, y: 20} } />`;
149
304
  const output = parser.parse(input);
150
- expect(output).toBe(`h(Canvas, { width: ({x: 10, y: 20}) })`);
305
+ expect(output).toBe(`h(Canvas, { width: { x: 10, y: 20 } })`);
151
306
  });
152
307
 
153
308
  test("should compile component with complex object attribute", () => {
@@ -171,6 +326,42 @@ describe("Compiler", () => {
171
326
  expect(output).toBe(`h(Canvas, { width: computed(() => deep().value()) })`);
172
327
  });
173
328
 
329
+ test('should compile component with deep object attribute', () => {
330
+ const input = `<Button
331
+ style={{
332
+ backgroundColor: {
333
+ normal: "#6c757d",
334
+ hover: "#5a6268",
335
+ pressed: "#545b62"
336
+ },
337
+ text: {
338
+ fontSize: 16,
339
+ color: "#ffffff"
340
+ }
341
+ }}
342
+ />`;
343
+ const output = parser.parse(input);
344
+ expect(output).toBe(`h(Button, { style: { backgroundColor: { normal: "#6c757d", hover: "#5a6268", pressed: "#545b62" }, text: { fontSize: 16, color: "#ffffff" } } })`);
345
+ });
346
+
347
+ test('should compile component with deep object attribute and shorthand', () => {
348
+ const input = `<Canvas>
349
+ <Container>
350
+ <Sprite x y sheet={{
351
+ definition,
352
+ playing: animationPlaying,
353
+ params: {
354
+ direction
355
+ }
356
+ }}
357
+ controls />
358
+ </Container>
359
+ </Canvas>
360
+ `;
361
+ const output = parser.parse(input);
362
+ expect(output).toBe(`h(Canvas, null, h(Container, null, h(Sprite, { x, y, sheet: { definition, playing: animationPlaying, params: { direction } }, controls })))`);
363
+ });
364
+
174
365
  test("should compile component with deep object attribute but not transform to signal", () => {
175
366
  const input = `<Canvas width={@deep.value} />`;
176
367
  const output = parser.parse(input);
@@ -186,7 +377,7 @@ describe("Compiler", () => {
186
377
  test("should compile component with dynamic object attribute", () => {
187
378
  const input = `<Canvas width={ {x: x, y: 20} } />`;
188
379
  const output = parser.parse(input);
189
- expect(output).toBe(`h(Canvas, { width: computed(() => ({x: x(), y: 20})) })`);
380
+ expect(output).toBe(`h(Canvas, { width: { x: x, y: 20 } })`);
190
381
  });
191
382
 
192
383
  test("should compile component with array attribute", () => {
@@ -198,7 +389,7 @@ describe("Compiler", () => {
198
389
  test("should compile component with dynamic array attribute", () => {
199
390
  const input = `<Canvas width={ [x, 20] } />`;
200
391
  const output = parser.parse(input);
201
- expect(output).toBe(`h(Canvas, { width: computed(() => [x(), 20]) })`);
392
+ expect(output).toBe(`h(Canvas, { width: [x, 20] })`);
202
393
  });
203
394
 
204
395
  test("should compile component with standalone dynamic attribute", () => {
@@ -576,6 +767,43 @@ describe("Condition", () => {
576
767
  `cond(sprite.visible, () => [h(Sprite), cond(deep, () => h(Sprite)), h(Sprite)])`
577
768
  );
578
769
  });
770
+
771
+ test("should compile if/else within canvas", () => {
772
+ const input = `
773
+ <Canvas>
774
+ @if (showSprite) {
775
+ <Sprite />
776
+ }
777
+ @else {
778
+ <Text text="No sprite" />
779
+ }
780
+ </Canvas>
781
+ `;
782
+ const output = parser.parse(input);
783
+ expect(output).toBe(
784
+ `h(Canvas, null, cond(showSprite, () => h(Sprite), () => h(Text, { text: 'No sprite' })))`
785
+ );
786
+ });
787
+
788
+ test("should compile if/else if/else within loop", () => {
789
+ const input = `
790
+ @for (item of items) {
791
+ @if (item.type === 'sprite') {
792
+ <Sprite />
793
+ }
794
+ @else if (item.type === 'text') {
795
+ <Text />
796
+ }
797
+ @else {
798
+ <Container />
799
+ }
800
+ }
801
+ `;
802
+ const output = parser.parse(input);
803
+ expect(output.replace(/\s+/g, "")).toBe(
804
+ `loop(items,item=>cond(computed(()=>item().type()==='sprite'),()=>h(Sprite),[computed(()=>item().type()==='text'),()=>h(Text)],()=>h(Container)))`.replace(/\s+/g, "")
805
+ );
806
+ });
579
807
  });
580
808
 
581
809
  describe("Condition in Loops", () => {
@@ -748,6 +976,24 @@ describe('DOM with special attributes', () => {
748
976
  const output = parser.parse(input);
749
977
  expect(output).toBe('h(DOMElement, { element: "input", attrs: { type: \'password\' }, x: 100, y: 100 })');
750
978
  });
979
+
980
+ test('should compile DOM with text object', () => {
981
+ const input = `<p>{{ object.x }}</p>`;
982
+ const output = parser.parse(input);
983
+ expect(output).toBe('h(DOMElement, { element: "p", textContent: computed(() => object().x()) })');
984
+ });
985
+
986
+ test('should compile DOM with text object with @', () => {
987
+ const input = `<p>{{ @object.x }}</p>`;
988
+ const output = parser.parse(input);
989
+ expect(output).toBe('h(DOMElement, { element: "p", textContent: computed(() => object.x()) })');
990
+ });
991
+
992
+ test('should compile DOM with text object with literal ', () => {
993
+ const input = `<p>{{ @object.@x }}</p>`;
994
+ const output = parser.parse(input);
995
+ expect(output).toBe('h(DOMElement, { element: "p", textContent: object.x })');
996
+ });
751
997
  });
752
998
 
753
999
  describe('DOM with Control Structures', () => {