@mulanjs/mulanjs 1.0.1-dev.20260226191839 → 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/core/component.js +4 -0
- package/dist/mulan.esm.js +4 -0
- package/dist/mulan.esm.js.map +1 -1
- package/dist/mulan.js +4 -0
- package/dist/mulan.js.map +1 -1
- 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,17 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.compileTemplate = void 0;
|
|
4
|
-
|
|
4
|
+
const ast_parser_1 = require("./ast-parser");
|
|
5
5
|
const source_map_1 = require("source-map");
|
|
6
6
|
function compileTemplate(descriptor, scriptResult, scopedId) {
|
|
7
|
-
console.log(`[MulanJS Compiler v1.0.1-dev.2] Compiling template for: ${descriptor.filename || 'anonymous'}`);
|
|
7
|
+
console.log(`[MulanJS Template Compiler v1.0.1-dev.2] Compiling template for: ${descriptor.filename || 'anonymous'}`);
|
|
8
8
|
const template = descriptor.template;
|
|
9
9
|
if (!template)
|
|
10
10
|
return { code: 'function render() { return ""; }', errors: [] };
|
|
11
11
|
let html = template.content;
|
|
12
12
|
const errors = [];
|
|
13
|
-
// 1. Parsing Phase (HTML -> AST)
|
|
14
|
-
const ast = parse(html, errors);
|
|
13
|
+
// 1. Parsing Phase (HTML -> AST) - Unified Parser
|
|
14
|
+
const ast = (0, ast_parser_1.parse)(html, errors);
|
|
15
15
|
// 2. Transform Phase (Scopes, Bindings)
|
|
16
16
|
transform(ast, scriptResult, scopedId, [], descriptor.filename);
|
|
17
17
|
// 3. Codegen Phase (AST -> JS Function)
|
|
@@ -30,38 +30,23 @@ function compileTemplate(descriptor, scriptResult, scopedId) {
|
|
|
30
30
|
return ${code};
|
|
31
31
|
}`;
|
|
32
32
|
// 4. Source Map Generation
|
|
33
|
-
// We map the entire render function to the starting line of the template in the source file
|
|
34
33
|
let map = undefined;
|
|
35
34
|
if (descriptor.filename) {
|
|
36
35
|
const generator = new source_map_1.SourceMapGenerator({
|
|
37
|
-
file: descriptor.filename + '.template.js',
|
|
36
|
+
file: descriptor.filename + '.template.js',
|
|
38
37
|
});
|
|
39
|
-
// Find start line of template in original source
|
|
40
|
-
// descriptor.template.start is the index of content start
|
|
41
38
|
const sourceBefore = descriptor.source.substring(0, template.start);
|
|
42
39
|
const startLine = sourceBefore.split(/\r?\n/).length;
|
|
43
|
-
// Simple mapping: One-to-one mapping isn't easy without AST location tracking
|
|
44
|
-
// But we can map the 'return' statement to the template start
|
|
45
|
-
// The render function has about 12 lines of preamble.
|
|
46
|
-
// Let's just mapping the whole block to the start line for now.
|
|
47
|
-
// This ensures the file shows up in devtools.
|
|
48
40
|
generator.addMapping({
|
|
49
41
|
generated: { line: 1, column: 0 },
|
|
50
|
-
source: descriptor.filename,
|
|
42
|
+
source: descriptor.filename,
|
|
51
43
|
original: { line: startLine, column: 0 }
|
|
52
44
|
});
|
|
53
|
-
// Also map the return line (approx line 12)
|
|
54
45
|
generator.addMapping({
|
|
55
46
|
generated: { line: 12, column: 0 },
|
|
56
47
|
source: descriptor.filename,
|
|
57
48
|
original: { line: startLine, column: 0 }
|
|
58
49
|
});
|
|
59
|
-
// We must include the "source content" so DevTools can display it!
|
|
60
|
-
// IMPORTANT: We want the FULL .mujs content here, so it matches what Webpack sees?
|
|
61
|
-
// Actually, if we use the same filename as script-compiler, they might conflict or merge.
|
|
62
|
-
// Script compiler uses 'webpack:///...'
|
|
63
|
-
// Let's use the same convention.
|
|
64
|
-
// But we just use the filename here, compiler.ts will handle the final path adjustment if needed.
|
|
65
50
|
generator.setSourceContent(descriptor.filename, descriptor.source);
|
|
66
51
|
map = generator.toString();
|
|
67
52
|
}
|
|
@@ -72,174 +57,6 @@ function compileTemplate(descriptor, scriptResult, scopedId) {
|
|
|
72
57
|
};
|
|
73
58
|
}
|
|
74
59
|
exports.compileTemplate = compileTemplate;
|
|
75
|
-
// --- Parser (Recursive Descent) ---
|
|
76
|
-
function parse(template, errors) {
|
|
77
|
-
// Root Wrapper
|
|
78
|
-
const root = {
|
|
79
|
-
type: 'Element',
|
|
80
|
-
tag: 'fragment', // Virtual root
|
|
81
|
-
props: {},
|
|
82
|
-
children: [],
|
|
83
|
-
directives: {}
|
|
84
|
-
};
|
|
85
|
-
const stack = [root];
|
|
86
|
-
let cursor = 0;
|
|
87
|
-
while (cursor < template.length) {
|
|
88
|
-
const char = template[cursor];
|
|
89
|
-
if (template.startsWith('<!--', cursor)) {
|
|
90
|
-
// Comment <!-- ... -->
|
|
91
|
-
const end = template.indexOf('-->', cursor);
|
|
92
|
-
if (end === -1)
|
|
93
|
-
break;
|
|
94
|
-
// Just skip comments or keep them?
|
|
95
|
-
// Let's keep them as raw strings to preserve output structure if needed,
|
|
96
|
-
// but usually valid HTML comments shouldn't affect structure.
|
|
97
|
-
// For now, let's just append them to the previous text node or create a text node.
|
|
98
|
-
// Actually, simply ignoring them is safer for logic, but might remove user comments.
|
|
99
|
-
// Better: treat as Text so they are emitted as-is.
|
|
100
|
-
const content = template.slice(cursor, end + 3);
|
|
101
|
-
stack[stack.length - 1].children.push({ type: 'Text', content });
|
|
102
|
-
cursor = end + 3;
|
|
103
|
-
}
|
|
104
|
-
else if (char === '<') {
|
|
105
|
-
// Tag
|
|
106
|
-
if (template[cursor + 1] === '/') {
|
|
107
|
-
// Closing Tag </tag>
|
|
108
|
-
const end = template.indexOf('>', cursor);
|
|
109
|
-
if (end === -1)
|
|
110
|
-
break;
|
|
111
|
-
stack.pop();
|
|
112
|
-
cursor = end + 1;
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
// Opening Tag <tag ...>
|
|
116
|
-
const end = template.indexOf('>', cursor);
|
|
117
|
-
if (end === -1)
|
|
118
|
-
break;
|
|
119
|
-
let tagContent = template.slice(cursor + 1, end);
|
|
120
|
-
const isSelfClosing = tagContent.endsWith('/') || ['img', 'br', 'input', 'hr'].includes(tagContent.split(' ')[0]);
|
|
121
|
-
if (tagContent.endsWith('/')) {
|
|
122
|
-
tagContent = tagContent.slice(0, -1).trim();
|
|
123
|
-
}
|
|
124
|
-
const { tag, props, directives } = parseTag(tagContent);
|
|
125
|
-
const element = { type: 'Element', tag, props, children: [], directives };
|
|
126
|
-
stack[stack.length - 1].children.push(element);
|
|
127
|
-
if (!isSelfClosing) {
|
|
128
|
-
stack.push(element);
|
|
129
|
-
}
|
|
130
|
-
cursor = end + 1;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
else if (char === '{' && template[cursor + 1] === '{') {
|
|
134
|
-
// Interpolation {{ }}
|
|
135
|
-
const end = template.indexOf('}}', cursor);
|
|
136
|
-
if (end === -1)
|
|
137
|
-
break; // formatting error
|
|
138
|
-
const content = template.slice(cursor + 2, end).trim();
|
|
139
|
-
stack[stack.length - 1].children.push({ type: 'Interpolation', content });
|
|
140
|
-
cursor = end + 2;
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
// Text
|
|
144
|
-
let nextTag = template.indexOf('<', cursor);
|
|
145
|
-
let nextInterp = template.indexOf('{{', cursor);
|
|
146
|
-
let end = template.length;
|
|
147
|
-
if (nextTag !== -1 && nextTag < end)
|
|
148
|
-
end = nextTag;
|
|
149
|
-
if (nextInterp !== -1 && nextInterp < end)
|
|
150
|
-
end = nextInterp;
|
|
151
|
-
const content = template.slice(cursor, end);
|
|
152
|
-
if (content) {
|
|
153
|
-
stack[stack.length - 1].children.push({ type: 'Text', content });
|
|
154
|
-
}
|
|
155
|
-
cursor = end;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return root;
|
|
159
|
-
}
|
|
160
|
-
function parseTag(content) {
|
|
161
|
-
// console.log('DEBUG: parseTag content:', content);
|
|
162
|
-
const parts = content.split(' ');
|
|
163
|
-
const tag = parts[0];
|
|
164
|
-
const props = {};
|
|
165
|
-
const directives = {};
|
|
166
|
-
// Resume attribute parsing after tag
|
|
167
|
-
const attrStr = content.slice(tag.length).trim();
|
|
168
|
-
// console.log('DEBUG: attrStr:', attrStr);
|
|
169
|
-
let i = 0;
|
|
170
|
-
while (i < attrStr.length) {
|
|
171
|
-
// Skip spaces
|
|
172
|
-
if (/\s/.test(attrStr[i])) {
|
|
173
|
-
i++;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
// Find key
|
|
177
|
-
const keyStart = i;
|
|
178
|
-
while (i < attrStr.length && !/\s|=/.test(attrStr[i])) {
|
|
179
|
-
i++;
|
|
180
|
-
}
|
|
181
|
-
const key = attrStr.slice(keyStart, i);
|
|
182
|
-
// Check for value
|
|
183
|
-
let value = 'true'; // Default for boolean attributes
|
|
184
|
-
// Skip potential spaces before '=' (e.g. class = "foo")
|
|
185
|
-
let peek = i;
|
|
186
|
-
while (peek < attrStr.length && /\s/.test(attrStr[peek]))
|
|
187
|
-
peek++;
|
|
188
|
-
if (peek < attrStr.length && attrStr[peek] === '=') {
|
|
189
|
-
i = peek + 1; // Move past '='
|
|
190
|
-
// Skip spaces after '='
|
|
191
|
-
while (i < attrStr.length && /\s/.test(attrStr[i]))
|
|
192
|
-
i++;
|
|
193
|
-
if (i < attrStr.length && (attrStr[i] === '"' || attrStr[i] === "'")) {
|
|
194
|
-
const quote = attrStr[i];
|
|
195
|
-
i++; // skip quote
|
|
196
|
-
const valStart = i;
|
|
197
|
-
while (i < attrStr.length && attrStr[i] !== quote) {
|
|
198
|
-
if (attrStr[i] === '\\' && attrStr[i + 1] === quote) {
|
|
199
|
-
i += 2;
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
i++;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
value = attrStr.slice(valStart, i);
|
|
206
|
-
i++; // skip closing quote
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
// unquoted value
|
|
210
|
-
const valStart = i;
|
|
211
|
-
while (i < attrStr.length && !/\s/.test(attrStr[i])) {
|
|
212
|
-
i++;
|
|
213
|
-
}
|
|
214
|
-
value = attrStr.slice(valStart, i);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
// Boolean attribute, no value
|
|
219
|
-
// i remains at end of key
|
|
220
|
-
}
|
|
221
|
-
// Store
|
|
222
|
-
if (key === 'v-if' || key === 'mu-if') {
|
|
223
|
-
directives.vIf = value;
|
|
224
|
-
}
|
|
225
|
-
else if (key === 'v-for' || key === 'mu-for') {
|
|
226
|
-
const parts = value.split(' in ');
|
|
227
|
-
if (parts.length < 2) {
|
|
228
|
-
console.warn(`[MulanJS Compiler] Warning: Invalid loop expression "${value}". Expected "item in list".`);
|
|
229
|
-
directives.vFor = { item: '_item', list: '[]' }; // Fallback with safe identifier
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
const item = parts[0];
|
|
233
|
-
const list = parts.slice(1).join(' in '); // Join rest in case list has 'in'
|
|
234
|
-
directives.vFor = { item: item.trim(), list: list.trim() };
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
else if (key) {
|
|
238
|
-
props[key] = value;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return { tag, props, directives };
|
|
242
|
-
}
|
|
243
60
|
// --- Transformer ---
|
|
244
61
|
const JS_KEYWORDS = new Set([
|
|
245
62
|
'true', 'false', 'null', 'undefined', 'this', 'window',
|
|
@@ -257,25 +74,20 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
|
|
|
257
74
|
var _a;
|
|
258
75
|
if (node.type === 'Element') {
|
|
259
76
|
const element = node;
|
|
260
|
-
// Scoped ID
|
|
261
77
|
if (scopedId && element.tag !== 'fragment') {
|
|
262
78
|
element.props[scopedId] = '';
|
|
263
79
|
}
|
|
264
|
-
// IRON FORTRESS: Detect mu-raw/v-raw for XSS bypass
|
|
265
80
|
const isRaw = 'mu-raw' in element.props || 'v-raw' in element.props;
|
|
266
81
|
if (isRaw) {
|
|
267
82
|
delete element.props['mu-raw'];
|
|
268
83
|
delete element.props['v-raw'];
|
|
269
84
|
}
|
|
270
|
-
// 0. Update Local Scope for Children (v-for)
|
|
271
85
|
const childScope = [...localScope];
|
|
272
86
|
if (element.directives.vFor) {
|
|
273
87
|
childScope.push(element.directives.vFor.item);
|
|
274
88
|
}
|
|
275
|
-
|
|
276
|
-
const propertyBindings = []; // Stores generated side-effect code (props and events)
|
|
89
|
+
const propertyBindings = [];
|
|
277
90
|
for (const key in element.props) {
|
|
278
|
-
// 0. Property Binding: .columns="${state.cols}"
|
|
279
91
|
if (key.startsWith('.')) {
|
|
280
92
|
if (!element.props['data-mu-id']) {
|
|
281
93
|
const id = 'mu_' + Math.random().toString(36).substr(2, 9);
|
|
@@ -295,7 +107,6 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
|
|
|
295
107
|
propertyBindings.push(`this._b('${id}', '${propName}', ${expr})`);
|
|
296
108
|
delete element.props[key];
|
|
297
109
|
}
|
|
298
|
-
// 1. Event Handlers: @click="count++", v-on:click="toggle", or onclick="increment()"
|
|
299
110
|
else if (key.startsWith('@') || key.startsWith('v-on:') || (key.startsWith('on') && key.length > 2)) {
|
|
300
111
|
if (!element.props['data-mu-id']) {
|
|
301
112
|
const id = 'mu_' + Math.random().toString(36).substr(2, 9);
|
|
@@ -308,39 +119,29 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
|
|
|
308
119
|
else if (key.startsWith('v-on:'))
|
|
309
120
|
eventName = key.slice(5);
|
|
310
121
|
else
|
|
311
|
-
eventName = key.slice(2);
|
|
122
|
+
eventName = key.slice(2);
|
|
312
123
|
const rawHandler = element.props[key];
|
|
313
124
|
let bound = processBindings(rawHandler, scriptResult.bindings, localScope);
|
|
314
|
-
// Wrap in anonymous function if it looks like a statement or expression with side effects
|
|
315
|
-
// Simple heuristic: if it has parentheses and isn't a simple function reference
|
|
316
125
|
const finalHandler = (bound.includes('(') || bound.includes('=') || bound.includes('++'))
|
|
317
126
|
? `($event) => { ${bound} }`
|
|
318
127
|
: bound;
|
|
319
128
|
propertyBindings.push(`this._e('${id}', '${eventName}', ${finalHandler})`);
|
|
320
129
|
delete element.props[key];
|
|
321
130
|
}
|
|
322
|
-
// 2. Standard Attributes Interpolation: class="{{ active }}"
|
|
323
131
|
else {
|
|
324
132
|
let rawValue = element.props[key];
|
|
325
|
-
// Check for {{ }} -> convert to ${ }
|
|
326
133
|
if (rawValue.includes('{{')) {
|
|
327
|
-
// Capture content and wrap in ${}, trimming whitespace
|
|
328
134
|
rawValue = rawValue.replace(/\{\{\s*(.*?)\s*\}\}/g, '${$1}');
|
|
329
135
|
}
|
|
330
|
-
// Check for ${ } -> process internal bindings
|
|
331
136
|
if (rawValue.includes('${')) {
|
|
332
|
-
// It's a template literal now
|
|
333
137
|
element.props[key] = rawValue.replace(/\$\{(.*?)\}/g, (_, expr) => {
|
|
334
138
|
const bound = processBindings(expr, scriptResult.bindings, localScope);
|
|
335
139
|
return '${_h(' + bound + ')}';
|
|
336
140
|
});
|
|
337
141
|
}
|
|
338
142
|
}
|
|
339
|
-
// ... (rest of attributes)
|
|
340
143
|
}
|
|
341
|
-
// Store side-effects on the node for the Generator to use
|
|
342
144
|
if (propertyBindings.length > 0) {
|
|
343
|
-
// We'll attach it to a temporary property on the AST node
|
|
344
145
|
element._propertySideEffects = propertyBindings;
|
|
345
146
|
}
|
|
346
147
|
element.children.forEach(child => {
|
|
@@ -356,13 +157,11 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
|
|
|
356
157
|
}
|
|
357
158
|
else if (node.type === 'Text') {
|
|
358
159
|
const text = node;
|
|
359
|
-
// Support native template literals: ${ expr }
|
|
360
160
|
if (text.content.includes('${')) {
|
|
361
161
|
const componentName = filename ? ((_a = filename.split(/[/\\]/).pop()) === null || _a === void 0 ? void 0 : _a.split('.')[0].replace(/\W/g, '')) || 'App' : 'App';
|
|
362
162
|
const bindPrefix = `window['${componentName}'].`;
|
|
363
163
|
text.content = text.content.replace(/\$\{(.*?)\}/g, (_, expr) => {
|
|
364
164
|
let bound = processBindings(expr, scriptResult.bindings, localScope);
|
|
365
|
-
// If processBindings added 'this.', replace it with global access for maximum safety in string templates
|
|
366
165
|
bound = bound.replace(/this\./g, bindPrefix);
|
|
367
166
|
return '${_h(' + bound + ')}';
|
|
368
167
|
});
|
|
@@ -370,38 +169,26 @@ function transform(node, scriptResult, scopedId, localScope = [], filename) {
|
|
|
370
169
|
}
|
|
371
170
|
}
|
|
372
171
|
function processBindings(exp, bindings, localScope) {
|
|
373
|
-
// Strategy:
|
|
374
|
-
// 1. Identifiers in 'bindings' (Setup API) -> prefix this.
|
|
375
|
-
// 2. Identifiers in 'localScope' (v-for) -> keep as is.
|
|
376
|
-
// 3. If 'bindings' is empty (Options API), prefix everything NOT in localScope/Keywords/Globals.
|
|
377
172
|
const isOptionsAPI = !bindings || bindings.length === 0;
|
|
378
173
|
const bindingSet = new Set(bindings || []);
|
|
379
|
-
// Regex for identifiers
|
|
380
174
|
return exp.replace(/\b([a-zA-Z_$][\w$]*)\b/g, (match, id, offset, str) => {
|
|
381
|
-
// 1. Skip if property access (dot before) (e.g. user.name -> user is checked, name is skipped)
|
|
382
175
|
if (offset > 0 && str[offset - 1] === '.')
|
|
383
176
|
return match;
|
|
384
|
-
// 2. Skip object key (colon after) (e.g. { name: val } -> name skipped)
|
|
385
|
-
// Simple check: next char is ':'
|
|
386
177
|
let i = offset + match.length;
|
|
387
178
|
while (i < str.length && /\s/.test(str[i]))
|
|
388
179
|
i++;
|
|
389
180
|
if (str[i] === ':' && str[i + 1] !== '=')
|
|
390
|
-
return match;
|
|
391
|
-
// 3. Skip Keywords / Globals / Local Vars
|
|
181
|
+
return match;
|
|
392
182
|
if (JS_KEYWORDS.has(id))
|
|
393
183
|
return match;
|
|
394
184
|
if (GLOBALS.has(id))
|
|
395
185
|
return match;
|
|
396
186
|
if (localScope.includes(id))
|
|
397
187
|
return match;
|
|
398
|
-
// 4. Decision
|
|
399
188
|
if (isOptionsAPI) {
|
|
400
|
-
// Options API: Prefix everything unknown
|
|
401
189
|
return `this.${id}`;
|
|
402
190
|
}
|
|
403
191
|
else {
|
|
404
|
-
// Setup API: Only prefix explicit bindings
|
|
405
192
|
if (bindingSet.has(id)) {
|
|
406
193
|
return `this.${id}`;
|
|
407
194
|
}
|
|
@@ -413,54 +200,41 @@ function processBindings(exp, bindings, localScope) {
|
|
|
413
200
|
function generate(node, bindings, localScope = []) {
|
|
414
201
|
if (node.type === 'Text') {
|
|
415
202
|
const text = node;
|
|
416
|
-
// Escape backticks
|
|
417
203
|
return `\`${text.content.replace(/`/g, '\\`')}\``;
|
|
418
204
|
}
|
|
419
205
|
if (node.type === 'Interpolation') {
|
|
420
206
|
const interp = node;
|
|
421
207
|
const isRaw = node.raw === true;
|
|
422
|
-
return `String(_h(${interp.content}, ${isRaw}))`;
|
|
208
|
+
return `String(_h(${interp.content}, ${isRaw}))`;
|
|
423
209
|
}
|
|
424
210
|
if (node.type === 'Element') {
|
|
425
211
|
const element = node;
|
|
426
|
-
// Update Local Scope for Children (v-for)
|
|
427
212
|
const childScope = [...localScope];
|
|
428
213
|
if (element.directives.vFor) {
|
|
429
214
|
childScope.push(element.directives.vFor.item);
|
|
430
215
|
}
|
|
431
216
|
if (element.tag === 'fragment') {
|
|
432
|
-
return element.children.map(c => generate(c, bindings, childScope)).join(' + ');
|
|
217
|
+
return element.children.map(c => generate(c, bindings, childScope)).join(' + ');
|
|
433
218
|
}
|
|
434
|
-
// Directives
|
|
435
219
|
if (element.directives.vIf) {
|
|
436
220
|
const condition = element.directives.vIf;
|
|
437
|
-
// Generate ternary: cond ? render() : ''
|
|
438
221
|
const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
|
|
439
222
|
const open = `<${element.tag}${genProps(element.props)}>`;
|
|
440
223
|
const close = `</${element.tag}>`;
|
|
441
|
-
// Apply bindings to v-if condition
|
|
442
|
-
// FIX: Wrap condition in _s() to unwrap signals (handling 'isOpen' vs 'isOpen.value')
|
|
443
224
|
return `(_s(${processBindings(condition, bindings, localScope)}) ? \`${open}\` + (${children}) + \`${close}\` : "")`;
|
|
444
225
|
}
|
|
445
226
|
if (element.directives.vFor) {
|
|
446
227
|
const { item, list } = element.directives.vFor;
|
|
447
|
-
// list.map(item => render).join('')
|
|
448
|
-
// Apply bindings to the list expression
|
|
449
|
-
// FIX: Wrap list in _s() to ensure we map over the Array, not the Signal Object
|
|
450
228
|
const boundList = `_s(${processBindings(list, bindings, localScope)})`;
|
|
451
229
|
const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
|
|
452
230
|
const open = `<${element.tag}${genProps(element.props)}>`;
|
|
453
231
|
const close = `</${element.tag}>`;
|
|
454
|
-
// Add safety check: ensure boundList is an Array before mapping
|
|
455
232
|
return `(Array.isArray(${boundList}) ? ${boundList}.map(${item} => \`${open}\` + (${children}) + \`${close}\`).join('') : "")`;
|
|
456
233
|
}
|
|
457
|
-
// Standard Element
|
|
458
234
|
const children = element.children.map(c => generate(c, bindings, childScope)).join(' + ') || '""';
|
|
459
235
|
const open = `<${element.tag}${genProps(element.props)}>`;
|
|
460
236
|
const close = `</${element.tag}>`;
|
|
461
237
|
let code = `\`${open}\` + (${children}) + \`${close}\``;
|
|
462
|
-
// Inject Property Binding Side Effects using Comma Operator
|
|
463
|
-
// (this._b(..), this._b(..), `html`)
|
|
464
238
|
if (element._propertySideEffects) {
|
|
465
239
|
const effects = element._propertySideEffects.join(', ');
|
|
466
240
|
code = `(${effects}, ${code})`;
|
|
@@ -474,7 +248,6 @@ function genProps(props) {
|
|
|
474
248
|
for (const key in props) {
|
|
475
249
|
str += ` ${key}`;
|
|
476
250
|
if (props[key] !== '') {
|
|
477
|
-
// Escape double quotes in value
|
|
478
251
|
const escaped = props[key].replace(/"/g, '"');
|
|
479
252
|
str += `="${escaped}"`;
|
|
480
253
|
}
|
package/dist/core/component.js
CHANGED
|
@@ -145,6 +145,8 @@ export class MuComponent {
|
|
|
145
145
|
const cache = this._listCaches.get(listId);
|
|
146
146
|
const newKeys = new Set();
|
|
147
147
|
const parent = anchorToken.parentNode;
|
|
148
|
+
if (!parent)
|
|
149
|
+
return;
|
|
148
150
|
// 1. Array Iteration: Match against cache or Create
|
|
149
151
|
// Use a DocumentFragment to batch new node insertions natively
|
|
150
152
|
const collectorFrag = document.createDocumentFragment();
|
|
@@ -247,6 +249,8 @@ export class MuComponent {
|
|
|
247
249
|
}
|
|
248
250
|
cached.isMounted = true;
|
|
249
251
|
const parent = anchorToken.parentNode;
|
|
252
|
+
if (!parent)
|
|
253
|
+
return;
|
|
250
254
|
const targetSibling = anchorToken.nextSibling;
|
|
251
255
|
nodesToMount.forEach(node => {
|
|
252
256
|
parent.insertBefore(node, targetSibling);
|
package/dist/mulan.esm.js
CHANGED
|
@@ -911,6 +911,8 @@ class MuComponent {
|
|
|
911
911
|
const cache = this._listCaches.get(listId);
|
|
912
912
|
const newKeys = new Set();
|
|
913
913
|
const parent = anchorToken.parentNode;
|
|
914
|
+
if (!parent)
|
|
915
|
+
return;
|
|
914
916
|
// 1. Array Iteration: Match against cache or Create
|
|
915
917
|
// Use a DocumentFragment to batch new node insertions natively
|
|
916
918
|
const collectorFrag = document.createDocumentFragment();
|
|
@@ -1013,6 +1015,8 @@ class MuComponent {
|
|
|
1013
1015
|
}
|
|
1014
1016
|
cached.isMounted = true;
|
|
1015
1017
|
const parent = anchorToken.parentNode;
|
|
1018
|
+
if (!parent)
|
|
1019
|
+
return;
|
|
1016
1020
|
const targetSibling = anchorToken.nextSibling;
|
|
1017
1021
|
nodesToMount.forEach(node => {
|
|
1018
1022
|
parent.insertBefore(node, targetSibling);
|