@lithent/lithent-template-parser 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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/codegen/createElement.d.ts +10 -0
- package/dist/codegen/index.d.ts +1 -0
- package/dist/compiler.d.ts +26 -0
- package/dist/docPipe/index.d.ts +40 -0
- package/dist/docPipe/scan.d.ts +6 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +2302 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.cjs +13 -0
- package/dist/index.umd.cjs.map +1 -0
- package/dist/parser/ast.d.ts +207 -0
- package/dist/parser/lexer.d.ts +109 -0
- package/dist/parser/parser.d.ts +79 -0
- package/dist/parser/tokens.d.ts +51 -0
- package/dist/transform/directives.d.ts +30 -0
- package/dist/transform/index.d.ts +14 -0
- package/dist/transform/optimize.d.ts +21 -0
- package/package.json +53 -0
- package/src/codegen/createElement.ts +661 -0
- package/src/codegen/index.ts +1 -0
- package/src/compiler.ts +70 -0
- package/src/docPipe/index.ts +104 -0
- package/src/docPipe/scan.ts +318 -0
- package/src/index.ts +16 -0
- package/src/parser/ast.ts +478 -0
- package/src/parser/lexer.ts +561 -0
- package/src/parser/parser.ts +514 -0
- package/src/parser/tokens.ts +84 -0
- package/src/playground/main.ts +206 -0
- package/src/playground/style.css +135 -0
- package/src/shims/babel-env.ts +17 -0
- package/src/tests/compiler.test.ts +434 -0
- package/src/tests/docPipe.test.ts +41 -0
- package/src/tests/helpers.ts +96 -0
- package/src/tests/lexer.test.ts +282 -0
- package/src/tests/parser.test.ts +691 -0
- package/src/transform/directives.ts +160 -0
- package/src/transform/index.ts +29 -0
- package/src/transform/optimize.ts +203 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { tokenize } from '../parser/lexer';
|
|
3
|
+
import { parse } from '../parser/parser';
|
|
4
|
+
import { NodeType } from '../parser/ast';
|
|
5
|
+
import {
|
|
6
|
+
expectElement,
|
|
7
|
+
expectFragment,
|
|
8
|
+
expectText,
|
|
9
|
+
expectInterpolation,
|
|
10
|
+
expectComment,
|
|
11
|
+
expectDirective,
|
|
12
|
+
isDirectiveFor,
|
|
13
|
+
isDirectiveIf,
|
|
14
|
+
isDirectiveElseIf,
|
|
15
|
+
isDirectiveElse,
|
|
16
|
+
} from './helpers';
|
|
17
|
+
|
|
18
|
+
describe('Parser', () => {
|
|
19
|
+
describe('Basic elements', () => {
|
|
20
|
+
it('should parse simple element', () => {
|
|
21
|
+
const tokens = tokenize('<div></div>');
|
|
22
|
+
const ast = parse(tokens);
|
|
23
|
+
|
|
24
|
+
expect(ast.type).toBe(NodeType.ROOT);
|
|
25
|
+
expect(ast.children).toHaveLength(1);
|
|
26
|
+
|
|
27
|
+
const element = expectElement(ast.children[0]);
|
|
28
|
+
expect(element.type).toBe(NodeType.ELEMENT);
|
|
29
|
+
expect(element.tag).toBe('div');
|
|
30
|
+
expect(element.isSelfClosing).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should parse self-closing element', () => {
|
|
34
|
+
const tokens = tokenize('<img />');
|
|
35
|
+
const ast = parse(tokens);
|
|
36
|
+
|
|
37
|
+
expect(ast.children).toHaveLength(1);
|
|
38
|
+
const element = expectElement(ast.children[0]);
|
|
39
|
+
expect(element.type).toBe(NodeType.ELEMENT);
|
|
40
|
+
expect(element.tag).toBe('img');
|
|
41
|
+
expect(element.isSelfClosing).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should parse fragment', () => {
|
|
45
|
+
const tokens = tokenize('<><div></div><span /></>');
|
|
46
|
+
const ast = parse(tokens);
|
|
47
|
+
|
|
48
|
+
expect(ast.children).toHaveLength(1);
|
|
49
|
+
const fragment = expectFragment(ast.children[0]);
|
|
50
|
+
expect(fragment.type).toBe(NodeType.FRAGMENT);
|
|
51
|
+
expect(fragment.children).toHaveLength(2);
|
|
52
|
+
|
|
53
|
+
const firstChild = expectElement(fragment.children[0]);
|
|
54
|
+
expect(firstChild.tag).toBe('div');
|
|
55
|
+
const secondChild = expectElement(fragment.children[1]);
|
|
56
|
+
expect(secondChild.tag).toBe('span');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should parse component (PascalCase)', () => {
|
|
60
|
+
const tokens = tokenize('<MyComponent></MyComponent>');
|
|
61
|
+
const ast = parse(tokens);
|
|
62
|
+
|
|
63
|
+
const element = expectElement(ast.children[0]);
|
|
64
|
+
expect(element.type).toBe(NodeType.ELEMENT);
|
|
65
|
+
expect(element.tag).toBe('MyComponent');
|
|
66
|
+
expect(element.isComponent).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should parse fragment', () => {
|
|
70
|
+
const tokens = tokenize('<><div></div><span /></>');
|
|
71
|
+
const ast = parse(tokens);
|
|
72
|
+
|
|
73
|
+
expect(ast.children).toHaveLength(1);
|
|
74
|
+
const fragment = expectFragment(ast.children[0]);
|
|
75
|
+
expect(fragment.type).toBe(NodeType.FRAGMENT);
|
|
76
|
+
expect(fragment.children).toHaveLength(2);
|
|
77
|
+
|
|
78
|
+
const firstChild = expectElement(fragment.children[0]);
|
|
79
|
+
expect(firstChild.tag).toBe('div');
|
|
80
|
+
const secondChild = expectElement(fragment.children[1]);
|
|
81
|
+
expect(secondChild.tag).toBe('span');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should parse compound component (namespaced)', () => {
|
|
85
|
+
const tokens = tokenize('<Card.Header></Card.Header>');
|
|
86
|
+
const ast = parse(tokens);
|
|
87
|
+
|
|
88
|
+
const element = expectElement(ast.children[0]);
|
|
89
|
+
expect(element.type).toBe(NodeType.ELEMENT);
|
|
90
|
+
expect(element.tag).toBe('Card.Header');
|
|
91
|
+
expect(element.isComponent).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should treat lowercase namespace member as component', () => {
|
|
95
|
+
const tokens = tokenize('<a.Children></a.Children>');
|
|
96
|
+
const ast = parse(tokens);
|
|
97
|
+
|
|
98
|
+
const element = expectElement(ast.children[0]);
|
|
99
|
+
expect(element.tag).toBe('a.Children');
|
|
100
|
+
expect(element.isComponent).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should parse nested elements', () => {
|
|
104
|
+
const tokens = tokenize('<div><span></span></div>');
|
|
105
|
+
const ast = parse(tokens);
|
|
106
|
+
|
|
107
|
+
const div = expectElement(ast.children[0]);
|
|
108
|
+
expect(div.type).toBe(NodeType.ELEMENT);
|
|
109
|
+
expect(div.tag).toBe('div');
|
|
110
|
+
expect(div.children).toHaveLength(1);
|
|
111
|
+
|
|
112
|
+
const span = expectElement(div.children[0]);
|
|
113
|
+
expect(span.type).toBe(NodeType.ELEMENT);
|
|
114
|
+
expect(span.tag).toBe('span');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('Attributes', () => {
|
|
119
|
+
it('should parse static attribute', () => {
|
|
120
|
+
const tokens = tokenize('<div class="container"></div>');
|
|
121
|
+
const ast = parse(tokens);
|
|
122
|
+
|
|
123
|
+
const element = expectElement(ast.children[0]);
|
|
124
|
+
expect(element.type).toBe(NodeType.ELEMENT);
|
|
125
|
+
expect(element.attributes).toHaveLength(1);
|
|
126
|
+
|
|
127
|
+
const attr = element.attributes[0];
|
|
128
|
+
expect(attr.name).toBe('class');
|
|
129
|
+
expect(attr.value).toBe('container');
|
|
130
|
+
expect(attr.isDynamic).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should parse dynamic attribute', () => {
|
|
134
|
+
const tokens = tokenize('<div class={className}></div>');
|
|
135
|
+
const ast = parse(tokens);
|
|
136
|
+
|
|
137
|
+
const element = expectElement(ast.children[0]);
|
|
138
|
+
expect(element.attributes).toHaveLength(1);
|
|
139
|
+
|
|
140
|
+
const attr = element.attributes[0];
|
|
141
|
+
expect(attr.name).toBe('class');
|
|
142
|
+
expect(attr.value).toEqual({ expression: 'className' });
|
|
143
|
+
expect(attr.isDynamic).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should parse multiple attributes', () => {
|
|
147
|
+
const tokens = tokenize(
|
|
148
|
+
'<div id="app" class={className} disabled></div>'
|
|
149
|
+
);
|
|
150
|
+
const ast = parse(tokens);
|
|
151
|
+
|
|
152
|
+
const element = expectElement(ast.children[0]);
|
|
153
|
+
expect(element.attributes).toHaveLength(3);
|
|
154
|
+
|
|
155
|
+
expect(element.attributes[0].name).toBe('id');
|
|
156
|
+
expect(element.attributes[0].value).toBe('app');
|
|
157
|
+
|
|
158
|
+
expect(element.attributes[1].name).toBe('class');
|
|
159
|
+
expect(element.attributes[1].isDynamic).toBe(true);
|
|
160
|
+
|
|
161
|
+
expect(element.attributes[2].name).toBe('disabled');
|
|
162
|
+
expect(element.attributes[2].value).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should parse ref attribute', () => {
|
|
166
|
+
const tokens = tokenize('<div ref={myRef}></div>');
|
|
167
|
+
const ast = parse(tokens);
|
|
168
|
+
|
|
169
|
+
const element = expectElement(ast.children[0]);
|
|
170
|
+
expect(element.type).toBe(NodeType.ELEMENT);
|
|
171
|
+
expect(element.ref).toBe('myRef');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('Directives', () => {
|
|
176
|
+
it('should parse l-if directive', () => {
|
|
177
|
+
const tokens = tokenize('<div l-if={count > 0}></div>');
|
|
178
|
+
const ast = parse(tokens);
|
|
179
|
+
|
|
180
|
+
const element = expectElement(ast.children[0]);
|
|
181
|
+
expect(element.type).toBe(NodeType.ELEMENT);
|
|
182
|
+
expect(element.directives).toHaveLength(1);
|
|
183
|
+
|
|
184
|
+
const directive = expectDirective(
|
|
185
|
+
element.directives[0],
|
|
186
|
+
isDirectiveIf,
|
|
187
|
+
'DirectiveIf'
|
|
188
|
+
);
|
|
189
|
+
expect(directive.type).toBe(NodeType.DIRECTIVE_IF);
|
|
190
|
+
expect(directive.condition).toBe('count > 0');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should parse l-else-if directive', () => {
|
|
194
|
+
const tokens = tokenize('<div l-else-if={count === 0}></div>');
|
|
195
|
+
const ast = parse(tokens);
|
|
196
|
+
|
|
197
|
+
const element = expectElement(ast.children[0]);
|
|
198
|
+
const directive = expectDirective(
|
|
199
|
+
element.directives[0],
|
|
200
|
+
isDirectiveElseIf,
|
|
201
|
+
'DirectiveElseIf'
|
|
202
|
+
);
|
|
203
|
+
expect(directive.type).toBe(NodeType.DIRECTIVE_ELSE_IF);
|
|
204
|
+
expect(directive.condition).toBe('count === 0');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should parse l-else directive', () => {
|
|
208
|
+
const tokens = tokenize('<div l-else></div>');
|
|
209
|
+
const ast = parse(tokens);
|
|
210
|
+
|
|
211
|
+
const element = expectElement(ast.children[0]);
|
|
212
|
+
const directive = expectDirective(
|
|
213
|
+
element.directives[0],
|
|
214
|
+
isDirectiveElse,
|
|
215
|
+
'DirectiveElse'
|
|
216
|
+
);
|
|
217
|
+
expect(directive.type).toBe(NodeType.DIRECTIVE_ELSE);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should parse l-for directive (simple)', () => {
|
|
221
|
+
const tokens = tokenize('<div l-for={item in items}></div>');
|
|
222
|
+
const ast = parse(tokens);
|
|
223
|
+
|
|
224
|
+
const element = expectElement(ast.children[0]);
|
|
225
|
+
const directive = expectDirective(
|
|
226
|
+
element.directives[0],
|
|
227
|
+
isDirectiveFor,
|
|
228
|
+
'DirectiveFor'
|
|
229
|
+
);
|
|
230
|
+
expect(directive.type).toBe(NodeType.DIRECTIVE_FOR);
|
|
231
|
+
expect(directive.item).toBe('item');
|
|
232
|
+
expect(directive.list).toBe('items');
|
|
233
|
+
expect(directive.index).toBeUndefined();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should parse l-for directive (with index)', () => {
|
|
237
|
+
const tokens = tokenize('<div l-for={(item, index) in items}></div>');
|
|
238
|
+
const ast = parse(tokens);
|
|
239
|
+
|
|
240
|
+
const element = expectElement(ast.children[0]);
|
|
241
|
+
const directive = expectDirective(
|
|
242
|
+
element.directives[0],
|
|
243
|
+
isDirectiveFor,
|
|
244
|
+
'DirectiveFor'
|
|
245
|
+
);
|
|
246
|
+
expect(directive.type).toBe(NodeType.DIRECTIVE_FOR);
|
|
247
|
+
expect(directive.item).toBe('item');
|
|
248
|
+
expect(directive.index).toBe('index');
|
|
249
|
+
expect(directive.list).toBe('items');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should parse l-for directive with multiline expression', () => {
|
|
253
|
+
const template = `<div l-for={(item, index) in
|
|
254
|
+
itemsList}></div>`;
|
|
255
|
+
const tokens = tokenize(template);
|
|
256
|
+
const ast = parse(tokens);
|
|
257
|
+
|
|
258
|
+
const element = expectElement(ast.children[0]);
|
|
259
|
+
const directive = expectDirective(
|
|
260
|
+
element.directives[0],
|
|
261
|
+
isDirectiveFor,
|
|
262
|
+
'DirectiveFor'
|
|
263
|
+
);
|
|
264
|
+
expect(directive.type).toBe(NodeType.DIRECTIVE_FOR);
|
|
265
|
+
expect(directive.item).toBe('item');
|
|
266
|
+
expect(directive.index).toBe('index');
|
|
267
|
+
expect(directive.list).toBe('itemsList');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should parse l-for directive with complex identifier names', () => {
|
|
271
|
+
const tokens = tokenize('<div l-for={($item, idx_1) in dataSets}></div>');
|
|
272
|
+
const ast = parse(tokens);
|
|
273
|
+
|
|
274
|
+
const element = expectElement(ast.children[0]);
|
|
275
|
+
const directive = expectDirective(
|
|
276
|
+
element.directives[0],
|
|
277
|
+
isDirectiveFor,
|
|
278
|
+
'DirectiveFor'
|
|
279
|
+
);
|
|
280
|
+
expect(directive.type).toBe(NodeType.DIRECTIVE_FOR);
|
|
281
|
+
expect(directive.item).toBe('$item');
|
|
282
|
+
expect(directive.index).toBe('idx_1');
|
|
283
|
+
expect(directive.list).toBe('dataSets');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should parse l-for directive with ternary expression list', () => {
|
|
287
|
+
const template =
|
|
288
|
+
'<div l-for={(item, idx) in items.length ? items : []}></div>';
|
|
289
|
+
const tokens = tokenize(template);
|
|
290
|
+
const ast = parse(tokens);
|
|
291
|
+
|
|
292
|
+
const element = expectElement(ast.children[0]);
|
|
293
|
+
const directive = expectDirective(
|
|
294
|
+
element.directives[0],
|
|
295
|
+
isDirectiveFor,
|
|
296
|
+
'DirectiveFor'
|
|
297
|
+
);
|
|
298
|
+
expect(directive.type).toBe(NodeType.DIRECTIVE_FOR);
|
|
299
|
+
expect(directive.item).toBe('item');
|
|
300
|
+
expect(directive.index).toBe('idx');
|
|
301
|
+
expect(directive.list).toBe('items.length ? items : []');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should parse l-for directive with complex list expression', () => {
|
|
305
|
+
const template = `<div l-for={(item, idx) in data?.map(value => \`inner \${value}\`).filter(Boolean)}></div>`;
|
|
306
|
+
const tokens = tokenize(template);
|
|
307
|
+
const ast = parse(tokens);
|
|
308
|
+
|
|
309
|
+
const element = expectElement(ast.children[0]);
|
|
310
|
+
const directive = expectDirective(
|
|
311
|
+
element.directives[0],
|
|
312
|
+
isDirectiveFor,
|
|
313
|
+
'DirectiveFor'
|
|
314
|
+
);
|
|
315
|
+
expect(directive.type).toBe(NodeType.DIRECTIVE_FOR);
|
|
316
|
+
expect(directive.item).toBe('item');
|
|
317
|
+
expect(directive.index).toBe('idx');
|
|
318
|
+
expect(directive.list).toBe(
|
|
319
|
+
'data?.map(value => `inner ${value}`).filter(Boolean)'
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('Text and interpolation', () => {
|
|
325
|
+
it('should parse text node', () => {
|
|
326
|
+
const tokens = tokenize('<div>Hello World</div>');
|
|
327
|
+
const ast = parse(tokens);
|
|
328
|
+
|
|
329
|
+
const element = expectElement(ast.children[0]);
|
|
330
|
+
expect(element.children).toHaveLength(1);
|
|
331
|
+
|
|
332
|
+
const textNode = expectText(element.children[0]);
|
|
333
|
+
expect(textNode.type).toBe(NodeType.TEXT);
|
|
334
|
+
expect(textNode.content).toBe('Hello World');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should parse interpolation', () => {
|
|
338
|
+
const tokens = tokenize('<div>{message}</div>');
|
|
339
|
+
const ast = parse(tokens);
|
|
340
|
+
|
|
341
|
+
const element = expectElement(ast.children[0]);
|
|
342
|
+
expect(element.children).toHaveLength(1);
|
|
343
|
+
|
|
344
|
+
const interpolation = expectInterpolation(element.children[0]);
|
|
345
|
+
expect(interpolation.type).toBe(NodeType.INTERPOLATION);
|
|
346
|
+
expect(interpolation.expression).toBe('message');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should parse mixed text and interpolation', () => {
|
|
350
|
+
const tokens = tokenize('<div>Hello {name}!</div>');
|
|
351
|
+
const ast = parse(tokens);
|
|
352
|
+
|
|
353
|
+
const element = expectElement(ast.children[0]);
|
|
354
|
+
expect(element.children).toHaveLength(3);
|
|
355
|
+
|
|
356
|
+
const firstChild = expectText(element.children[0]);
|
|
357
|
+
expect(firstChild.content).toBe('Hello ');
|
|
358
|
+
|
|
359
|
+
const secondChild = expectInterpolation(element.children[1]);
|
|
360
|
+
expect(secondChild.expression).toBe('name');
|
|
361
|
+
|
|
362
|
+
const thirdChild = expectText(element.children[2]);
|
|
363
|
+
expect(thirdChild.content).toBe('!');
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('Comments', () => {
|
|
368
|
+
it('should parse comments', () => {
|
|
369
|
+
const tokens = tokenize('<!-- This is a comment -->');
|
|
370
|
+
const ast = parse(tokens);
|
|
371
|
+
|
|
372
|
+
expect(ast.children).toHaveLength(1);
|
|
373
|
+
const comment = expectComment(ast.children[0]);
|
|
374
|
+
expect(comment.type).toBe(NodeType.COMMENT);
|
|
375
|
+
expect(comment.content).toBe('This is a comment');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('Complex templates', () => {
|
|
380
|
+
it('should parse complete component', () => {
|
|
381
|
+
const template = `
|
|
382
|
+
<div class="container">
|
|
383
|
+
<h1>{title}</h1>
|
|
384
|
+
<ul>
|
|
385
|
+
<li l-for={item in items}>
|
|
386
|
+
{item.name}
|
|
387
|
+
</li>
|
|
388
|
+
</ul>
|
|
389
|
+
</div>
|
|
390
|
+
`.trim();
|
|
391
|
+
|
|
392
|
+
const tokens = tokenize(template);
|
|
393
|
+
const ast = parse(tokens);
|
|
394
|
+
|
|
395
|
+
expect(ast.type).toBe(NodeType.ROOT);
|
|
396
|
+
expect(ast.children).toHaveLength(1);
|
|
397
|
+
|
|
398
|
+
const div = expectElement(ast.children[0]);
|
|
399
|
+
expect(div.type).toBe(NodeType.ELEMENT);
|
|
400
|
+
expect(div.tag).toBe('div');
|
|
401
|
+
expect(div.children.length).toBeGreaterThan(0);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should parse conditional rendering', () => {
|
|
405
|
+
const template = `
|
|
406
|
+
<div l-if={isLoading}>Loading...</div>
|
|
407
|
+
<div l-else-if={hasError}>Error</div>
|
|
408
|
+
<div l-else>Content</div>
|
|
409
|
+
`.trim();
|
|
410
|
+
|
|
411
|
+
const tokens = tokenize(template);
|
|
412
|
+
const ast = parse(tokens);
|
|
413
|
+
|
|
414
|
+
expect(ast.children).toHaveLength(3);
|
|
415
|
+
|
|
416
|
+
const first = expectElement(ast.children[0]);
|
|
417
|
+
const second = expectElement(ast.children[1]);
|
|
418
|
+
const third = expectElement(ast.children[2]);
|
|
419
|
+
|
|
420
|
+
expect(first.directives[0].type).toBe(NodeType.DIRECTIVE_IF);
|
|
421
|
+
expect(second.directives[0].type).toBe(NodeType.DIRECTIVE_ELSE_IF);
|
|
422
|
+
expect(third.directives[0].type).toBe(NodeType.DIRECTIVE_ELSE);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should parse component with nested content', () => {
|
|
426
|
+
const template = `
|
|
427
|
+
<Card title="User">
|
|
428
|
+
<div class="header">{title}</div>
|
|
429
|
+
<p>{content}</p>
|
|
430
|
+
</Card>
|
|
431
|
+
`.trim();
|
|
432
|
+
|
|
433
|
+
const tokens = tokenize(template);
|
|
434
|
+
const ast = parse(tokens);
|
|
435
|
+
|
|
436
|
+
const card = expectElement(ast.children[0]);
|
|
437
|
+
expect(card.type).toBe(NodeType.ELEMENT);
|
|
438
|
+
expect(card.tag).toBe('Card');
|
|
439
|
+
expect(card.isComponent).toBe(true);
|
|
440
|
+
|
|
441
|
+
// Should have template and p as children
|
|
442
|
+
expect(card.children.length).toBeGreaterThan(0);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should parse deeply nested structures', () => {
|
|
446
|
+
const template = `
|
|
447
|
+
<>
|
|
448
|
+
<Card l-for={(section, sectionIndex) in sections} key={section.id}>
|
|
449
|
+
<Section.Header l-if={section.showHeader}>
|
|
450
|
+
<!-- header -->
|
|
451
|
+
<Title>{section.title ?? 'Untitled'}</Title>
|
|
452
|
+
</Section.Header>
|
|
453
|
+
<Section.Header l-else-if={sectionIndex === 0}>
|
|
454
|
+
<Title>First Section</Title>
|
|
455
|
+
</Section.Header>
|
|
456
|
+
<Section.Header l-else>
|
|
457
|
+
<Title>Hidden</Title>
|
|
458
|
+
</Section.Header>
|
|
459
|
+
<Section.Content>
|
|
460
|
+
<ul>
|
|
461
|
+
<li l-for={(item, itemIndex) in section.items}>
|
|
462
|
+
<span l-if={item.visible}>
|
|
463
|
+
<>
|
|
464
|
+
<Label>{itemIndex + 1}</Label>
|
|
465
|
+
<Text>{item.label}</Text>
|
|
466
|
+
</>
|
|
467
|
+
</span>
|
|
468
|
+
<span l-else>{item.fallback}</span>
|
|
469
|
+
</li>
|
|
470
|
+
</ul>
|
|
471
|
+
</Section.Content>
|
|
472
|
+
</Card>
|
|
473
|
+
</>
|
|
474
|
+
`.trim();
|
|
475
|
+
|
|
476
|
+
const tokens = tokenize(template);
|
|
477
|
+
const ast = parse(tokens);
|
|
478
|
+
|
|
479
|
+
expect(ast.children).toHaveLength(1);
|
|
480
|
+
const fragment = expectFragment(ast.children[0]);
|
|
481
|
+
expect(fragment.children).toHaveLength(1);
|
|
482
|
+
|
|
483
|
+
const card = expectElement(fragment.children[0]);
|
|
484
|
+
expect(card.tag).toBe('Card');
|
|
485
|
+
const cardLoop = expectDirective(
|
|
486
|
+
card.directives[0],
|
|
487
|
+
isDirectiveFor,
|
|
488
|
+
'DirectiveFor'
|
|
489
|
+
);
|
|
490
|
+
expect(cardLoop.item).toBe('section');
|
|
491
|
+
expect(card.children).toHaveLength(4);
|
|
492
|
+
|
|
493
|
+
const headerIf = expectElement(card.children[0]);
|
|
494
|
+
expect(
|
|
495
|
+
expectDirective(headerIf.directives[0], isDirectiveIf, 'DirectiveIf')
|
|
496
|
+
.condition
|
|
497
|
+
).toContain('section.showHeader');
|
|
498
|
+
|
|
499
|
+
const headerElseIf = expectElement(card.children[1]);
|
|
500
|
+
expect(
|
|
501
|
+
expectDirective(
|
|
502
|
+
headerElseIf.directives[0],
|
|
503
|
+
isDirectiveElseIf,
|
|
504
|
+
'DirectiveElseIf'
|
|
505
|
+
).condition
|
|
506
|
+
).toContain('sectionIndex === 0');
|
|
507
|
+
|
|
508
|
+
const headerElse = expectElement(card.children[2]);
|
|
509
|
+
expect(
|
|
510
|
+
expectDirective(
|
|
511
|
+
headerElse.directives[0],
|
|
512
|
+
isDirectiveElse,
|
|
513
|
+
'DirectiveElse'
|
|
514
|
+
)
|
|
515
|
+
).toBeDefined();
|
|
516
|
+
|
|
517
|
+
const content = expectElement(card.children[3]);
|
|
518
|
+
const list = expectElement(content.children[0]);
|
|
519
|
+
const listItem = expectElement(list.children[0]);
|
|
520
|
+
const itemLoop = expectDirective(
|
|
521
|
+
listItem.directives[0],
|
|
522
|
+
isDirectiveFor,
|
|
523
|
+
'DirectiveFor'
|
|
524
|
+
);
|
|
525
|
+
expect(itemLoop.item).toBe('item');
|
|
526
|
+
|
|
527
|
+
const spanIf = expectElement(listItem.children[0]);
|
|
528
|
+
expect(
|
|
529
|
+
expectDirective(spanIf.directives[0], isDirectiveIf, 'DirectiveIf')
|
|
530
|
+
.condition
|
|
531
|
+
).toContain('item.visible');
|
|
532
|
+
const spanFragment = expectFragment(spanIf.children[0]);
|
|
533
|
+
expect(spanFragment.children).toHaveLength(2);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should parse nested conditionals and loops within l-if blocks', () => {
|
|
537
|
+
const template = `
|
|
538
|
+
<div l-if={outer}><section l-if={middle}><span l-if={inner}>{value}</span><ul><li l-for={(item, index) in items}><span l-if={item.visible}>{item.label}</span><span l-else>{index}</span></li></ul></section></div>
|
|
539
|
+
<div l-else>Fallback</div>
|
|
540
|
+
`.trim();
|
|
541
|
+
|
|
542
|
+
const tokens = tokenize(template);
|
|
543
|
+
const ast = parse(tokens);
|
|
544
|
+
|
|
545
|
+
expect(ast.children).toHaveLength(2);
|
|
546
|
+
|
|
547
|
+
const outerElement = expectElement(ast.children[0]);
|
|
548
|
+
const outerDirective = expectDirective(
|
|
549
|
+
outerElement.directives[0],
|
|
550
|
+
isDirectiveIf,
|
|
551
|
+
'DirectiveIf'
|
|
552
|
+
);
|
|
553
|
+
expect(outerDirective.condition).toBe('outer');
|
|
554
|
+
|
|
555
|
+
const innerSection = expectElement(outerElement.children[0]);
|
|
556
|
+
const sectionDirective = expectDirective(
|
|
557
|
+
innerSection.directives[0],
|
|
558
|
+
isDirectiveIf,
|
|
559
|
+
'DirectiveIf'
|
|
560
|
+
);
|
|
561
|
+
expect(sectionDirective.condition).toBe('middle');
|
|
562
|
+
|
|
563
|
+
const innerSpan = expectElement(innerSection.children[0]);
|
|
564
|
+
const innerDirective = expectDirective(
|
|
565
|
+
innerSpan.directives[0],
|
|
566
|
+
isDirectiveIf,
|
|
567
|
+
'DirectiveIf'
|
|
568
|
+
);
|
|
569
|
+
expect(innerDirective.condition).toBe('inner');
|
|
570
|
+
|
|
571
|
+
const innerList = expectElement(innerSection.children[1]);
|
|
572
|
+
const listItem = expectElement(innerList.children[0]);
|
|
573
|
+
const loopDirective = expectDirective(
|
|
574
|
+
listItem.directives[0],
|
|
575
|
+
isDirectiveFor,
|
|
576
|
+
'DirectiveFor'
|
|
577
|
+
);
|
|
578
|
+
expect(loopDirective.item).toBe('item');
|
|
579
|
+
expect(loopDirective.index).toBe('index');
|
|
580
|
+
expect(loopDirective.list).toBe('items');
|
|
581
|
+
|
|
582
|
+
const loopIf = expectElement(listItem.children[0]);
|
|
583
|
+
const loopIfDirective = expectDirective(
|
|
584
|
+
loopIf.directives[0],
|
|
585
|
+
isDirectiveIf,
|
|
586
|
+
'DirectiveIf'
|
|
587
|
+
);
|
|
588
|
+
expect(loopIfDirective.condition).toBe('item.visible');
|
|
589
|
+
|
|
590
|
+
const loopElse = expectElement(listItem.children[1]);
|
|
591
|
+
expectDirective(loopElse.directives[0], isDirectiveElse, 'DirectiveElse');
|
|
592
|
+
|
|
593
|
+
const fallback = expectElement(ast.children[1]);
|
|
594
|
+
expectDirective(fallback.directives[0], isDirectiveElse, 'DirectiveElse');
|
|
595
|
+
const fallbackText = expectText(fallback.children[0]);
|
|
596
|
+
expect(fallbackText.content).toBe('Fallback');
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
describe('Compatibility fixtures', () => {
|
|
601
|
+
const fixtures = [
|
|
602
|
+
{
|
|
603
|
+
name: 'todo section with conditionals and loops',
|
|
604
|
+
template: `
|
|
605
|
+
<section class="todo">
|
|
606
|
+
<header l-if={showHeader}>
|
|
607
|
+
<h2>{title}</h2>
|
|
608
|
+
<p l-if={description}>{description}</p>
|
|
609
|
+
<p l-else>No description</p>
|
|
610
|
+
</header>
|
|
611
|
+
<main>
|
|
612
|
+
<article l-for={(item, index) in items} key={item.id}>
|
|
613
|
+
<header>
|
|
614
|
+
<h3>{index + 1}. {item.name}</h3>
|
|
615
|
+
<span l-if={item.done}>✓</span>
|
|
616
|
+
</header>
|
|
617
|
+
<ul>
|
|
618
|
+
<li l-for={(tag, tagIndex) in item.tags}>
|
|
619
|
+
<span>{tagIndex}</span>
|
|
620
|
+
<span>{tag.label}</span>
|
|
621
|
+
</li>
|
|
622
|
+
</ul>
|
|
623
|
+
</article>
|
|
624
|
+
</main>
|
|
625
|
+
<footer l-else-if={items.length === 0}>
|
|
626
|
+
<p>No items</p>
|
|
627
|
+
</footer>
|
|
628
|
+
</section>
|
|
629
|
+
`.trim(),
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
name: 'nested fragments with mixed content',
|
|
633
|
+
template: `
|
|
634
|
+
<>
|
|
635
|
+
<div class="layout">
|
|
636
|
+
<aside l-if={sidebar}>
|
|
637
|
+
<nav>
|
|
638
|
+
<ul>
|
|
639
|
+
<li l-for={link in sidebar.links}>
|
|
640
|
+
<a href={link.href}>{link.label}</a>
|
|
641
|
+
</li>
|
|
642
|
+
</ul>
|
|
643
|
+
</nav>
|
|
644
|
+
</aside>
|
|
645
|
+
<section>
|
|
646
|
+
<>
|
|
647
|
+
<header>
|
|
648
|
+
<h1>{page.title}</h1>
|
|
649
|
+
<p l-if={page.subtitle}>{page.subtitle}</p>
|
|
650
|
+
</header>
|
|
651
|
+
<article>
|
|
652
|
+
<slot-content />
|
|
653
|
+
</article>
|
|
654
|
+
</>
|
|
655
|
+
</section>
|
|
656
|
+
</div>
|
|
657
|
+
<footer l-else>
|
|
658
|
+
<p>Sidebar disabled</p>
|
|
659
|
+
</footer>
|
|
660
|
+
</>
|
|
661
|
+
`.trim(),
|
|
662
|
+
},
|
|
663
|
+
];
|
|
664
|
+
|
|
665
|
+
for (const { name, template } of fixtures) {
|
|
666
|
+
it(`should parse fixture: ${name}`, () => {
|
|
667
|
+
const tokens = tokenize(template);
|
|
668
|
+
const ast = parse(tokens);
|
|
669
|
+
|
|
670
|
+
expect(ast.type).toBe(NodeType.ROOT);
|
|
671
|
+
expect(ast.children.length).toBeGreaterThan(0);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
describe('Error handling', () => {
|
|
677
|
+
it('should throw on mismatched tags', () => {
|
|
678
|
+
const tokens = tokenize('<div></span>');
|
|
679
|
+
|
|
680
|
+
expect(() => parse(tokens)).toThrow();
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('should handle empty template', () => {
|
|
684
|
+
const tokens = tokenize('');
|
|
685
|
+
const ast = parse(tokens);
|
|
686
|
+
|
|
687
|
+
expect(ast.type).toBe(NodeType.ROOT);
|
|
688
|
+
expect(ast.children).toHaveLength(0);
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
});
|