@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 +34 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/grammar.pegjs +264 -0
- package/index.ts +53 -6
- package/package.json +1 -1
- package/tests/compiler.spec.ts +274 -0
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":"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","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
package/tests/compiler.spec.ts
CHANGED
|
@@ -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
|
+
});
|