@mkdevofficial/liquid-compiler 0.0.0-stage → 0.1.0
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/package.json +22 -4
- package/src/analyzer.js +93 -0
- package/src/codegen.js +581 -0
- package/src/css.js +151 -0
- package/src/errors.js +52 -0
- package/src/index.js +53 -0
- package/src/parser.js +428 -0
- package/src/sourcemap.js +90 -0
- package/src/tokenizer.js +509 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mkdevofficial/liquid-compiler",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": " Compiler for the Liquid frontend framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"liquid",
|
|
18
|
+
"compiler",
|
|
19
|
+
"reactive",
|
|
20
|
+
"signals",
|
|
21
|
+
"parser"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT"
|
|
24
|
+
}
|
package/src/analyzer.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyzer: scope resolution, component resolution, scoped style hashing, warnings.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Simple deterministic string hash for scope attributes
|
|
6
|
+
export function hashString(str) {
|
|
7
|
+
let hash = 5381;
|
|
8
|
+
let i = str.length;
|
|
9
|
+
while (i) {
|
|
10
|
+
hash = (hash * 33) ^ str.charCodeAt(--i);
|
|
11
|
+
}
|
|
12
|
+
return (hash >>> 0).toString(36).slice(0, 8);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function analyze(ast, options = {}) {
|
|
16
|
+
const file = options.file || 'Component.liq';
|
|
17
|
+
const warnings = [];
|
|
18
|
+
const referencedComponents = new Set();
|
|
19
|
+
const explicitImports = new Set();
|
|
20
|
+
|
|
21
|
+
// Scan explicit imports in logic section
|
|
22
|
+
const logicContent = ast.logic.content || '';
|
|
23
|
+
const importRegex = /import\s+(?:([a-zA-Z_$][a-zA-Z0-9_$]*)|(?:\{([^}]+)\}))\s+from\s+['"][^'"]+['"]/g;
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = importRegex.exec(logicContent)) !== null) {
|
|
26
|
+
if (match[1]) {
|
|
27
|
+
explicitImports.add(match[1].trim());
|
|
28
|
+
}
|
|
29
|
+
if (match[2]) {
|
|
30
|
+
const named = match[2].split(',').map(s => s.trim().split(/\s+as\s+/)[0]);
|
|
31
|
+
for (const n of named) {
|
|
32
|
+
if (n) explicitImports.add(n);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Traverse template
|
|
38
|
+
function visit(node) {
|
|
39
|
+
if (!node) return;
|
|
40
|
+
|
|
41
|
+
if (node.type === 'Element') {
|
|
42
|
+
if (node.isComponent) {
|
|
43
|
+
referencedComponents.add(node.tag);
|
|
44
|
+
}
|
|
45
|
+
for (const child of node.children) {
|
|
46
|
+
visit(child);
|
|
47
|
+
}
|
|
48
|
+
} else if (node.type === 'IfBlock') {
|
|
49
|
+
for (const c of node.consequent) visit(c);
|
|
50
|
+
if (node.alternate) {
|
|
51
|
+
if (Array.isArray(node.alternate)) {
|
|
52
|
+
for (const a of node.alternate) visit(a);
|
|
53
|
+
} else {
|
|
54
|
+
visit(node.alternate);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else if (node.type === 'EachBlock') {
|
|
58
|
+
if (!node.key) {
|
|
59
|
+
warnings.push({
|
|
60
|
+
message: `'each' block for '${node.collection}' does not have a key expression. Provide a key like '(${node.item}.id)' for optimal reconciliation.`,
|
|
61
|
+
loc: node.loc
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
for (const child of node.children) {
|
|
65
|
+
visit(child);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const node of ast.template) {
|
|
71
|
+
visit(node);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Components that are referenced but not explicitly imported
|
|
75
|
+
const autoImportComponents = [];
|
|
76
|
+
for (const comp of referencedComponents) {
|
|
77
|
+
if (!explicitImports.has(comp)) {
|
|
78
|
+
autoImportComponents.push(comp);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Generate unique scoped attribute
|
|
83
|
+
const scopeId = hashString(file + (ast.styles.content || '') + (ast.logic.content || ''));
|
|
84
|
+
const scopeAttr = `data-l-${scopeId}`;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
scopeId,
|
|
88
|
+
scopeAttr,
|
|
89
|
+
referencedComponents: Array.from(referencedComponents),
|
|
90
|
+
autoImportComponents,
|
|
91
|
+
warnings
|
|
92
|
+
};
|
|
93
|
+
}
|
package/src/codegen.js
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { SourceMapGenerator } from './sourcemap.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate readable ES module code from Liquid AST and Analysis.
|
|
5
|
+
*/
|
|
6
|
+
export function generate(ast, analysis, options = {}) {
|
|
7
|
+
const file = options.file || 'Component.liq';
|
|
8
|
+
const componentName = options.name || getComponentNameFromFile(file);
|
|
9
|
+
const runtimeModule = options.runtimeModule || 'liquid';
|
|
10
|
+
const scopeAttr = analysis.scopeAttr;
|
|
11
|
+
|
|
12
|
+
const mapGen = new SourceMapGenerator({
|
|
13
|
+
file: `${componentName}.js`,
|
|
14
|
+
sourceFile: file
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const imports = [];
|
|
18
|
+
const autoImports = [];
|
|
19
|
+
const bodyLines = [];
|
|
20
|
+
let varCounter = 0;
|
|
21
|
+
|
|
22
|
+
function nextVar(prefix = '_v') {
|
|
23
|
+
return `${prefix}${varCounter++}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Runtime helper tracking
|
|
27
|
+
const usedRuntimeHelpers = new Set(['element', 'text']);
|
|
28
|
+
|
|
29
|
+
// Separate logic into top-level imports and component body
|
|
30
|
+
const rawLogic = ast.logic.content || '';
|
|
31
|
+
const logicLines = rawLogic.split(/\r?\n/);
|
|
32
|
+
const userImports = [];
|
|
33
|
+
const userBody = [];
|
|
34
|
+
|
|
35
|
+
let inMultiLineImport = false;
|
|
36
|
+
for (let idx = 0; idx < logicLines.length; idx++) {
|
|
37
|
+
const line = logicLines[idx];
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
|
|
40
|
+
if (inMultiLineImport) {
|
|
41
|
+
userImports.push(line);
|
|
42
|
+
if (/['"][^'"]+['"]\s*;?$/.test(trimmed)) {
|
|
43
|
+
inMultiLineImport = false;
|
|
44
|
+
}
|
|
45
|
+
} else if (trimmed.startsWith('import ') || trimmed.startsWith('import{')) {
|
|
46
|
+
userImports.push(line);
|
|
47
|
+
if (!/['"][^'"]+['"]\s*;?$/.test(trimmed)) {
|
|
48
|
+
inMultiLineImport = true;
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
userBody.push({ text: line, lineNum: ast.logic.line + idx });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle auto-imported components
|
|
56
|
+
if (options.resolveComponent) {
|
|
57
|
+
for (const comp of analysis.autoImportComponents) {
|
|
58
|
+
const resolved = options.resolveComponent(comp, file);
|
|
59
|
+
if (resolved) {
|
|
60
|
+
autoImports.push(resolved);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
for (const comp of analysis.autoImportComponents) {
|
|
65
|
+
autoImports.push(`import ${comp} from './${comp}.liq';`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generate DOM tree code
|
|
70
|
+
const rootVar = '_root';
|
|
71
|
+
bodyLines.push(` const ${rootVar} = [];`);
|
|
72
|
+
|
|
73
|
+
for (const node of ast.template) {
|
|
74
|
+
generateNode(node, rootVar, true);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
bodyLines.push(` return ${rootVar}.length === 1 ? ${rootVar}[0] : ${rootVar};`);
|
|
78
|
+
|
|
79
|
+
// Build final code
|
|
80
|
+
const outputLines = [];
|
|
81
|
+
|
|
82
|
+
// 1. Runtime imports
|
|
83
|
+
outputLines.push(`import { ${Array.from(usedRuntimeHelpers).join(', ')} } from '${runtimeModule}';`);
|
|
84
|
+
|
|
85
|
+
// 2. User & Auto imports
|
|
86
|
+
if (userImports.length > 0) {
|
|
87
|
+
outputLines.push(userImports.join('\n'));
|
|
88
|
+
}
|
|
89
|
+
if (autoImports.length > 0) {
|
|
90
|
+
outputLines.push(autoImports.join('\n'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
outputLines.push('');
|
|
94
|
+
|
|
95
|
+
// 3. Component function definition
|
|
96
|
+
const funcHeaderLine = outputLines.length + 1;
|
|
97
|
+
outputLines.push(`export default function ${componentName}(props = {}, context = {}) {`);
|
|
98
|
+
|
|
99
|
+
// Inject props accessor helper
|
|
100
|
+
outputLines.push(` function defineProps(defaults = {}) { return Object.assign({}, defaults, props); }`);
|
|
101
|
+
|
|
102
|
+
// User logic body
|
|
103
|
+
for (const item of userBody) {
|
|
104
|
+
const genLine = outputLines.length + 1;
|
|
105
|
+
outputLines.push(` ${item.text}`);
|
|
106
|
+
if (item.text.trim()) {
|
|
107
|
+
mapGen.addMapping({
|
|
108
|
+
generatedLine: genLine,
|
|
109
|
+
generatedColumn: 2,
|
|
110
|
+
originalLine: item.lineNum,
|
|
111
|
+
originalColumn: 1
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
outputLines.push('');
|
|
117
|
+
|
|
118
|
+
// UI DOM creation body
|
|
119
|
+
for (const line of bodyLines) {
|
|
120
|
+
outputLines.push(line);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
outputLines.push('}');
|
|
124
|
+
|
|
125
|
+
const finalCode = outputLines.join('\n');
|
|
126
|
+
const sourceMap = mapGen.generate();
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
code: finalCode,
|
|
130
|
+
map: sourceMap
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Helper to recursively generate node creation instructions.
|
|
135
|
+
*/
|
|
136
|
+
function generateNode(node, parentVar, isRoot = false) {
|
|
137
|
+
if (!node) return;
|
|
138
|
+
|
|
139
|
+
if (node.type === 'Text') {
|
|
140
|
+
generateTextNode(node, parentVar, isRoot);
|
|
141
|
+
} else if (node.type === 'Element') {
|
|
142
|
+
if (node.isComponent) {
|
|
143
|
+
generateComponentNode(node, parentVar, isRoot);
|
|
144
|
+
} else {
|
|
145
|
+
generateElementNode(node, parentVar, isRoot);
|
|
146
|
+
}
|
|
147
|
+
} else if (node.type === 'IfBlock') {
|
|
148
|
+
generateIfBlock(node, parentVar, isRoot);
|
|
149
|
+
} else if (node.type === 'EachBlock') {
|
|
150
|
+
generateEachBlock(node, parentVar, isRoot);
|
|
151
|
+
} else if (node.type === 'Slot') {
|
|
152
|
+
generateSlotNode(node, parentVar, isRoot);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function generateTextNode(node, parentVar, isRoot) {
|
|
157
|
+
// If text has only one static raw part
|
|
158
|
+
if (node.parts.length === 1 && node.parts[0].type === 'raw') {
|
|
159
|
+
const tVar = nextVar('_t');
|
|
160
|
+
bodyLines.push(` const ${tVar} = text(${JSON.stringify(node.parts[0].value)});`);
|
|
161
|
+
appendOrPush(parentVar, tVar, isRoot);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Dynamic text parts
|
|
166
|
+
for (const part of node.parts) {
|
|
167
|
+
const tVar = nextVar('_t');
|
|
168
|
+
if (part.type === 'raw') {
|
|
169
|
+
bodyLines.push(` const ${tVar} = text(${JSON.stringify(part.value)});`);
|
|
170
|
+
} else {
|
|
171
|
+
usedRuntimeHelpers.add('bindText');
|
|
172
|
+
bodyLines.push(` const ${tVar} = text('');`);
|
|
173
|
+
bodyLines.push(` bindText(${tVar}, () => (${part.value}));`);
|
|
174
|
+
}
|
|
175
|
+
appendOrPush(parentVar, tVar, isRoot);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function generateElementNode(node, parentVar, isRoot) {
|
|
180
|
+
const elVar = nextVar('_el');
|
|
181
|
+
const classList = node.classes.length > 0 ? JSON.stringify(node.classes) : 'null';
|
|
182
|
+
bodyLines.push(` const ${elVar} = element(${JSON.stringify(node.tag)}, ${JSON.stringify(scopeAttr)}, ${classList});`);
|
|
183
|
+
|
|
184
|
+
if (node.id) {
|
|
185
|
+
bodyLines.push(` ${elVar}.id = ${JSON.stringify(node.id)};`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Static and dynamic attributes
|
|
189
|
+
for (const attr of node.attributes) {
|
|
190
|
+
if (attr.isBoolean) {
|
|
191
|
+
bodyLines.push(` ${elVar}.setAttribute(${JSON.stringify(attr.name)}, '');`);
|
|
192
|
+
} else if (!attr.isDynamic) {
|
|
193
|
+
if (attr.name === 'class' && node.classes.length > 0) {
|
|
194
|
+
const combined = node.classes.join(' ') + (attr.value ? ' ' + attr.value : '');
|
|
195
|
+
bodyLines.push(` ${elVar}.setAttribute('class', ${JSON.stringify(combined)});`);
|
|
196
|
+
} else {
|
|
197
|
+
bodyLines.push(` ${elVar}.setAttribute(${JSON.stringify(attr.name)}, ${JSON.stringify(attr.value)});`);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
usedRuntimeHelpers.add('bindAttr');
|
|
201
|
+
if (attr.name === 'class' && node.classes.length > 0) {
|
|
202
|
+
const baseClasses = JSON.stringify(node.classes.join(' '));
|
|
203
|
+
bodyLines.push(` bindAttr(${elVar}, "class", () => (${baseClasses} + ((${attr.value}) ? ' ' + (${attr.value}) : '')));`);
|
|
204
|
+
} else {
|
|
205
|
+
bodyLines.push(` bindAttr(${elVar}, ${JSON.stringify(attr.name)}, () => (${attr.value}));`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Event listeners
|
|
211
|
+
for (const ev of node.events) {
|
|
212
|
+
usedRuntimeHelpers.add('listen');
|
|
213
|
+
const mods = ev.modifiers.length > 0 ? JSON.stringify(ev.modifiers) : '[]';
|
|
214
|
+
const rawHandler = (ev.handler || '').trim();
|
|
215
|
+
// If handler is empty, use a no-op function (common with standalone modifiers like @click.stop)
|
|
216
|
+
const handlerExpr = !rawHandler
|
|
217
|
+
? '() => {}'
|
|
218
|
+
: (/[\(\)=]/.test(rawHandler) && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(rawHandler)
|
|
219
|
+
? `(e) => { ${rawHandler}; }`
|
|
220
|
+
: rawHandler);
|
|
221
|
+
bodyLines.push(` listen(${elVar}, ${JSON.stringify(ev.name)}, ${handlerExpr}, ${mods});`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Two-way bindings
|
|
225
|
+
for (const bind of node.bindings) {
|
|
226
|
+
usedRuntimeHelpers.add('bindInput');
|
|
227
|
+
bodyLines.push(` bindInput(${elVar}, ${bind.target}, ${JSON.stringify(bind.prop)});`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Children
|
|
231
|
+
for (const child of node.children) {
|
|
232
|
+
generateNode(child, elVar, false);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
appendOrPush(parentVar, elVar, isRoot);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function generateComponentNode(node, parentVar, isRoot) {
|
|
239
|
+
usedRuntimeHelpers.add('append');
|
|
240
|
+
const compVar = nextVar('_c');
|
|
241
|
+
const propsObj = [];
|
|
242
|
+
|
|
243
|
+
for (const attr of node.attributes) {
|
|
244
|
+
const val = attr.isDynamic ? attr.value : JSON.stringify(attr.value);
|
|
245
|
+
propsObj.push(`${JSON.stringify(attr.name)}: ${val}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Default slot content if children exist
|
|
249
|
+
if (node.children.length > 0) {
|
|
250
|
+
const slotChildrenVar = nextVar('_slot');
|
|
251
|
+
const slotBody = [];
|
|
252
|
+
slotBody.push(`() => {`);
|
|
253
|
+
slotBody.push(` const ${slotChildrenVar} = [];`);
|
|
254
|
+
|
|
255
|
+
// Temporary redirect
|
|
256
|
+
const origBodyLines = bodyLines;
|
|
257
|
+
const subLines = [];
|
|
258
|
+
// Replace bodyLines temporarily
|
|
259
|
+
// We will generate children into subLines
|
|
260
|
+
generateSubtreeInto(node.children, slotChildrenVar, subLines);
|
|
261
|
+
slotBody.push(...subLines.map(l => ' ' + l));
|
|
262
|
+
slotBody.push(` return ${slotChildrenVar};`);
|
|
263
|
+
slotBody.push(`}`);
|
|
264
|
+
|
|
265
|
+
propsObj.push(`default: ${slotBody.join('\n')}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
bodyLines.push(` const ${compVar} = ${node.tag}({ ${propsObj.join(', ')} });`);
|
|
269
|
+
if (isRoot) {
|
|
270
|
+
bodyLines.push(` if (Array.isArray(${compVar})) ${parentVar}.push(...${compVar}); else if (${compVar}) ${parentVar}.push(${compVar});`);
|
|
271
|
+
} else {
|
|
272
|
+
bodyLines.push(` if (Array.isArray(${compVar})) append(${parentVar}, ...${compVar}); else if (${compVar}) append(${parentVar}, ${compVar});`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function generateIfBlock(node, parentVar, isRoot) {
|
|
277
|
+
usedRuntimeHelpers.add('createIfBlock');
|
|
278
|
+
usedRuntimeHelpers.add('append');
|
|
279
|
+
const anchorVar = nextVar('_anchor');
|
|
280
|
+
bodyLines.push(` const ${anchorVar} = document.createComment('if');`);
|
|
281
|
+
appendOrPush(parentVar, anchorVar, isRoot);
|
|
282
|
+
|
|
283
|
+
const thenNodesVar = nextVar('_thenNodes');
|
|
284
|
+
const thenSubLines = [];
|
|
285
|
+
generateSubtreeInto(node.consequent, thenNodesVar, thenSubLines);
|
|
286
|
+
|
|
287
|
+
let elseSubLines = null;
|
|
288
|
+
let elseNodesVar = null;
|
|
289
|
+
if (node.alternate) {
|
|
290
|
+
elseNodesVar = nextVar('_elseNodes');
|
|
291
|
+
elseSubLines = [];
|
|
292
|
+
const altNodes = Array.isArray(node.alternate) ? node.alternate : [node.alternate];
|
|
293
|
+
generateSubtreeInto(altNodes, elseNodesVar, elseSubLines);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const thenFn = `() => {\n const ${thenNodesVar} = [];\n${thenSubLines.map(l => ' ' + l).join('\n')}\n return ${thenNodesVar};\n }`;
|
|
297
|
+
const elseFn = elseSubLines
|
|
298
|
+
? `() => {\n const ${elseNodesVar} = [];\n${elseSubLines.map(l => ' ' + l).join('\n')}\n return ${elseNodesVar};\n }`
|
|
299
|
+
: 'null';
|
|
300
|
+
|
|
301
|
+
bodyLines.push(` createIfBlock(${anchorVar}, () => (${node.test}), ${thenFn}, ${elseFn});`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function generateEachBlock(node, parentVar, isRoot) {
|
|
305
|
+
usedRuntimeHelpers.add('createEachBlock');
|
|
306
|
+
usedRuntimeHelpers.add('append');
|
|
307
|
+
const anchorVar = nextVar('_anchor');
|
|
308
|
+
bodyLines.push(` const ${anchorVar} = document.createComment('each');`);
|
|
309
|
+
appendOrPush(parentVar, anchorVar, isRoot);
|
|
310
|
+
|
|
311
|
+
const itemArg = node.item;
|
|
312
|
+
const indexArg = node.index || '_idx';
|
|
313
|
+
const keyExpr = node.key ? `(${itemArg}, ${indexArg}) => (${node.key})` : `(${itemArg}, ${indexArg}) => ${indexArg}`;
|
|
314
|
+
|
|
315
|
+
const eachNodesVar = nextVar('_eachNodes');
|
|
316
|
+
const eachSubLines = [];
|
|
317
|
+
generateSubtreeInto(node.children, eachNodesVar, eachSubLines);
|
|
318
|
+
|
|
319
|
+
// Create item proxy so item.prop, item(), and primitive {item} both work cleanly!
|
|
320
|
+
const itemProxySetup = ` const ${itemArg} = new Proxy(${itemArg}Sig, {
|
|
321
|
+
get(target, prop) {
|
|
322
|
+
if (prop === 'isSignal') return true;
|
|
323
|
+
if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') {
|
|
324
|
+
return () => target();
|
|
325
|
+
}
|
|
326
|
+
const val = target();
|
|
327
|
+
if (val != null && prop in Object(val)) {
|
|
328
|
+
const res = val[prop];
|
|
329
|
+
return typeof res === 'function' ? res.bind(val) : res;
|
|
330
|
+
}
|
|
331
|
+
return val;
|
|
332
|
+
},
|
|
333
|
+
apply(target) { return target(); }
|
|
334
|
+
});
|
|
335
|
+
const ${indexArg} = new Proxy(${indexArg}Sig, {
|
|
336
|
+
apply(target) { return target(); },
|
|
337
|
+
get(target, prop) {
|
|
338
|
+
if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') return () => target();
|
|
339
|
+
const val = target();
|
|
340
|
+
if (val != null && prop in Object(val)) {
|
|
341
|
+
const res = val[prop];
|
|
342
|
+
return typeof res === 'function' ? res.bind(val) : res;
|
|
343
|
+
}
|
|
344
|
+
return val;
|
|
345
|
+
}
|
|
346
|
+
});`;
|
|
347
|
+
|
|
348
|
+
bodyLines.push(` createEachBlock(${anchorVar}, () => (${node.collection}), ${keyExpr}, (${itemArg}Sig, ${indexArg}Sig) => {
|
|
349
|
+
${itemProxySetup}
|
|
350
|
+
const ${eachNodesVar} = [];
|
|
351
|
+
${eachSubLines.map(l => ' ' + l).join('\n')}
|
|
352
|
+
return ${eachNodesVar};
|
|
353
|
+
});`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function generateSlotNode(node, parentVar, isRoot) {
|
|
357
|
+
usedRuntimeHelpers.add('createSlot');
|
|
358
|
+
usedRuntimeHelpers.add('append');
|
|
359
|
+
const slotVar = nextVar('_slot');
|
|
360
|
+
const slotName = node.name ? JSON.stringify(node.name) : "'default'";
|
|
361
|
+
let fallbackFn = 'null';
|
|
362
|
+
|
|
363
|
+
if (node.fallback.length > 0) {
|
|
364
|
+
const fbVar = nextVar('_fb');
|
|
365
|
+
const fbLines = [];
|
|
366
|
+
generateSubtreeInto(node.fallback, fbVar, fbLines);
|
|
367
|
+
fallbackFn = `() => {\n const ${fbVar} = [];\n${fbLines.map(l => ' ' + l).join('\n')}\n return ${fbVar};\n }`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
bodyLines.push(` const ${slotVar} = createSlot(props, ${slotName}, ${fallbackFn});`);
|
|
371
|
+
if (isRoot) {
|
|
372
|
+
bodyLines.push(` ${parentVar}.push(...${slotVar});`);
|
|
373
|
+
} else {
|
|
374
|
+
bodyLines.push(` append(${parentVar}, ...${slotVar});`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function generateSubtreeInto(nodes, containerVar, lineBuffer) {
|
|
379
|
+
for (const n of nodes) {
|
|
380
|
+
generateSubNode(n, containerVar, true, lineBuffer);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function generateSubNode(n, parent, isRootSub, buf) {
|
|
385
|
+
if (!n) return;
|
|
386
|
+
if (n.type === 'Text') {
|
|
387
|
+
if (n.parts.length === 1 && n.parts[0].type === 'raw') {
|
|
388
|
+
const tVar = nextVar('_t');
|
|
389
|
+
buf.push(`const ${tVar} = text(${JSON.stringify(n.parts[0].value)});`);
|
|
390
|
+
if (isRootSub) {
|
|
391
|
+
buf.push(`${parent}.push(${tVar});`);
|
|
392
|
+
} else {
|
|
393
|
+
buf.push(`${parent}.appendChild(${tVar});`);
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
for (const p of n.parts) {
|
|
397
|
+
const tVar = nextVar('_t');
|
|
398
|
+
if (p.type === 'raw') {
|
|
399
|
+
buf.push(`const ${tVar} = text(${JSON.stringify(p.value)});`);
|
|
400
|
+
} else {
|
|
401
|
+
usedRuntimeHelpers.add('bindText');
|
|
402
|
+
buf.push(`const ${tVar} = text('');`);
|
|
403
|
+
buf.push(`bindText(${tVar}, () => (${p.value}));`);
|
|
404
|
+
}
|
|
405
|
+
if (isRootSub) {
|
|
406
|
+
buf.push(`${parent}.push(${tVar});`);
|
|
407
|
+
} else {
|
|
408
|
+
buf.push(`${parent}.appendChild(${tVar});`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} else if (n.type === 'Element') {
|
|
413
|
+
if (n.isComponent) {
|
|
414
|
+
usedRuntimeHelpers.add('append');
|
|
415
|
+
const compVar = nextVar('_c');
|
|
416
|
+
const propsObj = [];
|
|
417
|
+
for (const attr of n.attributes) {
|
|
418
|
+
const val = attr.isDynamic ? attr.value : JSON.stringify(attr.value);
|
|
419
|
+
propsObj.push(`${JSON.stringify(attr.name)}: ${val}`);
|
|
420
|
+
}
|
|
421
|
+
if (n.children.length > 0) {
|
|
422
|
+
const slotChildrenVar = nextVar('_slot');
|
|
423
|
+
const slotBody = [];
|
|
424
|
+
slotBody.push(`() => {`);
|
|
425
|
+
slotBody.push(` const ${slotChildrenVar} = [];`);
|
|
426
|
+
const subLines = [];
|
|
427
|
+
generateSubtreeInto(n.children, slotChildrenVar, subLines);
|
|
428
|
+
slotBody.push(...subLines.map(l => ' ' + l));
|
|
429
|
+
slotBody.push(` return ${slotChildrenVar};`);
|
|
430
|
+
slotBody.push(`}`);
|
|
431
|
+
propsObj.push(`default: ${slotBody.join('\n')}`);
|
|
432
|
+
}
|
|
433
|
+
buf.push(`const ${compVar} = ${n.tag}({ ${propsObj.join(', ')} });`);
|
|
434
|
+
if (isRootSub) {
|
|
435
|
+
buf.push(`if (Array.isArray(${compVar})) ${parent}.push(...${compVar}); else if (${compVar}) ${parent}.push(${compVar});`);
|
|
436
|
+
} else {
|
|
437
|
+
buf.push(`if (Array.isArray(${compVar})) append(${parent}, ...${compVar}); else if (${compVar}) append(${parent}, ${compVar});`);
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
const elVar = nextVar('_el');
|
|
441
|
+
const classList = n.classes.length > 0 ? JSON.stringify(n.classes) : 'null';
|
|
442
|
+
buf.push(`const ${elVar} = element(${JSON.stringify(n.tag)}, ${JSON.stringify(scopeAttr)}, ${classList});`);
|
|
443
|
+
if (n.id) buf.push(`${elVar}.id = ${JSON.stringify(n.id)};`);
|
|
444
|
+
|
|
445
|
+
for (const attr of n.attributes) {
|
|
446
|
+
if (attr.isBoolean) {
|
|
447
|
+
buf.push(`${elVar}.setAttribute(${JSON.stringify(attr.name)}, '');`);
|
|
448
|
+
} else if (!attr.isDynamic) {
|
|
449
|
+
if (attr.name === 'class' && n.classes.length > 0) {
|
|
450
|
+
const combined = n.classes.join(' ') + (attr.value ? ' ' + attr.value : '');
|
|
451
|
+
buf.push(`${elVar}.setAttribute('class', ${JSON.stringify(combined)});`);
|
|
452
|
+
} else {
|
|
453
|
+
buf.push(`${elVar}.setAttribute(${JSON.stringify(attr.name)}, ${JSON.stringify(attr.value)});`);
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
usedRuntimeHelpers.add('bindAttr');
|
|
457
|
+
if (attr.name === 'class' && n.classes.length > 0) {
|
|
458
|
+
const baseClasses = JSON.stringify(n.classes.join(' '));
|
|
459
|
+
buf.push(`bindAttr(${elVar}, "class", () => (${baseClasses} + ((${attr.value}) ? ' ' + (${attr.value}) : '')));`);
|
|
460
|
+
} else {
|
|
461
|
+
buf.push(`bindAttr(${elVar}, ${JSON.stringify(attr.name)}, () => (${attr.value}));`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
for (const ev of n.events) {
|
|
466
|
+
usedRuntimeHelpers.add('listen');
|
|
467
|
+
const mods = ev.modifiers.length > 0 ? JSON.stringify(ev.modifiers) : '[]';
|
|
468
|
+
const rawHandler = (ev.handler || '').trim();
|
|
469
|
+
const handlerExpr = !rawHandler
|
|
470
|
+
? '() => {}'
|
|
471
|
+
: (/[\(\)=]/.test(rawHandler) && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(rawHandler)
|
|
472
|
+
? `(e) => { ${rawHandler}; }`
|
|
473
|
+
: rawHandler);
|
|
474
|
+
buf.push(`listen(${elVar}, ${JSON.stringify(ev.name)}, ${handlerExpr}, ${mods});`);
|
|
475
|
+
}
|
|
476
|
+
for (const bind of n.bindings) {
|
|
477
|
+
usedRuntimeHelpers.add('bindInput');
|
|
478
|
+
buf.push(`bindInput(${elVar}, ${bind.target}, ${JSON.stringify(bind.prop)});`);
|
|
479
|
+
}
|
|
480
|
+
for (const child of n.children) {
|
|
481
|
+
const childBuf = [];
|
|
482
|
+
generateSubNode(child, elVar, false, childBuf);
|
|
483
|
+
buf.push(...childBuf);
|
|
484
|
+
}
|
|
485
|
+
if (isRootSub) {
|
|
486
|
+
buf.push(`${parent}.push(${elVar});`);
|
|
487
|
+
} else {
|
|
488
|
+
buf.push(`${parent}.appendChild(${elVar});`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} else if (n.type === 'IfBlock') {
|
|
492
|
+
usedRuntimeHelpers.add('createIfBlock');
|
|
493
|
+
const anchorVar = nextVar('_anchor');
|
|
494
|
+
buf.push(`const ${anchorVar} = document.createComment('if');`);
|
|
495
|
+
if (isRootSub) {
|
|
496
|
+
buf.push(`${parent}.push(${anchorVar});`);
|
|
497
|
+
} else {
|
|
498
|
+
buf.push(`${parent}.appendChild(${anchorVar});`);
|
|
499
|
+
}
|
|
500
|
+
const thenNodesVar = nextVar('_thenNodes');
|
|
501
|
+
const thenSub = [];
|
|
502
|
+
generateSubtreeInto(n.consequent, thenNodesVar, thenSub);
|
|
503
|
+
let elseSub = null;
|
|
504
|
+
let elseNodesVar = null;
|
|
505
|
+
if (n.alternate) {
|
|
506
|
+
elseNodesVar = nextVar('_elseNodes');
|
|
507
|
+
const altNodes = Array.isArray(n.alternate) ? n.alternate : [n.alternate];
|
|
508
|
+
elseSub = [];
|
|
509
|
+
generateSubtreeInto(altNodes, elseNodesVar, elseSub);
|
|
510
|
+
}
|
|
511
|
+
const thenFn = `() => {\n const ${thenNodesVar} = [];\n${thenSub.map(l => ' ' + l).join('\n')}\n return ${thenNodesVar};\n }`;
|
|
512
|
+
const elseFn = elseSub
|
|
513
|
+
? `() => {\n const ${elseNodesVar} = [];\n${elseSub.map(l => ' ' + l).join('\n')}\n return ${elseNodesVar};\n }`
|
|
514
|
+
: 'null';
|
|
515
|
+
buf.push(`createIfBlock(${anchorVar}, () => (${n.test}), ${thenFn}, ${elseFn});`);
|
|
516
|
+
} else if (n.type === 'EachBlock') {
|
|
517
|
+
usedRuntimeHelpers.add('createEachBlock');
|
|
518
|
+
const anchorVar = nextVar('_anchor');
|
|
519
|
+
buf.push(`const ${anchorVar} = document.createComment('each');`);
|
|
520
|
+
if (isRootSub) {
|
|
521
|
+
buf.push(`${parent}.push(${anchorVar});`);
|
|
522
|
+
} else {
|
|
523
|
+
buf.push(`${parent}.appendChild(${anchorVar});`);
|
|
524
|
+
}
|
|
525
|
+
const itemArg = n.item;
|
|
526
|
+
const indexArg = n.index || '_idx';
|
|
527
|
+
const keyExpr = n.key ? `(${itemArg}, ${indexArg}) => (${n.key})` : `(${itemArg}, ${indexArg}) => ${indexArg}`;
|
|
528
|
+
const eachNodesVar = nextVar('_eachNodes');
|
|
529
|
+
const eachSub = [];
|
|
530
|
+
generateSubtreeInto(n.children, eachNodesVar, eachSub);
|
|
531
|
+
|
|
532
|
+
const itemProxySetup = ` const ${itemArg} = new Proxy(${itemArg}Sig, {
|
|
533
|
+
get(target, prop) {
|
|
534
|
+
if (prop === 'isSignal') return true;
|
|
535
|
+
if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') {
|
|
536
|
+
return () => target();
|
|
537
|
+
}
|
|
538
|
+
const val = target();
|
|
539
|
+
if (val != null && prop in Object(val)) {
|
|
540
|
+
const res = val[prop];
|
|
541
|
+
return typeof res === 'function' ? res.bind(val) : res;
|
|
542
|
+
}
|
|
543
|
+
return val;
|
|
544
|
+
},
|
|
545
|
+
apply(target) { return target(); }
|
|
546
|
+
});
|
|
547
|
+
const ${indexArg} = new Proxy(${indexArg}Sig, {
|
|
548
|
+
apply(target) { return target(); },
|
|
549
|
+
get(target, prop) {
|
|
550
|
+
if (prop === Symbol.toPrimitive || prop === 'valueOf' || prop === 'toString') return () => target();
|
|
551
|
+
const val = target();
|
|
552
|
+
if (val != null && prop in Object(val)) {
|
|
553
|
+
const res = val[prop];
|
|
554
|
+
return typeof res === 'function' ? res.bind(val) : res;
|
|
555
|
+
}
|
|
556
|
+
return val;
|
|
557
|
+
}
|
|
558
|
+
});`;
|
|
559
|
+
|
|
560
|
+
buf.push(`createEachBlock(${anchorVar}, () => (${n.collection}), ${keyExpr}, (${itemArg}Sig, ${indexArg}Sig) => {
|
|
561
|
+
${itemProxySetup}
|
|
562
|
+
const ${eachNodesVar} = [];
|
|
563
|
+
${eachSub.map(l => ' ' + l).join('\n')}
|
|
564
|
+
return ${eachNodesVar};
|
|
565
|
+
});`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function appendOrPush(parent, childVar, isRoot) {
|
|
570
|
+
if (isRoot) {
|
|
571
|
+
bodyLines.push(` ${parent}.push(${childVar});`);
|
|
572
|
+
} else {
|
|
573
|
+
bodyLines.push(` ${parent}.appendChild(${childVar});`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function getComponentNameFromFile(filePath) {
|
|
579
|
+
const base = filePath.split(/[/\\]/).pop().replace(/\.liq$/, '');
|
|
580
|
+
return base.replace(/[^a-zA-Z0-9_$]/g, '_') || 'Component';
|
|
581
|
+
}
|