@canvasengine/compiler 2.0.0-beta.19 → 2.0.0-beta.20

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 CHANGED
@@ -1,3 +1,36 @@
1
+ /**
2
+ * Vite plugin to load shader files (.frag, .vert, .wgsl) as text strings
3
+ *
4
+ * This plugin allows importing shader files directly as string literals in your code.
5
+ * It supports fragment shaders (.frag), vertex shaders (.vert), and WebGPU shaders (.wgsl).
6
+ * The content is loaded as a raw string and can be used directly with graphics APIs.
7
+ *
8
+ * @returns {object} - Vite plugin configuration object
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // In your vite.config.ts
13
+ * import { shaderLoader } from './path/to/compiler'
14
+ *
15
+ * export default defineConfig({
16
+ * plugins: [shaderLoader()]
17
+ * })
18
+ *
19
+ * // In your code
20
+ * import fragmentShader from './shader.frag'
21
+ * import vertexShader from './shader.vert'
22
+ * import computeShader from './shader.wgsl'
23
+ *
24
+ * console.log(fragmentShader) // Raw shader code as string
25
+ * ```
26
+ */
27
+ declare function shaderLoader(): {
28
+ name: string;
29
+ transform(code: string, id: string): {
30
+ code: string;
31
+ map: null;
32
+ } | undefined;
33
+ };
1
34
  declare function canvasengine(): {
2
35
  name: string;
3
36
  transform(code: string, id: string): {
@@ -6,4 +39,4 @@ declare function canvasengine(): {
6
39
  } | undefined;
7
40
  };
8
41
 
9
- export { canvasengine as default };
42
+ export { canvasengine as default, shaderLoader };
package/dist/index.js CHANGED
@@ -22,6 +22,20 @@ ${errorLine}
22
22
  ${pointer}
23
23
  `;
24
24
  }
25
+ function shaderLoader() {
26
+ const filter = createFilter(/\.(frag|vert|wgsl)$/);
27
+ return {
28
+ name: "vite-plugin-shader-loader",
29
+ transform(code, id) {
30
+ if (!filter(id)) return;
31
+ const escapedCode = code.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
32
+ return {
33
+ code: `export default \`${escapedCode}\`;`,
34
+ map: null
35
+ };
36
+ }
37
+ };
38
+ }
25
39
  function canvasengine() {
26
40
  const filter = createFilter("**/*.ce");
27
41
  const __filename = fileURLToPath(import.meta.url);
@@ -48,7 +62,10 @@ function canvasengine() {
48
62
  "Triangle",
49
63
  "TilingSprite",
50
64
  "svg",
51
- "Video"
65
+ "Video",
66
+ "Mesh",
67
+ "Svg",
68
+ "DOMContainer"
52
69
  ];
53
70
  return {
54
71
  name: "vite-plugin-ce",
@@ -57,9 +74,6 @@ function canvasengine() {
57
74
  const scriptMatch = code.match(/<script>([\s\S]*?)<\/script>/);
58
75
  let scriptContent = scriptMatch ? scriptMatch[1].trim() : "";
59
76
  let template = code.replace(/<script>[\s\S]*?<\/script>/, "").replace(/^\s+|\s+$/g, "");
60
- template = template.replace(/<svg>([\s\S]*?)<\/svg>/g, (match, content) => {
61
- return `<Svg content="${content.trim()}" />`;
62
- });
63
77
  let parsedTemplate;
64
78
  try {
65
79
  parsedTemplate = parser.parse(template);
@@ -138,6 +152,7 @@ ${importsCode}`;
138
152
  };
139
153
  }
140
154
  export {
141
- canvasengine as default
155
+ canvasengine as default,
156
+ shaderLoader
142
157
  };
143
158
  //# sourceMappingURL=index.js.map
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\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":[]}
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 ];\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 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":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,aAAa;AACtB,OAAO,QAAQ;AACf,OAAO,SAAS;AAChB,OAAO,UAAU;AACjB,YAAY,QAAQ;AACpB,SAAS,qBAAqB;AAE9B,IAAM,EAAE,SAAS,IAAI;AAErB,IAAM,UAAU;AAehB,SAAS,iBAAiB,UAAkB,OAAoB;AAC9D,MAAI,CAAC,MAAM,UAAU;AACnB,WAAO,iBAAiB,MAAM,OAAO;AAAA,EACvC;AAEA,QAAM,QAAQ,SAAS,MAAM,IAAI;AACjC,QAAM,EAAE,MAAM,OAAO,IAAI,MAAM,SAAS;AACxC,QAAM,YAAY,MAAM,OAAO,CAAC,KAAK;AAGrC,QAAM,UAAU,IAAI,OAAO,SAAS,CAAC,IAAI;AAEzC,SAAO,wBAAwB,IAAI,YAAY,MAAM,KAAK,MAAM,OAAO;AAAA;AAAA,EAC7D,SAAS;AAAA,EAAK,OAAO;AAAA;AACjC;AA4BO,SAAS,eAAe;AAC7B,QAAM,SAAS,aAAa,qBAAqB;AAEjD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,MAAc,IAAY;AAClC,UAAI,CAAC,OAAO,EAAE,EAAG;AAGjB,YAAM,cAAc,KACjB,QAAQ,OAAO,MAAM,EACrB,QAAQ,MAAM,KAAK,EACnB,QAAQ,OAAO,KAAK;AAGvB,aAAO;AAAA,QACL,MAAM,oBAAoB,WAAW;AAAA,QACrC,KAAK;AAAA,MACP;AAAA,IACF;AAAA,EACF;AACF;AAEe,SAAR,eAAgC;AACrC,QAAM,SAAS,aAAa,SAAS;AAGrC,QAAM,aAAa,cAAc,YAAY,GAAG;AAChD,QAAM,YAAY,KAAK,QAAQ,UAAU;AAEzC,QAAM,UAAU,GAAG;AAAA,IACjB,KAAK,KAAK,WAAW,eAAe,EAAE,QAAQ,sBAAsB,eAAe;AAAA,IACrF;AAAA,EAAM;AACN,QAAM,SAAS,SAAS,OAAO;AAC/B,QAAM,QAAQ,QAAQ,IAAI,aAAa;AACvC,QAAM,eAAe;AAErB,QAAM,uBAAuB;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,MAAc,IAAY;AAClC,UAAI,CAAC,OAAO,EAAE,EAAG;AAGjB,YAAM,cAAc,KAAK,MAAM,8BAA8B;AAC7D,UAAI,gBAAgB,cAAc,YAAY,CAAC,EAAE,KAAK,IAAI;AAG1D,UAAI,WAAW,KAAK,QAAQ,8BAA8B,EAAE,EACzD,QAAQ,cAAc,EAAE;AAE3B,UAAI;AACJ,UAAI;AACF,yBAAiB,OAAO,MAAM,QAAQ;AAAA,MACxC,SAAS,OAAO;AACd,cAAM,WAAW,iBAAiB,UAAU,KAAK;AACjD,cAAM,IAAI,MAAM,kCAAkC,EAAE;AAAA,EAAM,QAAQ,EAAE;AAAA,MACtE;AAGA,uBAAiB,eAAe;AAEhC,UAAI,iBAAoB,mBAAgB,eAAe;AAAA,QACrD,iBAAiB;AAAA,UACf,QAAW,cAAW;AAAA,QACxB;AAAA,MACF,CAAC,EAAE;AAGH,uBAAiB,eAAe,MAAM,YAAY,EAAE,CAAC;AAGrD,YAAM,SAAS,MAAM,gBAAgB;AAAA,QACnC,YAAY;AAAA,QACZ,aAAa;AAAA,MACf,CAAC;AAGD,YAAM,UAAU,OAAO,KAAK;AAAA,QAC1B,CAAC,SAAS,KAAK,SAAS;AAAA,MAC1B;AAGA,YAAM,gBAAgB,OAAO,KAC1B,OAAO,CAAC,SAAS,KAAK,SAAS,mBAAmB,EAClD,IAAI,CAAC,SAAS,eAAe,MAAM,KAAK,OAAO,KAAK,GAAG,CAAC,EACxD,KAAK,IAAI;AAEZ,UAAI,cAAc,QACf,IAAI,CAAC,QAAQ;AACZ,YAAI,aAAa,eAAe,MAAM,IAAI,OAAO,IAAI,GAAG;AACxD,YAAI,SAAS,WAAW,SAAS,qBAAqB,GAAG;AACvD,uBAAa,WAAW;AAAA,YACtB;AAAA,YACA,SAAS,OAAO;AAAA,UAClB;AAAA,QACF;AACA,eAAO;AAAA,MACT,CAAC,EACA,KAAK,IAAI;AAGZ,YAAM,kBAAkB,CAAC,KAAK,YAAY,QAAQ,MAAM;AAGxD,YAAM,iBAAiB,gBAAgB;AAAA,QACrC,CAAC,eACC,CAAC,QAAQ;AAAA,UACP,CAAC,QACC,IAAI,cACJ,IAAI,WAAW;AAAA,YACb,CAAC,SACC,KAAK,SAAS,qBACd,KAAK,YACL,UAAU,KAAK,YACf,KAAK,SAAS,SAAS;AAAA,UAC3B;AAAA,QACJ;AAAA,MACJ;AAGA,UAAI,eAAe,SAAS,GAAG;AAC7B,cAAM,uBAAuB,YAAY,eAAe;AAAA,UACtD;AAAA,QACF,CAAC,WAAW,QAAQ,IAAI,OAAO,MAAM,gBAAgB;AACrD,sBAAc,GAAG,oBAAoB;AAAA,EAAK,WAAW;AAAA,MACvD;AAGA,YAAM,mBAAmB,qBAAqB;AAAA,QAAO,CAAC,cACpD,eAAe,SAAS,KAAK,SAAS,EAAE;AAAA,MAC1C;AAGA,uBAAiB,QAAQ,CAAC,cAAc;AACtC,cAAM,kBAAkB,YAAY,SAAS,WAC3C,QAAQ,IAAI,OAAO,MAAM,gBAC3B;AACA,YAAI,CAAC,YAAY,SAAS,eAAe,GAAG;AAC1C,wBAAc,GAAG,eAAe;AAAA,EAAK,WAAW;AAAA,QAClD;AAAA,MACF,CAAC;AAGD,YAAM,SAAS,OAAO;AAAA,QACpB,WAAW;AAAA,iDAC8B,QAAQ,IAAI,OAAO,MAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA,UAKhF,aAAa;AAAA,sBACD,cAAc;AAAA;AAAA;AAAA;AAK9B,aAAO;AAAA,QACL,MAAM;AAAA,QACN,KAAK;AAAA,MACP;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/grammar.pegjs CHANGED
@@ -29,6 +29,35 @@
29
29
  );
30
30
  }
31
31
 
32
+ // List of standard HTML DOM elements
33
+ const domElements = new Set([
34
+ 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 'samp', 's', 'script', 'section', 'select', 'slot', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'
35
+ ]);
36
+
37
+ // Framework components that should NOT be transformed to DOM elements
38
+ const frameworkComponents = new Set([
39
+ 'Canvas', 'Container', 'Sprite', 'Text', 'DOMContainer', 'Svg'
40
+ ]);
41
+
42
+ // DisplayObject special attributes that should not be in attrs
43
+ const displayObjectAttributes = new Set([
44
+ 'x', 'y', 'scale', 'anchor', 'skew', 'tint', 'rotation', 'angle',
45
+ 'zIndex', 'roundPixels', 'cursor', 'visible', 'alpha', 'pivot', 'filters', 'maskOf',
46
+ 'blendMode', 'filterArea', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
47
+ 'aspectRatio', 'flexGrow', 'flexShrink', 'flexBasis', 'rowGap', 'columnGap',
48
+ 'positionType', 'top', 'right', 'bottom', 'left', 'objectFit', 'objectPosition',
49
+ 'transformOrigin', 'flexDirection', 'justifyContent', 'alignItems', 'alignContent',
50
+ 'alignSelf', 'margin', 'padding', 'border', 'gap', 'blur', 'shadow'
51
+ ]);
52
+
53
+ function isDOMElement(tagName) {
54
+ // Don't transform framework components to DOM elements
55
+ if (frameworkComponents.has(tagName)) {
56
+ return false;
57
+ }
58
+ return domElements.has(tagName.toLowerCase());
59
+ }
60
+
32
61
  function formatAttributes(attributes) {
33
62
  if (attributes.length === 0) {
34
63
  return null;
@@ -56,6 +85,54 @@
56
85
 
57
86
  return `{ ${formattedAttrs.join(', ')} }`;
58
87
  }
88
+
89
+ function formatDOMElement(tagName, attributes) {
90
+ if (attributes.length === 0) {
91
+ return `h(DOMContainer, { element: "${tagName}" })`;
92
+ }
93
+
94
+ // Separate DisplayObject attributes from DOM attributes
95
+ const domAttrs = [];
96
+ const displayObjectAttrs = [];
97
+
98
+ attributes.forEach(attr => {
99
+ // Handle spread attributes
100
+ if (attr.startsWith('...')) {
101
+ displayObjectAttrs.push(attr);
102
+ return;
103
+ }
104
+
105
+ // Extract attribute name
106
+ let attrName;
107
+ if (attr.includes(':')) {
108
+ // Format: "name: value" or "'name': value"
109
+ attrName = attr.split(':')[0].trim().replace(/['"]/g, '');
110
+ } else {
111
+ // Standalone attribute
112
+ attrName = attr.replace(/['"]/g, '');
113
+ }
114
+
115
+ // Check if it's a DisplayObject attribute
116
+ if (displayObjectAttributes.has(attrName)) {
117
+ displayObjectAttrs.push(attr);
118
+ } else {
119
+ domAttrs.push(attr);
120
+ }
121
+ });
122
+
123
+ // Build the result
124
+ const parts = [`element: "${tagName}"`];
125
+
126
+ if (domAttrs.length > 0) {
127
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
128
+ }
129
+
130
+ if (displayObjectAttrs.length > 0) {
131
+ parts.push(...displayObjectAttrs);
132
+ }
133
+
134
+ return `h(DOMContainer, { ${parts.join(', ')} })`;
135
+ }
59
136
  }
60
137
 
61
138
  start
@@ -69,6 +146,8 @@ start
69
146
  element "component or control structure"
70
147
  = forLoop
71
148
  / ifCondition
149
+ / svgElement
150
+ / domElementWithText
72
151
  / selfClosingElement
73
152
  / openCloseElement
74
153
  / openUnclosedTag
@@ -76,10 +155,118 @@ element "component or control structure"
76
155
 
77
156
  selfClosingElement "self-closing component tag"
78
157
  = _ "<" _ tagName:tagName _ attributes:attributes _ "/>" _ {
158
+ // Check if it's a DOM element
159
+ if (isDOMElement(tagName)) {
160
+ return formatDOMElement(tagName, attributes);
161
+ }
162
+ // Otherwise, treat as regular component
79
163
  const attrsString = formatAttributes(attributes);
80
164
  return attrsString ? `h(${tagName}, ${attrsString})` : `h(${tagName})`;
81
165
  }
82
166
 
167
+ domElementWithText "DOM element with text content"
168
+ = "<" _ tagName:tagName _ attributes:attributes _ ">" _ text:simpleTextContent _ "</" _ closingTagName:tagName _ ">" _ {
169
+ if (tagName !== closingTagName) {
170
+ generateError(
171
+ `Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
172
+ location()
173
+ );
174
+ }
175
+
176
+ if (isDOMElement(tagName)) {
177
+ if (attributes.length === 0) {
178
+ return `h(DOMContainer, { element: "${tagName}", textContent: ${text} })`;
179
+ }
180
+
181
+ // Separate DisplayObject attributes from DOM attributes
182
+ const domAttrs = [];
183
+ const displayObjectAttrs = [];
184
+
185
+ attributes.forEach(attr => {
186
+ // Handle spread attributes
187
+ if (attr.startsWith('...')) {
188
+ displayObjectAttrs.push(attr);
189
+ return;
190
+ }
191
+
192
+ // Extract attribute name
193
+ let attrName;
194
+ if (attr.includes(':')) {
195
+ // Format: "name: value" or "'name': value"
196
+ attrName = attr.split(':')[0].trim().replace(/['"]/g, '');
197
+ } else {
198
+ // Standalone attribute
199
+ attrName = attr.replace(/['"]/g, '');
200
+ }
201
+
202
+ // Check if it's a DisplayObject attribute
203
+ if (displayObjectAttributes.has(attrName)) {
204
+ displayObjectAttrs.push(attr);
205
+ } else {
206
+ domAttrs.push(attr);
207
+ }
208
+ });
209
+
210
+ // Build the result
211
+ const parts = [`element: "${tagName}"`];
212
+
213
+ if (domAttrs.length > 0) {
214
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
215
+ }
216
+
217
+ parts.push(`textContent: ${text}`);
218
+
219
+ if (displayObjectAttrs.length > 0) {
220
+ parts.push(...displayObjectAttrs);
221
+ }
222
+
223
+ return `h(DOMContainer, { ${parts.join(', ')} })`;
224
+ }
225
+
226
+ // If not a DOM element, fall back to regular parsing
227
+ return null;
228
+ }
229
+
230
+ simpleTextContent "simple text content"
231
+ = parts:(simpleDynamicPart / simpleTextPart)+ {
232
+ const validParts = parts.filter(p => p !== null);
233
+ if (validParts.length === 0) return null;
234
+ if (validParts.length === 1) return validParts[0];
235
+
236
+ // Multiple parts - need to concatenate
237
+ const hasSignals = validParts.some(part => part && part.includes && part.includes('()'));
238
+ if (hasSignals) {
239
+ return `computed(() => ${validParts.join(' + ')})`;
240
+ }
241
+ return validParts.join(' + ');
242
+ }
243
+
244
+ simpleTextPart "simple text part"
245
+ = !("@for" / "@if") text:$([^<{@]+) {
246
+ const trimmed = text.trim();
247
+ return trimmed ? `'${trimmed}'` : null;
248
+ }
249
+
250
+ simpleDynamicPart "simple dynamic part"
251
+ = "{" _ expr:attributeValue _ "}" {
252
+ // Handle dynamic expressions like {item.name}
253
+ if (expr.trim().match(/^[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
254
+ let foundSignal = false;
255
+ const computedValue = expr.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => {
256
+ if (match.startsWith('@')) {
257
+ return match.substring(1);
258
+ }
259
+ foundSignal = true;
260
+ return `${match}()`;
261
+ });
262
+ if (foundSignal) {
263
+ return `computed(() => ${computedValue})`;
264
+ }
265
+ return computedValue;
266
+ }
267
+ return expr;
268
+ }
269
+
83
270
  openCloseElement "component with content"
84
271
  = "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ "</" _ closingTagName:tagName _ ">" _ {
85
272
  if (tagName !== closingTagName) {
@@ -88,6 +275,67 @@ openCloseElement "component with content"
88
275
  location()
89
276
  );
90
277
  }
278
+
279
+ // Check if it's a DOM element
280
+ if (isDOMElement(tagName)) {
281
+ const children = content ? content : null;
282
+
283
+ if (attributes.length === 0) {
284
+ if (children) {
285
+ return `h(DOMContainer, { element: "${tagName}" }, ${children})`;
286
+ } else {
287
+ return `h(DOMContainer, { element: "${tagName}" })`;
288
+ }
289
+ }
290
+
291
+ // Separate DisplayObject attributes from DOM attributes
292
+ const domAttrs = [];
293
+ const displayObjectAttrs = [];
294
+
295
+ attributes.forEach(attr => {
296
+ // Handle spread attributes
297
+ if (attr.startsWith('...')) {
298
+ displayObjectAttrs.push(attr);
299
+ return;
300
+ }
301
+
302
+ // Extract attribute name
303
+ let attrName;
304
+ if (attr.includes(':')) {
305
+ // Format: "name: value" or "'name': value"
306
+ attrName = attr.split(':')[0].trim().replace(/['"]/g, '');
307
+ } else {
308
+ // Standalone attribute
309
+ attrName = attr.replace(/['"]/g, '');
310
+ }
311
+
312
+ // Check if it's a DisplayObject attribute
313
+ if (displayObjectAttributes.has(attrName)) {
314
+ displayObjectAttrs.push(attr);
315
+ } else {
316
+ domAttrs.push(attr);
317
+ }
318
+ });
319
+
320
+ // Build the result
321
+ const parts = [`element: "${tagName}"`];
322
+
323
+ if (domAttrs.length > 0) {
324
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
325
+ }
326
+
327
+ if (displayObjectAttrs.length > 0) {
328
+ parts.push(...displayObjectAttrs);
329
+ }
330
+
331
+ if (children) {
332
+ return `h(DOMContainer, { ${parts.join(', ')} }, ${children})`;
333
+ } else {
334
+ return `h(DOMContainer, { ${parts.join(', ')} })`;
335
+ }
336
+ }
337
+
338
+ // Otherwise, treat as regular component
91
339
  const attrsString = formatAttributes(attributes);
92
340
  const children = content ? content : null;
93
341
  if (attrsString && children) {
@@ -270,6 +518,8 @@ content "component content"
270
518
  return `[${filteredElements.join(', ')}]`;
271
519
  }
272
520
 
521
+
522
+
273
523
  textNode
274
524
  = text:$([^<]+) {
275
525
  const trimmed = text.trim();
@@ -427,4 +677,18 @@ unclosedBrace "unclosed brace"
427
677
  `Missing closing brace in dynamic attribute '${attributeName}'`,
428
678
  location()
429
679
  );
680
+ }
681
+
682
+ svgElement "SVG element"
683
+ = "<svg" attrs:([^>]*) ">" content:svgInnerContent "</svg>" _ {
684
+ const attributes = attrs.join('').trim();
685
+ // Clean up the content by removing extra whitespace and newlines
686
+ const cleanContent = content.replace(/\s+/g, ' ').trim();
687
+ const rawContent = `<svg${attributes ? ' ' + attributes : ''}>${cleanContent}</svg>`;
688
+ return `h(Svg, { content: \`${rawContent}\` })`;
689
+ }
690
+
691
+ svgInnerContent "SVG inner content"
692
+ = content:$((!("</svg>") .)*) {
693
+ return content;
430
694
  }
package/index.ts CHANGED
@@ -39,6 +39,55 @@ function showErrorMessage(template: string, error: any): string {
39
39
  `${errorLine}\n${pointer}\n`;
40
40
  }
41
41
 
42
+ /**
43
+ * Vite plugin to load shader files (.frag, .vert, .wgsl) as text strings
44
+ *
45
+ * This plugin allows importing shader files directly as string literals in your code.
46
+ * It supports fragment shaders (.frag), vertex shaders (.vert), and WebGPU shaders (.wgsl).
47
+ * The content is loaded as a raw string and can be used directly with graphics APIs.
48
+ *
49
+ * @returns {object} - Vite plugin configuration object
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * // In your vite.config.ts
54
+ * import { shaderLoader } from './path/to/compiler'
55
+ *
56
+ * export default defineConfig({
57
+ * plugins: [shaderLoader()]
58
+ * })
59
+ *
60
+ * // In your code
61
+ * import fragmentShader from './shader.frag'
62
+ * import vertexShader from './shader.vert'
63
+ * import computeShader from './shader.wgsl'
64
+ *
65
+ * console.log(fragmentShader) // Raw shader code as string
66
+ * ```
67
+ */
68
+ export function shaderLoader() {
69
+ const filter = createFilter(/\.(frag|vert|wgsl)$/);
70
+
71
+ return {
72
+ name: "vite-plugin-shader-loader",
73
+ transform(code: string, id: string) {
74
+ if (!filter(id)) return;
75
+
76
+ // Escape the shader code to be safely embedded in a JavaScript string
77
+ const escapedCode = code
78
+ .replace(/\\/g, '\\\\') // Escape backslashes
79
+ .replace(/`/g, '\\`') // Escape backticks
80
+ .replace(/\$/g, '\\$'); // Escape dollar signs
81
+
82
+ // Return the shader content as a default export string
83
+ return {
84
+ code: `export default \`${escapedCode}\`;`,
85
+ map: null,
86
+ };
87
+ },
88
+ };
89
+ }
90
+
42
91
  export default function canvasengine() {
43
92
  const filter = createFilter("**/*.ce");
44
93
 
@@ -68,7 +117,10 @@ export default function canvasengine() {
68
117
  "Triangle",
69
118
  "TilingSprite",
70
119
  "svg",
71
- "Video"
120
+ "Video",
121
+ "Mesh",
122
+ "Svg",
123
+ "DOMContainer"
72
124
  ];
73
125
 
74
126
  return {
@@ -83,11 +135,6 @@ export default function canvasengine() {
83
135
  // Transform SVG tags to Svg components
84
136
  let template = code.replace(/<script>[\s\S]*?<\/script>/, "")
85
137
  .replace(/^\s+|\s+$/g, '');
86
-
87
- // Add SVG transformation
88
- template = template.replace(/<svg>([\s\S]*?)<\/svg>/g, (match, content) => {
89
- return `<Svg content="${content.trim()}" />`;
90
- });
91
138
 
92
139
  let parsedTemplate;
93
140
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canvasengine/compiler",
3
- "version": "2.0.0-beta.19",
3
+ "version": "2.0.0-beta.20",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -615,3 +615,277 @@ describe("Condition in Loops", () => {
615
615
  });
616
616
 
617
617
  });
618
+
619
+ describe('Svg', () => {
620
+ test('should compile svg', () => {
621
+ const input = `<svg>
622
+ <path d="M 100 350 l 150 -300" stroke="red" stroke-width="4"/>
623
+ </svg>`;
624
+ const output = parser.parse(input);
625
+ expect(output).toBe('h(Svg, { content: `<svg><path d="M 100 350 l 150 -300" stroke="red" stroke-width="4"/></svg>` })');
626
+ });
627
+
628
+ test('should compile svg with canvas', () => {
629
+ const input = `<Canvas antialias={true}>
630
+ <svg height="400" width="450" xmlns="http://www.w3.org/2000/svg"></svg></Canvas>`;
631
+ const output = parser.parse(input);
632
+ expect(output).toBe('h(Canvas, { antialias: true }, h(Svg, { content: `<svg height="400" width="450" xmlns="http://www.w3.org/2000/svg"></svg>` }))');
633
+ });
634
+ });
635
+
636
+ describe('DOM', () => {
637
+ test('should compile input DOM', () => {
638
+ const input = `<input type="text" />`;
639
+ const output = parser.parse(input);
640
+ expect(output).toBe('h(DOMContainer, { element: "input", attrs: { type: \'text\' } })');
641
+ });
642
+
643
+ test('should compile div DOM', () => {
644
+ const input = `<div class="container" />`;
645
+ const output = parser.parse(input);
646
+ expect(output).toBe('h(DOMContainer, { element: "div", attrs: { class: \'container\' } })');
647
+ });
648
+
649
+ test('should compile button DOM', () => {
650
+ const input = `<button type="submit" />`;
651
+ const output = parser.parse(input);
652
+ expect(output).toBe('h(DOMContainer, { element: "button", attrs: { type: \'submit\' } })');
653
+ });
654
+
655
+ test('should compile button DOM', () => {
656
+ const input = `<button>Text</button>`;
657
+ const output = parser.parse(input);
658
+ expect(output).toBe('h(DOMContainer, { element: "button", textContent: \'Text\' })');
659
+ });
660
+
661
+ test('should compile textarea DOM with dynamic attributes', () => {
662
+ const input = `<textarea rows={rows} cols={cols} />`;
663
+ const output = parser.parse(input);
664
+ expect(output).toBe('h(DOMContainer, { element: "textarea", attrs: { rows: rows, cols: cols } })');
665
+ });
666
+
667
+ test('should not transform Canvas to DOM', () => {
668
+ const input = `<Canvas width={800} />`;
669
+ const output = parser.parse(input);
670
+ expect(output).toBe('h(Canvas, { width: 800 })');
671
+ });
672
+
673
+ test('should not transform Sprite to DOM', () => {
674
+ const input = `<Sprite image="test.png" />`;
675
+ const output = parser.parse(input);
676
+ expect(output).toBe('h(Sprite, { image: \'test.png\' })');
677
+ });
678
+
679
+ // Tests avec imbrications DOM
680
+ test('should compile nested DOM elements', () => {
681
+ const input = `<div class="container">
682
+ <p>Hello World</p>
683
+ </div>`;
684
+ const output = parser.parse(input);
685
+ expect(output).toBe('h(DOMContainer, { element: "div", attrs: { class: \'container\' } }, h(DOMContainer, { element: "p", textContent: \'Hello World\' }))');
686
+ });
687
+
688
+ test('should compile deeply nested DOM elements', () => {
689
+ const input = `<div class="wrapper">
690
+ <section>
691
+ <h1>Title</h1>
692
+ <p>Content</p>
693
+ </section>
694
+ </div>`;
695
+ const output = parser.parse(input);
696
+ expect(output.replace(/\s+/g, "")).toBe(
697
+ `h(DOMContainer, { element: "div", attrs: { class: 'wrapper' } }, h(DOMContainer, { element: "section" }, [h(DOMContainer, { element: "h1", textContent: 'Title' }), h(DOMContainer, { element: "p", textContent: 'Content' })]))`.replace(/\s+/g, "")
698
+ );
699
+ });
700
+
701
+ test('should compile DOM with multiple children', () => {
702
+ const input = `<ul>
703
+ <li>Item 1</li>
704
+ <li>Item 2</li>
705
+ <li>Item 3</li>
706
+ </ul>`;
707
+ const output = parser.parse(input);
708
+ expect(output.replace(/\s+/g, "")).toBe(
709
+ `h(DOMContainer, { element: "ul" }, [h(DOMContainer, { element: "li", textContent: 'Item 1' }), h(DOMContainer, { element: "li", textContent: 'Item 2' }), h(DOMContainer, { element: "li", textContent: 'Item 3' })])`.replace(/\s+/g, "")
710
+ );
711
+ });
712
+
713
+ test('should compile mixed DOM and framework components', () => {
714
+ const input = `<div class="game-container">
715
+ <Canvas width={800} height={600} />
716
+ <div class="ui">
717
+ <button>Start Game</button>
718
+ </div>
719
+ </div>`;
720
+ const output = parser.parse(input);
721
+ expect(output.replace(/\s+/g, "")).toBe(
722
+ `h(DOMContainer, { element: "div", attrs: { class: 'game-container' } }, [h(Canvas, { width: 800, height: 600 }), h(DOMContainer, { element: "div", attrs: { class: 'ui' } }, h(DOMContainer, { element: "button", textContent: 'Start Game' }))])`.replace(/\s+/g, "")
723
+ );
724
+ });
725
+
726
+ test('should compile DOM with attributes and text content', () => {
727
+ const input = `<button class="btn primary" type="submit">Submit Form</button>`;
728
+ const output = parser.parse(input);
729
+ expect(output).toBe('h(DOMContainer, { element: "button", attrs: { class: \'btn primary\', type: \'submit\' }, textContent: \'Submit Form\' })');
730
+ });
731
+ });
732
+
733
+ describe('DOM with special attributes', () => {
734
+ test('should compile DOM with special attributes', () => {
735
+ const input = `<input type="password" x={100} y={100} />`;
736
+ const output = parser.parse(input);
737
+ expect(output).toBe('h(DOMContainer, { element: "input", attrs: { type: \'password\' }, x: 100, y: 100 })');
738
+ });
739
+ });
740
+
741
+ describe('DOM with Control Structures', () => {
742
+ test('should compile @for loop with DOM elements', () => {
743
+ const input = `
744
+ @for (item of items) {
745
+ <li>{item.name}</li>
746
+ }
747
+ `;
748
+ const output = parser.parse(input);
749
+ expect(output.replace(/\s+/g, "")).toBe(
750
+ `loop(items, item => h(DOMContainer, { element: "li", textContent: computed(() => item().name()) }))`.replace(/\s+/g, "")
751
+ );
752
+ });
753
+
754
+ test('should compile @for loop with nested DOM structure', () => {
755
+ const input = `
756
+ <ul class="menu">
757
+ @for (item of menuItems) {
758
+ <li class="menu-item">
759
+ <a href={item.url}>{item.title}</a>
760
+ </li>
761
+ }
762
+ </ul>
763
+ `;
764
+ const output = parser.parse(input);
765
+ expect(output.replace(/\s+/g, "")).toBe(
766
+ `h(DOMContainer, { element: "ul", attrs: { class: 'menu' } }, loop(menuItems, item => h(DOMContainer, { element: "li", attrs: { class: 'menu-item' } }, h(DOMContainer, { element: "a", attrs: { href: computed(() => item().url()) }, textContent: computed(() => item().title()) }))))`.replace(/\s+/g, "")
767
+ );
768
+ });
769
+
770
+ test('should compile @if condition with DOM elements', () => {
771
+ const input = `
772
+ @if (showMessage) {
773
+ <div class="alert">
774
+ <p>Important message!</p>
775
+ </div>
776
+ }
777
+ `;
778
+ const output = parser.parse(input);
779
+ expect(output.replace(/\s+/g, "")).toBe(
780
+ `cond(showMessage, () => h(DOMContainer, { element: "div", attrs: { class: 'alert' } }, h(DOMContainer, { element: "p", textContent: 'Important message!' })))`.replace(/\s+/g, "")
781
+ );
782
+ });
783
+
784
+ test('should compile @if condition with simple DOM element', () => {
785
+ const input = `
786
+ @if (isVisible) {
787
+ <button>Click me</button>
788
+ }
789
+ `;
790
+ const output = parser.parse(input);
791
+ expect(output.replace(/\s+/g, "")).toBe(
792
+ `cond(isVisible, () => h(DOMContainer, { element: "button", textContent: 'Click me' }))`.replace(/\s+/g, "")
793
+ );
794
+ });
795
+
796
+ test('should compile nested @for and @if with DOM', () => {
797
+ const input = `
798
+ <div class="container">
799
+ @for (section of sections) {
800
+ @if (section.visible) {
801
+ <section class="content">
802
+ <h2>{section.title}</h2>
803
+ <div class="items">
804
+ @for (item of section.items) {
805
+ <div class="item">{item.name}</div>
806
+ }
807
+ </div>
808
+ </section>
809
+ }
810
+ }
811
+ </div>
812
+ `;
813
+ const output = parser.parse(input);
814
+ expect(output.replace(/\s+/g, "")).toBe(
815
+ `h(DOMContainer, { element: "div", attrs: { class: 'container' } }, loop(sections, section => cond(section.visible, () => h(DOMContainer, { element: "section", attrs: { class: 'content' } }, [h(DOMContainer, { element: "h2", textContent: computed(() => section().title()) }), h(DOMContainer, { element: "div", attrs: { class: 'items' } }, loop(section.items, item => h(DOMContainer, { element: "div", attrs: { class: 'item' }, textContent: computed(() => item().name()) })))]))))`.replace(/\s+/g, "")
816
+ );
817
+ });
818
+
819
+ test('should compile @for with DOM table structure', () => {
820
+ const input = `
821
+ <table>
822
+ <thead>
823
+ <tr>
824
+ <th>Name</th>
825
+ <th>Age</th>
826
+ </tr>
827
+ </thead>
828
+ <tbody>
829
+ @for (user of users) {
830
+ <tr>
831
+ <td>{user.name}</td>
832
+ <td>{user.age}</td>
833
+ </tr>
834
+ }
835
+ </tbody>
836
+ </table>
837
+ `;
838
+ const output = parser.parse(input);
839
+ expect(output.replace(/\s+/g, "")).toBe(
840
+ `h(DOMContainer, { element: "table" }, [h(DOMContainer, { element: "thead" }, h(DOMContainer, { element: "tr" }, [h(DOMContainer, { element: "th", textContent: 'Name' }), h(DOMContainer, { element: "th", textContent: 'Age' })])), h(DOMContainer, { element: "tbody" }, loop(users, user => h(DOMContainer, { element: "tr" }, [h(DOMContainer, { element: "td", textContent: computed(() => user().name()) }), h(DOMContainer, { element: "td", textContent: computed(() => user().age()) })])))])`.replace(/\s+/g, "")
841
+ );
842
+ });
843
+
844
+ test('should compile @if with multiple DOM conditions', () => {
845
+ const input = `
846
+ <div class="status">
847
+ @if (isLoading) {
848
+ <div class="spinner">Loading...</div>
849
+ }
850
+ @if (hasError) {
851
+ <div class="error">Error occurred!</div>
852
+ }
853
+ @if (isSuccess) {
854
+ <div class="success">Success!</div>
855
+ }
856
+ </div>
857
+ `;
858
+ const output = parser.parse(input);
859
+ expect(output.replace(/\s+/g, "")).toBe(
860
+ `h(DOMContainer, { element: "div", attrs: { class: 'status' } }, [cond(isLoading, () => h(DOMContainer, { element: "div", attrs: { class: 'spinner' }, textContent: 'Loading...' })), cond(hasError, () => h(DOMContainer, { element: "div", attrs: { class: 'error' }, textContent: 'Error occurred!' })), cond(isSuccess, () => h(DOMContainer, { element: "div", attrs: { class: 'success' }, textContent: 'Success!' }))])`.replace(/\s+/g, "")
861
+ );
862
+ });
863
+
864
+ test('should compile mixed Canvas and DOM with control structures', () => {
865
+ const input = `
866
+ <div class="game-wrapper">
867
+ <Canvas width={800} height={600}>
868
+ @for (sprite of sprites) {
869
+ <Sprite x={sprite.x} y={sprite.y} />
870
+ }
871
+ </Canvas>
872
+ <div class="ui-overlay">
873
+ @if (showScore) {
874
+ <div class="score">Score: {score}</div>
875
+ }
876
+ @if (showMenu) {
877
+ <div class="menu">
878
+ @for (option of menuOptions) {
879
+ <button class="menu-btn">{option.label}</button>
880
+ }
881
+ </div>
882
+ }
883
+ </div>
884
+ </div>
885
+ `;
886
+ const output = parser.parse(input);
887
+ expect(output.replace(/\s+/g, "")).toBe(
888
+ `h(DOMContainer, { element: "div", attrs: { class: 'game-wrapper' } }, [h(Canvas, { width: 800, height: 600 }, loop(sprites, sprite => h(Sprite, { x: computed(() => sprite().x()), y: computed(() => sprite().y()) }))), h(DOMContainer, { element: "div", attrs: { class: 'ui-overlay' } }, [cond(showScore, () => h(DOMContainer, { element: "div", attrs: { class: 'score' }, textContent: computed(() => 'Score: ' + computed(() => score())) })), cond(showMenu, () => h(DOMContainer, { element: "div", attrs: { class: 'menu' } }, loop(menuOptions, option => h(DOMContainer, { element: "button", attrs: { class: 'menu-btn' }, textContent: computed(() => option().label()) }))))])])`.replace(/\s+/g, "")
889
+ );
890
+ });
891
+ });