@canvasengine/compiler 2.0.0-beta.25 → 2.0.0-beta.27
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.d.ts +1 -2
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/grammar.pegjs +91 -10
- package/index.ts +3 -5
- package/package.json +2 -2
- package/tests/compiler.spec.ts +224 -3
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -66,14 +66,13 @@ 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",
|
|
73
|
-
|
|
74
|
-
load(id) {
|
|
74
|
+
transform(code, id) {
|
|
75
75
|
if (!filter(id)) return null;
|
|
76
|
-
const code = fs.readFileSync(id, "utf8");
|
|
77
76
|
const scriptMatch = code.match(/<script>([\s\S]*?)<\/script>/);
|
|
78
77
|
let scriptContent = scriptMatch ? scriptMatch[1].trim() : "";
|
|
79
78
|
let template = code.replace(/<script>[\s\S]*?<\/script>/, "").replace(/^\s+|\s+$/g, "");
|
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 enforce: \"pre\",\n load(id: string) {\n if (!filter(id)) return null;\n\n const code = fs.readFileSync(id, \"utf8\");\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
|
|
@@ -396,8 +396,15 @@ dynamicAttribute "dynamic attribute"
|
|
|
396
396
|
const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
|
|
397
397
|
const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
|
|
398
398
|
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
|
|
400
|
+
// If it's a complex object with strings, preserve it as is
|
|
401
|
+
if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}') &&
|
|
402
|
+
(attributeValue.includes('"') || attributeValue.includes("'"))) {
|
|
403
|
+
return `${formattedName}: ${attributeValue}`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// If it's a template string, preserve it as is
|
|
407
|
+
if (attributeValue.trim().startsWith('`') && attributeValue.trim().endsWith('`')) {
|
|
401
408
|
return `${formattedName}: ${attributeValue}`;
|
|
402
409
|
}
|
|
403
410
|
|
|
@@ -408,16 +415,52 @@ dynamicAttribute "dynamic attribute"
|
|
|
408
415
|
return `${formattedName}: ${attributeValue}`;
|
|
409
416
|
} else {
|
|
410
417
|
let foundSignal = false;
|
|
411
|
-
|
|
418
|
+
let hasLiterals = false;
|
|
419
|
+
const computedValue = attributeValue.replace(/@?([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*:)/g, (match, p1, offset) => {
|
|
420
|
+
// Don't transform keywords, numbers, or if we're inside quotes
|
|
421
|
+
if (['true', 'false', 'null'].includes(p1) || /^\d+(\.\d+)?$/.test(p1)) {
|
|
422
|
+
return match;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Check if we're inside a string literal
|
|
426
|
+
const beforeMatch = attributeValue.substring(0, offset);
|
|
427
|
+
const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
|
|
428
|
+
const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
|
|
429
|
+
|
|
430
|
+
// If we're inside quotes, don't transform
|
|
431
|
+
if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
|
|
432
|
+
return match;
|
|
433
|
+
}
|
|
434
|
+
|
|
412
435
|
if (match.startsWith('@')) {
|
|
413
|
-
|
|
436
|
+
hasLiterals = true;
|
|
437
|
+
return p1; // Remove @ prefix
|
|
414
438
|
}
|
|
415
439
|
foundSignal = true;
|
|
416
|
-
return `${
|
|
440
|
+
return `${p1}()`;
|
|
417
441
|
});
|
|
442
|
+
|
|
418
443
|
if (foundSignal) {
|
|
444
|
+
// For objects, wrap in parentheses
|
|
445
|
+
if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) {
|
|
446
|
+
// Remove spaces for objects in parentheses
|
|
447
|
+
const cleanedObject = computedValue.replace(/{ /g, '{').replace(/ }/g, '}');
|
|
448
|
+
return `${formattedName}: computed(() => (${cleanedObject}))`;
|
|
449
|
+
}
|
|
419
450
|
return `${formattedName}: computed(() => ${computedValue})`;
|
|
420
451
|
}
|
|
452
|
+
|
|
453
|
+
// If only literals (all @), don't use computed
|
|
454
|
+
if (hasLiterals && !foundSignal) {
|
|
455
|
+
return `${formattedName}: ${computedValue}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// For static objects, add parentheses if it's an object
|
|
459
|
+
if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) {
|
|
460
|
+
// Remove spaces for objects in parentheses
|
|
461
|
+
const cleanedObject = computedValue.replace(/{ /g, '{').replace(/ }/g, '}');
|
|
462
|
+
return `${formattedName}: (${cleanedObject})`;
|
|
463
|
+
}
|
|
421
464
|
return `${formattedName}: ${computedValue}`;
|
|
422
465
|
}
|
|
423
466
|
}
|
|
@@ -462,6 +505,7 @@ propertyValue
|
|
|
462
505
|
/ element
|
|
463
506
|
/ functionWithElement
|
|
464
507
|
/ stringLiteral
|
|
508
|
+
/ number
|
|
465
509
|
/ identifier
|
|
466
510
|
|
|
467
511
|
nestedObject
|
|
@@ -543,8 +587,31 @@ tupleDestructuring "destructuring pattern"
|
|
|
543
587
|
}
|
|
544
588
|
|
|
545
589
|
ifCondition "if condition"
|
|
546
|
-
= _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
|
|
547
|
-
|
|
590
|
+
= _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ elseIfs:elseIfClause* elseClause:elseClause? _ {
|
|
591
|
+
let result = `cond(${condition}, () => ${content}`;
|
|
592
|
+
|
|
593
|
+
// Add else if clauses
|
|
594
|
+
elseIfs.forEach(elseIf => {
|
|
595
|
+
result += `, [${elseIf.condition}, () => ${elseIf.content}]`;
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Add else clause if present
|
|
599
|
+
if (elseClause) {
|
|
600
|
+
result += `, () => ${elseClause}`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
result += ')';
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
elseIfClause "else if clause"
|
|
608
|
+
= _ "@else" _ "if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
|
|
609
|
+
return { condition, content };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
elseClause "else clause"
|
|
613
|
+
= _ "@else" _ "{" _ content:content _ "}" _ {
|
|
614
|
+
return content;
|
|
548
615
|
}
|
|
549
616
|
|
|
550
617
|
tagName "tag name"
|
|
@@ -596,12 +663,26 @@ condition "condition expression"
|
|
|
596
663
|
// Transform simple identifiers to function calls like "foo" to "foo()"
|
|
597
664
|
// This regex matches identifiers not followed by an opening parenthesis.
|
|
598
665
|
// This transformation should only apply if we are wrapping in 'computed'.
|
|
599
|
-
if (originalText.includes('!') || originalText.includes('&&') || originalText.includes('||')
|
|
600
|
-
|
|
666
|
+
if (originalText.includes('!') || originalText.includes('&&') || originalText.includes('||') ||
|
|
667
|
+
originalText.includes('>=') || originalText.includes('<=') || originalText.includes('===') ||
|
|
668
|
+
originalText.includes('!==') || originalText.includes('==') || originalText.includes('!=') ||
|
|
669
|
+
originalText.includes('>') || originalText.includes('<')) {
|
|
670
|
+
const transformedText = originalText.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match, p1, offset) => {
|
|
601
671
|
// Do not transform keywords (true, false, null) or numeric literals
|
|
602
672
|
if (['true', 'false', 'null'].includes(match) || /^\d+(\.\d+)?$/.test(match)) {
|
|
603
673
|
return match;
|
|
604
674
|
}
|
|
675
|
+
// Check if the match is inside quotes
|
|
676
|
+
const beforeMatch = originalText.substring(0, offset);
|
|
677
|
+
const afterMatch = originalText.substring(offset + match.length);
|
|
678
|
+
const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
|
|
679
|
+
const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
|
|
680
|
+
|
|
681
|
+
// If we're inside quotes, don't transform
|
|
682
|
+
if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
|
|
683
|
+
return match;
|
|
684
|
+
}
|
|
685
|
+
|
|
605
686
|
return `${match}()`;
|
|
606
687
|
});
|
|
607
688
|
return `computed(() => ${transformedText})`;
|
package/index.ts
CHANGED
|
@@ -121,17 +121,15 @@ 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 {
|
|
128
129
|
name: "vite-plugin-ce",
|
|
129
|
-
|
|
130
|
-
load(id: string) {
|
|
130
|
+
transform(code: string, id: string) {
|
|
131
131
|
if (!filter(id)) return null;
|
|
132
132
|
|
|
133
|
-
const code = fs.readFileSync(id, "utf8");
|
|
134
|
-
|
|
135
133
|
// Extract the script content
|
|
136
134
|
const scriptMatch = code.match(/<script>([\s\S]*?)<\/script>/);
|
|
137
135
|
let scriptContent = scriptMatch ? scriptMatch[1].trim() : "";
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canvasengine/compiler",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.27",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "tsup",
|
|
8
|
+
"build": "tsup && cp grammar.pegjs ../../docs/public/grammar.pegjs",
|
|
9
9
|
"dev": "tsup --watch"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
package/tests/compiler.spec.ts
CHANGED
|
@@ -131,6 +131,172 @@ describe("Compiler", () => {
|
|
|
131
131
|
expect(output).toBe(`h(Canvas, { width: 20 })`);
|
|
132
132
|
});
|
|
133
133
|
|
|
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
|
+
// test("should compile component with templating string", () => {
|
|
290
|
+
// const input = `<Canvas width={\`direction: \${direction}\`} />`;
|
|
291
|
+
// const output = parser.parse(input);
|
|
292
|
+
// expect(output).toBe(`h(Canvas, { width: \`direction: \${direction()}\` })`);
|
|
293
|
+
// });
|
|
294
|
+
|
|
295
|
+
// test("should compile component with templating string with @ (literal)", () => {
|
|
296
|
+
// const input = `<Canvas width={\`direction: \${@direction}\`} />`;
|
|
297
|
+
// const output = parser.parse(input);
|
|
298
|
+
// expect(output).toBe(`h(Canvas, { width: \`direction: \${direction}\` })`);
|
|
299
|
+
// });
|
|
134
300
|
|
|
135
301
|
test("should compile component with object attribute", () => {
|
|
136
302
|
const input = `<Canvas width={ {x: 10, y: 20} } />`;
|
|
@@ -159,6 +325,24 @@ describe("Compiler", () => {
|
|
|
159
325
|
expect(output).toBe(`h(Canvas, { width: computed(() => deep().value()) })`);
|
|
160
326
|
});
|
|
161
327
|
|
|
328
|
+
test('should compile component with deep object attribute', () => {
|
|
329
|
+
const input = `<Button
|
|
330
|
+
style={{
|
|
331
|
+
backgroundColor: {
|
|
332
|
+
normal: "#6c757d",
|
|
333
|
+
hover: "#5a6268",
|
|
334
|
+
pressed: "#545b62"
|
|
335
|
+
},
|
|
336
|
+
text: {
|
|
337
|
+
fontSize: 16,
|
|
338
|
+
color: "#ffffff"
|
|
339
|
+
}
|
|
340
|
+
}}
|
|
341
|
+
/>`;
|
|
342
|
+
const output = parser.parse(input);
|
|
343
|
+
expect(output).toBe(`h(Button, { style: { backgroundColor: { normal: "#6c757d", hover: "#5a6268", pressed: "#545b62" }, text: { fontSize: 16, color: "#ffffff" } } })`);
|
|
344
|
+
});
|
|
345
|
+
|
|
162
346
|
test("should compile component with deep object attribute but not transform to signal", () => {
|
|
163
347
|
const input = `<Canvas width={@deep.value} />`;
|
|
164
348
|
const output = parser.parse(input);
|
|
@@ -262,19 +446,19 @@ describe("Compiler", () => {
|
|
|
262
446
|
});
|
|
263
447
|
|
|
264
448
|
test("should compile component with event handler", () => {
|
|
265
|
-
const input = `<Sprite
|
|
449
|
+
const input = `<Sprite click={fn} />`;
|
|
266
450
|
const output = parser.parse(input);
|
|
267
451
|
expect(output).toBe(`h(Sprite, { click: fn })`);
|
|
268
452
|
});
|
|
269
453
|
|
|
270
454
|
test("should compile component with standalone event handler", () => {
|
|
271
|
-
const input = `<Sprite
|
|
455
|
+
const input = `<Sprite click />`;
|
|
272
456
|
const output = parser.parse(input);
|
|
273
457
|
expect(output).toBe(`h(Sprite, { click })`);
|
|
274
458
|
});
|
|
275
459
|
|
|
276
460
|
test('should compile component with inline event handler', () => {
|
|
277
|
-
const input = `<Sprite
|
|
461
|
+
const input = `<Sprite click={() => console.log('click')} />`;
|
|
278
462
|
const output = parser.parse(input);
|
|
279
463
|
expect(output).toBe(`h(Sprite, { click: () => console.log('click') })`);
|
|
280
464
|
});
|
|
@@ -564,6 +748,43 @@ describe("Condition", () => {
|
|
|
564
748
|
`cond(sprite.visible, () => [h(Sprite), cond(deep, () => h(Sprite)), h(Sprite)])`
|
|
565
749
|
);
|
|
566
750
|
});
|
|
751
|
+
|
|
752
|
+
test("should compile if/else within canvas", () => {
|
|
753
|
+
const input = `
|
|
754
|
+
<Canvas>
|
|
755
|
+
@if (showSprite) {
|
|
756
|
+
<Sprite />
|
|
757
|
+
}
|
|
758
|
+
@else {
|
|
759
|
+
<Text text="No sprite" />
|
|
760
|
+
}
|
|
761
|
+
</Canvas>
|
|
762
|
+
`;
|
|
763
|
+
const output = parser.parse(input);
|
|
764
|
+
expect(output).toBe(
|
|
765
|
+
`h(Canvas, null, cond(showSprite, () => h(Sprite), () => h(Text, { text: 'No sprite' })))`
|
|
766
|
+
);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test("should compile if/else if/else within loop", () => {
|
|
770
|
+
const input = `
|
|
771
|
+
@for (item of items) {
|
|
772
|
+
@if (item.type === 'sprite') {
|
|
773
|
+
<Sprite />
|
|
774
|
+
}
|
|
775
|
+
@else if (item.type === 'text') {
|
|
776
|
+
<Text />
|
|
777
|
+
}
|
|
778
|
+
@else {
|
|
779
|
+
<Container />
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
`;
|
|
783
|
+
const output = parser.parse(input);
|
|
784
|
+
expect(output.replace(/\s+/g, "")).toBe(
|
|
785
|
+
`loop(items,item=>cond(computed(()=>item().type()==='sprite'),()=>h(Sprite),[computed(()=>item().type()==='text'),()=>h(Text)],()=>h(Container)))`.replace(/\s+/g, "")
|
|
786
|
+
);
|
|
787
|
+
});
|
|
567
788
|
});
|
|
568
789
|
|
|
569
790
|
describe("Condition in Loops", () => {
|