@mulanjs/mulanjs 1.0.1-dev.20260227091247 → 1.0.1-dev.20260227135307
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/compiler/ast-parser.js +221 -0
- package/dist/compiler/compiler.js +1 -1
- package/dist/compiler/dom-compiler.js +5 -126
- package/dist/compiler/template-compiler.js +11 -238
- package/dist/types/ast-parser.d.ts +46 -0
- package/dist/types/compiler/ast-parser.d.ts +46 -0
- package/package.json +1 -1
- package/src/compiler/ast-parser.ts +258 -0
- package/src/compiler/compiler.ts +1 -1
- package/src/compiler/dom-compiler.ts +5 -133
- package/src/compiler/template-compiler.ts +11 -283
|
@@ -1,39 +1,7 @@
|
|
|
1
|
+
|
|
1
2
|
import { SFCDescriptor } from './sfc-parser';
|
|
2
3
|
import { ScriptCompileResult } from './script-compiler';
|
|
3
|
-
|
|
4
|
-
// --- AST Definitions ---
|
|
5
|
-
|
|
6
|
-
type NodeType = 'Element' | 'Text' | 'Interpolation';
|
|
7
|
-
|
|
8
|
-
type Node = ElementNode | TextNode | InterpolationNode;
|
|
9
|
-
|
|
10
|
-
interface BaseNode {
|
|
11
|
-
type: NodeType;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface ElementNode extends BaseNode {
|
|
15
|
-
type: 'Element';
|
|
16
|
-
tag: string;
|
|
17
|
-
props: Record<string, string>;
|
|
18
|
-
children: Node[];
|
|
19
|
-
directives: {
|
|
20
|
-
vFor?: { item: string; list: string };
|
|
21
|
-
vIf?: string;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface TextNode extends BaseNode {
|
|
26
|
-
type: 'Text';
|
|
27
|
-
content: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface InterpolationNode extends BaseNode {
|
|
31
|
-
type: 'Interpolation';
|
|
32
|
-
content: string; // The expression inside {{ }}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// --- Compile Function ---
|
|
36
|
-
|
|
4
|
+
import { parse, Node, ElementNode, TextNode, InterpolationNode } from './ast-parser';
|
|
37
5
|
import { SourceMapGenerator } from 'source-map';
|
|
38
6
|
|
|
39
7
|
export interface TemplateCompileResult {
|
|
@@ -43,14 +11,14 @@ export interface TemplateCompileResult {
|
|
|
43
11
|
}
|
|
44
12
|
|
|
45
13
|
export function compileTemplate(descriptor: SFCDescriptor, scriptResult: ScriptCompileResult, scopedId?: string): TemplateCompileResult {
|
|
46
|
-
console.log(`[MulanJS Compiler v1.0.1-dev.2] Compiling template for: ${descriptor.filename || 'anonymous'}`);
|
|
14
|
+
console.log(`[MulanJS Template Compiler v1.0.1-dev.2] Compiling template for: ${descriptor.filename || 'anonymous'}`);
|
|
47
15
|
const template = descriptor.template;
|
|
48
16
|
if (!template) return { code: 'function render() { return ""; }', errors: [] };
|
|
49
17
|
|
|
50
18
|
let html = template.content;
|
|
51
19
|
const errors: string[] = [];
|
|
52
20
|
|
|
53
|
-
// 1. Parsing Phase (HTML -> AST)
|
|
21
|
+
// 1. Parsing Phase (HTML -> AST) - Unified Parser
|
|
54
22
|
const ast = parse(html, errors);
|
|
55
23
|
|
|
56
24
|
// 2. Transform Phase (Scopes, Bindings)
|
|
@@ -74,44 +42,28 @@ export function compileTemplate(descriptor: SFCDescriptor, scriptResult: ScriptC
|
|
|
74
42
|
}`;
|
|
75
43
|
|
|
76
44
|
// 4. Source Map Generation
|
|
77
|
-
// We map the entire render function to the starting line of the template in the source file
|
|
78
45
|
let map = undefined;
|
|
79
46
|
if (descriptor.filename) {
|
|
80
47
|
const generator = new SourceMapGenerator({
|
|
81
|
-
file: descriptor.filename + '.template.js',
|
|
48
|
+
file: descriptor.filename + '.template.js',
|
|
82
49
|
});
|
|
83
50
|
|
|
84
|
-
// Find start line of template in original source
|
|
85
|
-
// descriptor.template.start is the index of content start
|
|
86
51
|
const sourceBefore = descriptor.source.substring(0, template.start);
|
|
87
52
|
const startLine = sourceBefore.split(/\r?\n/).length;
|
|
88
53
|
|
|
89
|
-
// Simple mapping: One-to-one mapping isn't easy without AST location tracking
|
|
90
|
-
// But we can map the 'return' statement to the template start
|
|
91
|
-
// The render function has about 12 lines of preamble.
|
|
92
|
-
// Let's just mapping the whole block to the start line for now.
|
|
93
|
-
// This ensures the file shows up in devtools.
|
|
94
54
|
generator.addMapping({
|
|
95
55
|
generated: { line: 1, column: 0 },
|
|
96
|
-
source: descriptor.filename,
|
|
56
|
+
source: descriptor.filename,
|
|
97
57
|
original: { line: startLine, column: 0 }
|
|
98
58
|
});
|
|
99
59
|
|
|
100
|
-
// Also map the return line (approx line 12)
|
|
101
60
|
generator.addMapping({
|
|
102
61
|
generated: { line: 12, column: 0 },
|
|
103
62
|
source: descriptor.filename,
|
|
104
63
|
original: { line: startLine, column: 0 }
|
|
105
64
|
});
|
|
106
65
|
|
|
107
|
-
// We must include the "source content" so DevTools can display it!
|
|
108
|
-
// IMPORTANT: We want the FULL .mujs content here, so it matches what Webpack sees?
|
|
109
|
-
// Actually, if we use the same filename as script-compiler, they might conflict or merge.
|
|
110
|
-
// Script compiler uses 'webpack:///...'
|
|
111
|
-
// Let's use the same convention.
|
|
112
|
-
// But we just use the filename here, compiler.ts will handle the final path adjustment if needed.
|
|
113
66
|
generator.setSourceContent(descriptor.filename, descriptor.source);
|
|
114
|
-
|
|
115
67
|
map = generator.toString();
|
|
116
68
|
}
|
|
117
69
|
|
|
@@ -122,182 +74,6 @@ export function compileTemplate(descriptor: SFCDescriptor, scriptResult: ScriptC
|
|
|
122
74
|
};
|
|
123
75
|
}
|
|
124
76
|
|
|
125
|
-
// --- Parser (Recursive Descent) ---
|
|
126
|
-
|
|
127
|
-
function parse(template: string, errors: string[]): ElementNode {
|
|
128
|
-
// Root Wrapper
|
|
129
|
-
const root: ElementNode = {
|
|
130
|
-
type: 'Element',
|
|
131
|
-
tag: 'fragment', // Virtual root
|
|
132
|
-
props: {},
|
|
133
|
-
children: [],
|
|
134
|
-
directives: {}
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const stack: ElementNode[] = [root];
|
|
138
|
-
let cursor = 0;
|
|
139
|
-
|
|
140
|
-
while (cursor < template.length) {
|
|
141
|
-
const char = template[cursor];
|
|
142
|
-
|
|
143
|
-
if (template.startsWith('<!--', cursor)) {
|
|
144
|
-
// Comment <!-- ... -->
|
|
145
|
-
const end = template.indexOf('-->', cursor);
|
|
146
|
-
if (end === -1) break;
|
|
147
|
-
// Just skip comments or keep them?
|
|
148
|
-
// Let's keep them as raw strings to preserve output structure if needed,
|
|
149
|
-
// but usually valid HTML comments shouldn't affect structure.
|
|
150
|
-
// For now, let's just append them to the previous text node or create a text node.
|
|
151
|
-
// Actually, simply ignoring them is safer for logic, but might remove user comments.
|
|
152
|
-
// Better: treat as Text so they are emitted as-is.
|
|
153
|
-
const content = template.slice(cursor, end + 3);
|
|
154
|
-
stack[stack.length - 1].children.push({ type: 'Text', content });
|
|
155
|
-
cursor = end + 3;
|
|
156
|
-
} else if (char === '<') {
|
|
157
|
-
// Tag
|
|
158
|
-
if (template[cursor + 1] === '/') {
|
|
159
|
-
// Closing Tag </tag>
|
|
160
|
-
const end = template.indexOf('>', cursor);
|
|
161
|
-
if (end === -1) break;
|
|
162
|
-
stack.pop();
|
|
163
|
-
cursor = end + 1;
|
|
164
|
-
} else {
|
|
165
|
-
// Opening Tag <tag ...>
|
|
166
|
-
const end = template.indexOf('>', cursor);
|
|
167
|
-
if (end === -1) break;
|
|
168
|
-
|
|
169
|
-
let tagContent = template.slice(cursor + 1, end);
|
|
170
|
-
const isSelfClosing = tagContent.endsWith('/') || ['img', 'br', 'input', 'hr'].includes(tagContent.split(' ')[0]);
|
|
171
|
-
|
|
172
|
-
if (tagContent.endsWith('/')) {
|
|
173
|
-
tagContent = tagContent.slice(0, -1).trim();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const { tag, props, directives } = parseTag(tagContent);
|
|
177
|
-
|
|
178
|
-
const element: ElementNode = { type: 'Element', tag, props, children: [], directives };
|
|
179
|
-
stack[stack.length - 1].children.push(element);
|
|
180
|
-
|
|
181
|
-
if (!isSelfClosing) {
|
|
182
|
-
stack.push(element);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
cursor = end + 1;
|
|
186
|
-
}
|
|
187
|
-
} else if (char === '{' && template[cursor + 1] === '{') {
|
|
188
|
-
// Interpolation {{ }}
|
|
189
|
-
const end = template.indexOf('}}', cursor);
|
|
190
|
-
if (end === -1) break; // formatting error
|
|
191
|
-
|
|
192
|
-
const content = template.slice(cursor + 2, end).trim();
|
|
193
|
-
stack[stack.length - 1].children.push({ type: 'Interpolation', content });
|
|
194
|
-
cursor = end + 2;
|
|
195
|
-
} else {
|
|
196
|
-
// Text
|
|
197
|
-
let nextTag = template.indexOf('<', cursor);
|
|
198
|
-
let nextInterp = template.indexOf('{{', cursor);
|
|
199
|
-
|
|
200
|
-
let end = template.length;
|
|
201
|
-
if (nextTag !== -1 && nextTag < end) end = nextTag;
|
|
202
|
-
if (nextInterp !== -1 && nextInterp < end) end = nextInterp;
|
|
203
|
-
|
|
204
|
-
const content = template.slice(cursor, end);
|
|
205
|
-
if (content) {
|
|
206
|
-
stack[stack.length - 1].children.push({ type: 'Text', content });
|
|
207
|
-
}
|
|
208
|
-
cursor = end;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return root;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function parseTag(content: string) {
|
|
216
|
-
// console.log('DEBUG: parseTag content:', content);
|
|
217
|
-
const parts = content.split(' ');
|
|
218
|
-
const tag = parts[0];
|
|
219
|
-
const props: Record<string, string> = {};
|
|
220
|
-
const directives: ElementNode['directives'] = {};
|
|
221
|
-
|
|
222
|
-
// Resume attribute parsing after tag
|
|
223
|
-
const attrStr = content.slice(tag.length).trim();
|
|
224
|
-
// console.log('DEBUG: attrStr:', attrStr);
|
|
225
|
-
|
|
226
|
-
let i = 0;
|
|
227
|
-
while (i < attrStr.length) {
|
|
228
|
-
// Skip spaces
|
|
229
|
-
if (/\s/.test(attrStr[i])) {
|
|
230
|
-
i++;
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Find key
|
|
235
|
-
const keyStart = i;
|
|
236
|
-
while (i < attrStr.length && !/\s|=/.test(attrStr[i])) {
|
|
237
|
-
i++;
|
|
238
|
-
}
|
|
239
|
-
const key = attrStr.slice(keyStart, i);
|
|
240
|
-
|
|
241
|
-
// Check for value
|
|
242
|
-
let value = 'true'; // Default for boolean attributes
|
|
243
|
-
|
|
244
|
-
// Skip potential spaces before '=' (e.g. class = "foo")
|
|
245
|
-
let peek = i;
|
|
246
|
-
while (peek < attrStr.length && /\s/.test(attrStr[peek])) peek++;
|
|
247
|
-
|
|
248
|
-
if (peek < attrStr.length && attrStr[peek] === '=') {
|
|
249
|
-
i = peek + 1; // Move past '='
|
|
250
|
-
|
|
251
|
-
// Skip spaces after '='
|
|
252
|
-
while (i < attrStr.length && /\s/.test(attrStr[i])) i++;
|
|
253
|
-
|
|
254
|
-
if (i < attrStr.length && (attrStr[i] === '"' || attrStr[i] === "'")) {
|
|
255
|
-
const quote = attrStr[i];
|
|
256
|
-
i++; // skip quote
|
|
257
|
-
const valStart = i;
|
|
258
|
-
while (i < attrStr.length && attrStr[i] !== quote) {
|
|
259
|
-
if (attrStr[i] === '\\' && attrStr[i + 1] === quote) {
|
|
260
|
-
i += 2;
|
|
261
|
-
} else {
|
|
262
|
-
i++;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
value = attrStr.slice(valStart, i);
|
|
266
|
-
i++; // skip closing quote
|
|
267
|
-
} else {
|
|
268
|
-
// unquoted value
|
|
269
|
-
const valStart = i;
|
|
270
|
-
while (i < attrStr.length && !/\s/.test(attrStr[i])) {
|
|
271
|
-
i++;
|
|
272
|
-
}
|
|
273
|
-
value = attrStr.slice(valStart, i);
|
|
274
|
-
}
|
|
275
|
-
} else {
|
|
276
|
-
// Boolean attribute, no value
|
|
277
|
-
// i remains at end of key
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Store
|
|
281
|
-
if (key === 'v-if' || key === 'mu-if') {
|
|
282
|
-
directives.vIf = value;
|
|
283
|
-
} else if (key === 'v-for' || key === 'mu-for') {
|
|
284
|
-
const parts = value.split(' in ');
|
|
285
|
-
if (parts.length < 2) {
|
|
286
|
-
console.warn(`[MulanJS Compiler] Warning: Invalid loop expression "${value}". Expected "item in list".`);
|
|
287
|
-
directives.vFor = { item: '_item', list: '[]' }; // Fallback with safe identifier
|
|
288
|
-
} else {
|
|
289
|
-
const item = parts[0];
|
|
290
|
-
const list = parts.slice(1).join(' in '); // Join rest in case list has 'in'
|
|
291
|
-
directives.vFor = { item: item.trim(), list: list.trim() };
|
|
292
|
-
}
|
|
293
|
-
} else if (key) {
|
|
294
|
-
props[key] = value;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return { tag, props, directives };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
77
|
// --- Transformer ---
|
|
302
78
|
|
|
303
79
|
const JS_KEYWORDS = new Set([
|
|
@@ -317,29 +93,24 @@ const GLOBALS = new Set([
|
|
|
317
93
|
function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: string, localScope: string[] = [], filename?: string) {
|
|
318
94
|
if (node.type === 'Element') {
|
|
319
95
|
const element = node as ElementNode;
|
|
320
|
-
// Scoped ID
|
|
321
96
|
if (scopedId && element.tag !== 'fragment') {
|
|
322
97
|
element.props[scopedId] = '';
|
|
323
98
|
}
|
|
324
99
|
|
|
325
|
-
// IRON FORTRESS: Detect mu-raw/v-raw for XSS bypass
|
|
326
100
|
const isRaw = 'mu-raw' in element.props || 'v-raw' in element.props;
|
|
327
101
|
if (isRaw) {
|
|
328
102
|
delete element.props['mu-raw'];
|
|
329
103
|
delete element.props['v-raw'];
|
|
330
104
|
}
|
|
331
105
|
|
|
332
|
-
// 0. Update Local Scope for Children (v-for)
|
|
333
106
|
const childScope = [...localScope];
|
|
334
107
|
if (element.directives.vFor) {
|
|
335
108
|
childScope.push(element.directives.vFor.item);
|
|
336
109
|
}
|
|
337
110
|
|
|
338
|
-
|
|
339
|
-
const propertyBindings: string[] = []; // Stores generated side-effect code (props and events)
|
|
111
|
+
const propertyBindings: string[] = [];
|
|
340
112
|
|
|
341
113
|
for (const key in element.props) {
|
|
342
|
-
// 0. Property Binding: .columns="${state.cols}"
|
|
343
114
|
if (key.startsWith('.')) {
|
|
344
115
|
if (!element.props['data-mu-id']) {
|
|
345
116
|
const id = 'mu_' + Math.random().toString(36).substr(2, 9);
|
|
@@ -359,7 +130,6 @@ function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: str
|
|
|
359
130
|
propertyBindings.push(`this._b('${id}', '${propName}', ${expr})`);
|
|
360
131
|
delete element.props[key];
|
|
361
132
|
}
|
|
362
|
-
// 1. Event Handlers: @click="count++", v-on:click="toggle", or onclick="increment()"
|
|
363
133
|
else if (key.startsWith('@') || key.startsWith('v-on:') || (key.startsWith('on') && key.length > 2)) {
|
|
364
134
|
if (!element.props['data-mu-id']) {
|
|
365
135
|
const id = 'mu_' + Math.random().toString(36).substr(2, 9);
|
|
@@ -370,13 +140,11 @@ function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: str
|
|
|
370
140
|
let eventName = '';
|
|
371
141
|
if (key.startsWith('@')) eventName = key.slice(1);
|
|
372
142
|
else if (key.startsWith('v-on:')) eventName = key.slice(5);
|
|
373
|
-
else eventName = key.slice(2);
|
|
143
|
+
else eventName = key.slice(2);
|
|
374
144
|
|
|
375
145
|
const rawHandler = element.props[key];
|
|
376
146
|
|
|
377
147
|
let bound = processBindings(rawHandler, scriptResult.bindings, localScope);
|
|
378
|
-
// Wrap in anonymous function if it looks like a statement or expression with side effects
|
|
379
|
-
// Simple heuristic: if it has parentheses and isn't a simple function reference
|
|
380
148
|
const finalHandler = (bound.includes('(') || bound.includes('=') || bound.includes('++'))
|
|
381
149
|
? `($event) => { ${bound} }`
|
|
382
150
|
: bound;
|
|
@@ -384,30 +152,22 @@ function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: str
|
|
|
384
152
|
propertyBindings.push(`this._e('${id}', '${eventName}', ${finalHandler})`);
|
|
385
153
|
delete element.props[key];
|
|
386
154
|
}
|
|
387
|
-
// 2. Standard Attributes Interpolation: class="{{ active }}"
|
|
388
155
|
else {
|
|
389
156
|
let rawValue = element.props[key];
|
|
390
|
-
// Check for {{ }} -> convert to ${ }
|
|
391
157
|
if (rawValue.includes('{{')) {
|
|
392
|
-
// Capture content and wrap in ${}, trimming whitespace
|
|
393
158
|
rawValue = rawValue.replace(/\{\{\s*(.*?)\s*\}\}/g, '${$1}');
|
|
394
159
|
}
|
|
395
160
|
|
|
396
|
-
// Check for ${ } -> process internal bindings
|
|
397
161
|
if (rawValue.includes('${')) {
|
|
398
|
-
// It's a template literal now
|
|
399
162
|
element.props[key] = rawValue.replace(/\$\{(.*?)\}/g, (_, expr) => {
|
|
400
163
|
const bound = processBindings(expr, scriptResult.bindings, localScope);
|
|
401
164
|
return '${_h(' + bound + ')}';
|
|
402
165
|
});
|
|
403
166
|
}
|
|
404
167
|
}
|
|
405
|
-
// ... (rest of attributes)
|
|
406
168
|
}
|
|
407
169
|
|
|
408
|
-
// Store side-effects on the node for the Generator to use
|
|
409
170
|
if (propertyBindings.length > 0) {
|
|
410
|
-
// We'll attach it to a temporary property on the AST node
|
|
411
171
|
(element as any)._propertySideEffects = propertyBindings;
|
|
412
172
|
}
|
|
413
173
|
|
|
@@ -422,14 +182,12 @@ function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: str
|
|
|
422
182
|
interpolation.content = processBindings(interpolation.content, scriptResult.bindings, localScope);
|
|
423
183
|
} else if (node.type === 'Text') {
|
|
424
184
|
const text = node as TextNode;
|
|
425
|
-
// Support native template literals: ${ expr }
|
|
426
185
|
if (text.content.includes('${')) {
|
|
427
186
|
const componentName = filename ? filename.split(/[/\\]/).pop()?.split('.')[0].replace(/\W/g, '') || 'App' : 'App';
|
|
428
187
|
const bindPrefix = `window['${componentName}'].`;
|
|
429
188
|
|
|
430
189
|
text.content = text.content.replace(/\$\{(.*?)\}/g, (_, expr) => {
|
|
431
190
|
let bound = processBindings(expr, scriptResult.bindings, localScope);
|
|
432
|
-
// If processBindings added 'this.', replace it with global access for maximum safety in string templates
|
|
433
191
|
bound = bound.replace(/this\./g, bindPrefix);
|
|
434
192
|
return '${_h(' + bound + ')}';
|
|
435
193
|
});
|
|
@@ -438,36 +196,23 @@ function transform(node: Node, scriptResult: ScriptCompileResult, scopedId?: str
|
|
|
438
196
|
}
|
|
439
197
|
|
|
440
198
|
function processBindings(exp: string, bindings: string[] | undefined, localScope: string[]) {
|
|
441
|
-
// Strategy:
|
|
442
|
-
// 1. Identifiers in 'bindings' (Setup API) -> prefix this.
|
|
443
|
-
// 2. Identifiers in 'localScope' (v-for) -> keep as is.
|
|
444
|
-
// 3. If 'bindings' is empty (Options API), prefix everything NOT in localScope/Keywords/Globals.
|
|
445
|
-
|
|
446
199
|
const isOptionsAPI = !bindings || bindings.length === 0;
|
|
447
200
|
const bindingSet = new Set(bindings || []);
|
|
448
201
|
|
|
449
|
-
// Regex for identifiers
|
|
450
202
|
return exp.replace(/\b([a-zA-Z_$][\w$]*)\b/g, (match, id, offset, str) => {
|
|
451
|
-
// 1. Skip if property access (dot before) (e.g. user.name -> user is checked, name is skipped)
|
|
452
203
|
if (offset > 0 && str[offset - 1] === '.') return match;
|
|
453
204
|
|
|
454
|
-
// 2. Skip object key (colon after) (e.g. { name: val } -> name skipped)
|
|
455
|
-
// Simple check: next char is ':'
|
|
456
205
|
let i = offset + match.length;
|
|
457
206
|
while (i < str.length && /\s/.test(str[i])) i++;
|
|
458
|
-
if (str[i] === ':' && str[i + 1] !== '=') return match;
|
|
207
|
+
if (str[i] === ':' && str[i + 1] !== '=') return match;
|
|
459
208
|
|
|
460
|
-
// 3. Skip Keywords / Globals / Local Vars
|
|
461
209
|
if (JS_KEYWORDS.has(id)) return match;
|
|
462
210
|
if (GLOBALS.has(id)) return match;
|
|
463
211
|
if (localScope.includes(id)) return match;
|
|
464
212
|
|
|
465
|
-
// 4. Decision
|
|
466
213
|
if (isOptionsAPI) {
|
|
467
|
-
// Options API: Prefix everything unknown
|
|
468
214
|
return `this.${id}`;
|
|
469
215
|
} else {
|
|
470
|
-
// Setup API: Only prefix explicit bindings
|
|
471
216
|
if (bindingSet.has(id)) {
|
|
472
217
|
return `this.${id}`;
|
|
473
218
|
}
|
|
@@ -482,66 +227,50 @@ function processBindings(exp: string, bindings: string[] | undefined, localScope
|
|
|
482
227
|
function generate(node: Node, bindings: string[], localScope: string[] = []): string {
|
|
483
228
|
if (node.type === 'Text') {
|
|
484
229
|
const text = node as TextNode;
|
|
485
|
-
// Escape backticks
|
|
486
230
|
return `\`${text.content.replace(/`/g, '\\`')}\``;
|
|
487
231
|
}
|
|
488
232
|
|
|
489
233
|
if (node.type === 'Interpolation') {
|
|
490
234
|
const interp = node as InterpolationNode;
|
|
491
235
|
const isRaw = (node as any).raw === true;
|
|
492
|
-
return `String(_h(${interp.content}, ${isRaw}))`;
|
|
236
|
+
return `String(_h(${interp.content}, ${isRaw}))`;
|
|
493
237
|
}
|
|
494
238
|
|
|
495
239
|
if (node.type === 'Element') {
|
|
496
240
|
const element = node as ElementNode;
|
|
497
241
|
|
|
498
|
-
// Update Local Scope for Children (v-for)
|
|
499
242
|
const childScope = [...localScope];
|
|
500
243
|
if (element.directives.vFor) {
|
|
501
244
|
childScope.push(element.directives.vFor.item);
|
|
502
245
|
}
|
|
503
246
|
|
|
504
247
|
if (element.tag === 'fragment') {
|
|
505
|
-
return element.children.map(c => generate(c, bindings, childScope)).join(' + ');
|
|
248
|
+
return element.children.map(c => generate(c, bindings, childScope)).join(' + ');
|
|
506
249
|
}
|
|
507
250
|
|
|
508
|
-
// Directives
|
|
509
251
|
if (element.directives.vIf) {
|
|
510
252
|
const condition = element.directives.vIf;
|
|
511
|
-
// Generate ternary: cond ? render() : ''
|
|
512
253
|
const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
|
|
513
254
|
const open = `<${element.tag}${genProps(element.props)}>`;
|
|
514
255
|
const close = `</${element.tag}>`;
|
|
515
|
-
// Apply bindings to v-if condition
|
|
516
|
-
// FIX: Wrap condition in _s() to unwrap signals (handling 'isOpen' vs 'isOpen.value')
|
|
517
256
|
return `(_s(${processBindings(condition, bindings, localScope)}) ? \`${open}\` + (${children}) + \`${close}\` : "")`;
|
|
518
257
|
}
|
|
519
258
|
|
|
520
259
|
if (element.directives.vFor) {
|
|
521
260
|
const { item, list } = element.directives.vFor;
|
|
522
|
-
// list.map(item => render).join('')
|
|
523
|
-
|
|
524
|
-
// Apply bindings to the list expression
|
|
525
|
-
// FIX: Wrap list in _s() to ensure we map over the Array, not the Signal Object
|
|
526
261
|
const boundList = `_s(${processBindings(list, bindings, localScope)})`;
|
|
527
|
-
|
|
528
262
|
const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
|
|
529
263
|
const open = `<${element.tag}${genProps(element.props)}>`;
|
|
530
264
|
const close = `</${element.tag}>`;
|
|
531
|
-
|
|
532
|
-
// Add safety check: ensure boundList is an Array before mapping
|
|
533
265
|
return `(Array.isArray(${boundList}) ? ${boundList}.map(${item} => \`${open}\` + (${children}) + \`${close}\`).join('') : "")`;
|
|
534
266
|
}
|
|
535
267
|
|
|
536
|
-
// Standard Element
|
|
537
268
|
const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
|
|
538
269
|
const open = `<${element.tag}${genProps(element.props)}>`;
|
|
539
270
|
const close = `</${element.tag}>`;
|
|
540
271
|
|
|
541
272
|
let code = `\`${open}\` + (${children}) + \`${close}\``;
|
|
542
273
|
|
|
543
|
-
// Inject Property Binding Side Effects using Comma Operator
|
|
544
|
-
// (this._b(..), this._b(..), `html`)
|
|
545
274
|
if ((element as any)._propertySideEffects) {
|
|
546
275
|
const effects = (element as any)._propertySideEffects.join(', ');
|
|
547
276
|
code = `(${effects}, ${code})`;
|
|
@@ -558,7 +287,6 @@ function genProps(props: Record<string, string>) {
|
|
|
558
287
|
for (const key in props) {
|
|
559
288
|
str += ` ${key}`;
|
|
560
289
|
if (props[key] !== '') {
|
|
561
|
-
// Escape double quotes in value
|
|
562
290
|
const escaped = props[key].replace(/"/g, '"');
|
|
563
291
|
str += `="${escaped}"`;
|
|
564
292
|
}
|