@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.
@@ -0,0 +1,258 @@
1
+
2
+ // --- AST Definitions ---
3
+
4
+ export type NodeType = 'Element' | 'Text' | 'Interpolation';
5
+
6
+ export type Node = ElementNode | TextNode | InterpolationNode;
7
+
8
+ export interface BaseNode {
9
+ type: NodeType;
10
+ }
11
+
12
+ export interface ElementNode extends BaseNode {
13
+ type: 'Element';
14
+ tag: string;
15
+ props: Record<string, string>;
16
+ children: Node[];
17
+ directives: {
18
+ vFor?: { item: string; list: string };
19
+ vIf?: string;
20
+ };
21
+ }
22
+
23
+ export interface TextNode extends BaseNode {
24
+ type: 'Text';
25
+ content: string;
26
+ }
27
+
28
+ export interface InterpolationNode extends BaseNode {
29
+ type: 'Interpolation';
30
+ content: string; // The expression inside {{ }}
31
+ }
32
+
33
+ /**
34
+ * Decodes standard HTML entities.
35
+ */
36
+ export function decodeEntities(str: string): string {
37
+ return str
38
+ .replace(/&lt;/g, '<')
39
+ .replace(/&gt;/g, '>')
40
+ .replace(/&amp;/g, '&')
41
+ .replace(/&quot;/g, '"')
42
+ .replace(/&#039;/g, "'")
43
+ .replace(/&nbsp;/g, '\u00A0');
44
+ }
45
+
46
+ /**
47
+ * Unified MulanJS Template Parser
48
+ * Handles HTML tags, {{ }} interpolation, and is robust against raw < symbols in text.
49
+ */
50
+ export function parse(template: string, errors: string[]): ElementNode {
51
+ const root: ElementNode = {
52
+ type: 'Element',
53
+ tag: 'fragment',
54
+ props: {},
55
+ children: [],
56
+ directives: {}
57
+ };
58
+
59
+ const stack: ElementNode[] = [root];
60
+ let cursor = 0;
61
+
62
+ while (cursor < template.length) {
63
+ const char = template[cursor];
64
+
65
+ // 1. Comments
66
+ if (template.startsWith('<!--', cursor)) {
67
+ const end = template.indexOf('-->', cursor);
68
+ if (end === -1) {
69
+ errors.push('Unclosed HTML comment.');
70
+ break;
71
+ }
72
+ cursor = end + 3;
73
+ }
74
+ // 2. Tags (with lookahead check for robustness)
75
+ else if (char === '<' && /[\/a-zA-Z!]/.test(template[cursor + 1])) {
76
+ if (template[cursor + 1] === '/') {
77
+ // Closing Tag
78
+ const end = template.indexOf('>', cursor);
79
+ if (end === -1) {
80
+ errors.push('Unclosed closing tag.');
81
+ break;
82
+ }
83
+ const tagName = template.slice(cursor + 2, end).trim();
84
+ // Find matching element in stack to pop
85
+ if (stack.length > 1 && stack[stack.length - 1].tag === tagName) {
86
+ stack.pop();
87
+ } else {
88
+ errors.push(`Mismatched closing tag </${tagName}>.`);
89
+ }
90
+ cursor = end + 1;
91
+ } else {
92
+ // Opening Tag
93
+ const end = template.indexOf('>', cursor);
94
+ if (end === -1) {
95
+ errors.push('Unclosed opening tag.');
96
+ break;
97
+ }
98
+
99
+ let tagContent = template.slice(cursor + 1, end);
100
+ const isSelfClosing = tagContent.endsWith('/') || ['img', 'br', 'input', 'hr', 'link', 'meta'].includes(tagContent.split(' ')[0].toLowerCase());
101
+
102
+ if (tagContent.endsWith('/')) {
103
+ tagContent = tagContent.slice(0, -1).trim();
104
+ }
105
+
106
+ const { tag, props, directives } = parseTag(tagContent);
107
+
108
+ const element: ElementNode = { type: 'Element', tag, props, children: [], directives };
109
+ stack[stack.length - 1].children.push(element);
110
+
111
+ if (!isSelfClosing) {
112
+ stack.push(element);
113
+ }
114
+
115
+ cursor = end + 1;
116
+ }
117
+ }
118
+ // 3. Interpolation {{ }}
119
+ else if (template.startsWith('{{', cursor)) {
120
+ const end = template.indexOf('}}', cursor);
121
+ if (end === -1) {
122
+ errors.push('Unclosed interpolation {{ }}.');
123
+ break;
124
+ }
125
+
126
+ const content = template.slice(cursor + 2, end).trim();
127
+ stack[stack.length - 1].children.push({ type: 'Interpolation', content });
128
+ cursor = end + 2;
129
+ }
130
+ // 4. Native Template Literals ${ } (Protection)
131
+ else if (template.startsWith('${', cursor)) {
132
+ // Find end of ${ } while respecting nested braces
133
+ let innerCursor = cursor + 2;
134
+ let depth = 1;
135
+ while (innerCursor < template.length && depth > 0) {
136
+ if (template[innerCursor] === '{') depth++;
137
+ if (template[innerCursor] === '}') depth--;
138
+ innerCursor++;
139
+ }
140
+
141
+ const content = template.slice(cursor, innerCursor);
142
+ // Treat as text but this block is now "atomic" and won't be split by < checks
143
+ const lastChild = stack[stack.length - 1].children[stack[stack.length - 1].children.length - 1];
144
+ if (lastChild && lastChild.type === 'Text') {
145
+ lastChild.content += content;
146
+ } else {
147
+ stack[stack.length - 1].children.push({ type: 'Text', content });
148
+ }
149
+ cursor = innerCursor;
150
+ }
151
+ // 5. Text Nodes
152
+ else {
153
+ let nextTag = template.indexOf('<', cursor);
154
+ let nextInterp = template.indexOf('{{', cursor);
155
+ let nextNative = template.indexOf('${', cursor);
156
+
157
+ let end = template.length;
158
+
159
+ // Heuristic for nextTag: it must look like a tag start
160
+ while (nextTag !== -1) {
161
+ if (/[\/a-zA-Z!]/.test(template[nextTag + 1])) {
162
+ break;
163
+ }
164
+ nextTag = template.indexOf('<', nextTag + 1);
165
+ }
166
+
167
+ if (nextTag !== -1 && nextTag < end) end = nextTag;
168
+ if (nextInterp !== -1 && nextInterp < end) end = nextInterp;
169
+ if (nextNative !== -1 && nextNative < end) end = nextNative;
170
+
171
+ const rawContent = template.slice(cursor, end);
172
+ const content = decodeEntities(rawContent);
173
+
174
+ if (content) {
175
+ const lastChild = stack[stack.length - 1].children[stack[stack.length - 1].children.length - 1];
176
+ if (lastChild && lastChild.type === 'Text') {
177
+ lastChild.content += content;
178
+ } else {
179
+ stack[stack.length - 1].children.push({ type: 'Text', content });
180
+ }
181
+ }
182
+ cursor = end;
183
+ }
184
+ }
185
+
186
+ return root;
187
+ }
188
+
189
+ export function parseTag(content: string) {
190
+ const parts = content.split(' ');
191
+ const tag = parts[0];
192
+ const props: Record<string, string> = {};
193
+ const directives: ElementNode['directives'] = {};
194
+
195
+ const attrStr = content.slice(tag.length).trim();
196
+
197
+ let i = 0;
198
+ while (i < attrStr.length) {
199
+ if (/\s/.test(attrStr[i])) {
200
+ i++;
201
+ continue;
202
+ }
203
+
204
+ const keyStart = i;
205
+ while (i < attrStr.length && !/\s|=/.test(attrStr[i])) {
206
+ i++;
207
+ }
208
+ const key = attrStr.slice(keyStart, i);
209
+
210
+ let value = 'true';
211
+
212
+ let peek = i;
213
+ while (peek < attrStr.length && /\s/.test(attrStr[peek])) peek++;
214
+
215
+ if (peek < attrStr.length && attrStr[peek] === '=') {
216
+ i = peek + 1;
217
+ while (i < attrStr.length && /\s/.test(attrStr[i])) i++;
218
+
219
+ if (i < attrStr.length && (attrStr[i] === '"' || attrStr[i] === "'")) {
220
+ const quote = attrStr[i];
221
+ i++;
222
+ const valStart = i;
223
+ while (i < attrStr.length && attrStr[i] !== quote) {
224
+ if (attrStr[i] === '\\' && attrStr[i + 1] === quote) {
225
+ i += 2;
226
+ } else {
227
+ i++;
228
+ }
229
+ }
230
+ value = attrStr.slice(valStart, i);
231
+ i++;
232
+ } else {
233
+ const valStart = i;
234
+ while (i < attrStr.length && !/\s/.test(attrStr[i])) {
235
+ i++;
236
+ }
237
+ value = attrStr.slice(valStart, i);
238
+ }
239
+ }
240
+
241
+ if (key === 'v-if' || key === 'mu-if') {
242
+ directives.vIf = value;
243
+ } else if (key === 'v-for' || key === 'mu-for') {
244
+ const parts = value.split(' in ');
245
+ if (parts.length < 2) {
246
+ directives.vFor = { item: '_item', list: '[]' };
247
+ } else {
248
+ const item = parts[0];
249
+ const list = parts.slice(1).join(' in ');
250
+ directives.vFor = { item: item.trim(), list: list.trim() };
251
+ }
252
+ } else if (key) {
253
+ props[key] = value;
254
+ }
255
+ }
256
+
257
+ return { tag, props, directives };
258
+ }
@@ -30,7 +30,7 @@ export async function compileSFC(source: string, filename: string, options?: Com
30
30
  // 3. Style
31
31
  const styleResult = compileStyle(descriptor, filename, options);
32
32
 
33
- // 4. Template (Use the new No-VDOM compiler!)
33
+ // 4. Template (Use the new unified No-VDOM compiler!)
34
34
  const templateResult = compileToDOM(descriptor, scriptResult, styleResult.scopedId);
35
35
 
36
36
  // Calculate offsets for source map merging
@@ -1,36 +1,7 @@
1
+
1
2
  import { SFCDescriptor } from './sfc-parser';
2
3
  import { ScriptCompileResult } from './script-compiler';
3
-
4
- // --- AST Definitions (Reused from template-compiler) ---
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
- }
4
+ import { parse, Node, ElementNode, TextNode, InterpolationNode } from './ast-parser';
34
5
 
35
6
  // --- DOM Compiler Result ---
36
7
  export interface DOMCompileResult {
@@ -46,7 +17,7 @@ export function compileToDOM(descriptor: SFCDescriptor, scriptResult: ScriptComp
46
17
  let html = template.content;
47
18
  const errors: string[] = [];
48
19
 
49
- // 1. Parsing Phase (HTML -> AST) - We can reuse the parser logic, but for now we duplicate it to keep it isolated from the old string compiler
20
+ // 1. Parsing Phase (HTML -> AST) - Unified Parser
50
21
  const ast = parse(html, errors);
51
22
 
52
23
  // 2. Transform Phase (Scopes, Bindings)
@@ -247,9 +218,7 @@ function generateDOMInstruction(node: Node, chunks: string[], getUid: () => stri
247
218
  if ((element as any)._propertySideEffects) {
248
219
  const effects: string[] = (element as any)._propertySideEffects;
249
220
  for (const effect of effects) {
250
- // effect strings look like: `this._b('mu_abc', 'style', ...)`
251
- // We need to translate them to direct assignments, but the original transform already stripped the 'data-mu-id'.
252
- // Let's rely on the DOM-specific transform logic we will write below.
221
+ // ...
253
222
  }
254
223
  }
255
224
 
@@ -279,104 +248,7 @@ function generateDOMInstruction(node: Node, chunks: string[], getUid: () => stri
279
248
  return null;
280
249
  }
281
250
 
282
- // --- Copied Parser and Transformer ---
283
- // For this standalone compiler test, we duplicate the parser functions.
284
- // In the final refactor, these will be extracted to a shared `parse.ts` utility.
285
-
286
- function parse(template: string, errors: string[]): ElementNode {
287
- const root: ElementNode = {
288
- type: 'Element', tag: 'fragment', props: {}, children: [], directives: {}
289
- };
290
- const stack: ElementNode[] = [root];
291
- let cursor = 0;
292
-
293
- while (cursor < template.length) {
294
- const char = template[cursor];
295
- if (template.startsWith('<!--', cursor)) {
296
- const end = template.indexOf('-->', cursor);
297
- if (end === -1) break;
298
- cursor = end + 3;
299
- } else if (char === '<') {
300
- if (template[cursor + 1] === '/') {
301
- const end = template.indexOf('>', cursor);
302
- if (end === -1) break;
303
- stack.pop();
304
- cursor = end + 1;
305
- } else {
306
- const end = template.indexOf('>', cursor);
307
- if (end === -1) break;
308
- let tagContent = template.slice(cursor + 1, end);
309
- const isSelfClosing = tagContent.endsWith('/') || ['img', 'br', 'input', 'hr'].includes(tagContent.split(' ')[0]);
310
- if (tagContent.endsWith('/')) tagContent = tagContent.slice(0, -1).trim();
311
- const { tag, props, directives } = parseTag(tagContent);
312
- const element: ElementNode = { type: 'Element', tag, props, children: [], directives };
313
- stack[stack.length - 1].children.push(element);
314
- if (!isSelfClosing) stack.push(element);
315
- cursor = end + 1;
316
- }
317
- } else if (char === '{' && template[cursor + 1] === '{') {
318
- const end = template.indexOf('}}', cursor);
319
- if (end === -1) break;
320
- const content = template.slice(cursor + 2, end).trim();
321
- stack[stack.length - 1].children.push({ type: 'Interpolation', content });
322
- cursor = end + 2;
323
- } else {
324
- let nextTag = template.indexOf('<', cursor);
325
- let nextInterp = template.indexOf('{{', cursor);
326
- let end = template.length;
327
- if (nextTag !== -1 && nextTag < end) end = nextTag;
328
- if (nextInterp !== -1 && nextInterp < end) end = nextInterp;
329
- const content = template.slice(cursor, end);
330
- if (content.trim() || content === ' ') {
331
- stack[stack.length - 1].children.push({ type: 'Text', content });
332
- }
333
- cursor = end;
334
- }
335
- }
336
- return root;
337
- }
338
-
339
- function parseTag(content: string) {
340
- const parts = content.split(' ');
341
- const tag = parts[0];
342
- const props: Record<string, string> = {};
343
- const directives: ElementNode['directives'] = {};
344
- const attrStr = content.slice(tag.length).trim();
345
- let i = 0;
346
- while (i < attrStr.length) {
347
- if (/\s/.test(attrStr[i])) { i++; continue; }
348
- const keyStart = i;
349
- while (i < attrStr.length && !/\s|=/.test(attrStr[i])) i++;
350
- const key = attrStr.slice(keyStart, i);
351
- let value = 'true';
352
- let peek = i;
353
- while (peek < attrStr.length && /\s/.test(attrStr[peek])) peek++;
354
- if (peek < attrStr.length && attrStr[peek] === '=') {
355
- i = peek + 1;
356
- while (i < attrStr.length && /\s/.test(attrStr[i])) i++;
357
- if (i < attrStr.length && (attrStr[i] === '"' || attrStr[i] === "'")) {
358
- const quote = attrStr[i];
359
- i++;
360
- const valStart = i;
361
- while (i < attrStr.length && attrStr[i] !== quote) {
362
- if (attrStr[i] === '\\' && attrStr[i + 1] === quote) i += 2; else i++;
363
- }
364
- value = attrStr.slice(valStart, i);
365
- i++;
366
- } else {
367
- const valStart = i;
368
- while (i < attrStr.length && !/\s/.test(attrStr[i])) i++;
369
- value = attrStr.slice(valStart, i);
370
- }
371
- }
372
- if (key === 'v-if' || key === 'mu-if') directives.vIf = value;
373
- else if (key === 'v-for' || key === 'mu-for') {
374
- const parts = value.split(' in ');
375
- directives.vFor = { item: parts[0].trim(), list: parts.slice(1).join(' in ').trim() };
376
- } else if (key) props[key] = value;
377
- }
378
- return { tag, props, directives };
379
- }
251
+ // --- Transformer Re-implementation ---
380
252
 
381
253
  const JS_KEYWORDS = new Set(['true', 'false', 'null', 'undefined', 'this', 'window', 'if', 'else', 'for', 'while', 'return', 'let', 'const', 'typeof', 'instanceof', 'Math', 'Object', 'Array', 'JSON', 'console']);
382
254