@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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/codegen/createElement.d.ts +10 -0
  4. package/dist/codegen/index.d.ts +1 -0
  5. package/dist/compiler.d.ts +26 -0
  6. package/dist/docPipe/index.d.ts +40 -0
  7. package/dist/docPipe/scan.d.ts +6 -0
  8. package/dist/index.d.ts +13 -0
  9. package/dist/index.mjs +2302 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/dist/index.umd.cjs +13 -0
  12. package/dist/index.umd.cjs.map +1 -0
  13. package/dist/parser/ast.d.ts +207 -0
  14. package/dist/parser/lexer.d.ts +109 -0
  15. package/dist/parser/parser.d.ts +79 -0
  16. package/dist/parser/tokens.d.ts +51 -0
  17. package/dist/transform/directives.d.ts +30 -0
  18. package/dist/transform/index.d.ts +14 -0
  19. package/dist/transform/optimize.d.ts +21 -0
  20. package/package.json +53 -0
  21. package/src/codegen/createElement.ts +661 -0
  22. package/src/codegen/index.ts +1 -0
  23. package/src/compiler.ts +70 -0
  24. package/src/docPipe/index.ts +104 -0
  25. package/src/docPipe/scan.ts +318 -0
  26. package/src/index.ts +16 -0
  27. package/src/parser/ast.ts +478 -0
  28. package/src/parser/lexer.ts +561 -0
  29. package/src/parser/parser.ts +514 -0
  30. package/src/parser/tokens.ts +84 -0
  31. package/src/playground/main.ts +206 -0
  32. package/src/playground/style.css +135 -0
  33. package/src/shims/babel-env.ts +17 -0
  34. package/src/tests/compiler.test.ts +434 -0
  35. package/src/tests/docPipe.test.ts +41 -0
  36. package/src/tests/helpers.ts +96 -0
  37. package/src/tests/lexer.test.ts +282 -0
  38. package/src/tests/parser.test.ts +691 -0
  39. package/src/transform/directives.ts +160 -0
  40. package/src/transform/index.ts +29 -0
  41. package/src/transform/optimize.ts +203 -0
@@ -0,0 +1,434 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { compile } from '../compiler';
3
+
4
+ describe('Compiler', () => {
5
+ describe('Basic compilation', () => {
6
+ it('should compile simple element', () => {
7
+ const template = '<div></div>';
8
+ const result = compile(template);
9
+
10
+ expect(result.errors).toHaveLength(0);
11
+ expect(result.code).toContain("h('div', null)");
12
+ });
13
+
14
+ it('should compile element with text', () => {
15
+ const template = '<div>Hello World</div>';
16
+ const result = compile(template);
17
+
18
+ expect(result.errors).toHaveLength(0);
19
+ expect(result.code).toContain("'Hello World'");
20
+ });
21
+
22
+ it('should compile element with interpolation', () => {
23
+ const template = '<div>{message}</div>';
24
+ const result = compile(template);
25
+
26
+ expect(result.errors).toHaveLength(0);
27
+ expect(result.code).toContain('message');
28
+ });
29
+
30
+ it('should compile component', () => {
31
+ const template = '<MyComponent />';
32
+ const result = compile(template);
33
+
34
+ expect(result.errors).toHaveLength(0);
35
+ expect(result.code).toContain('h(MyComponent, null)');
36
+ });
37
+
38
+ it('should compile fragment', () => {
39
+ const template = '<><div>Hello</div><span>World</span></>';
40
+ const result = compile(template);
41
+
42
+ expect(result.errors).toHaveLength(0);
43
+ expect(result.code).toContain('h(Fragment, null');
44
+ });
45
+
46
+ it('should compile deeply nested structures', () => {
47
+ const template = `
48
+ <>
49
+ <Card l-for={(section, sectionIndex) in sections} key={section.id}>
50
+ <Section.Header l-if={section.showHeader}>
51
+ <Title>{section.title ?? 'Untitled'}</Title>
52
+ </Section.Header>
53
+ <Section.Header l-else-if={sectionIndex === 0}>
54
+ <Title>First Section</Title>
55
+ </Section.Header>
56
+ <Section.Header l-else>
57
+ <Title>Hidden</Title>
58
+ </Section.Header>
59
+ <Section.Content>
60
+ <ul>
61
+ <li l-for={(item, itemIndex) in section.items}>
62
+ <span l-if={item.visible}>
63
+ <>
64
+ <Label>{itemIndex + 1}</Label>
65
+ <Text>{item.label}</Text>
66
+ </>
67
+ </span>
68
+ <span l-else>{item.fallback}</span>
69
+ </li>
70
+ </ul>
71
+ </Section.Content>
72
+ </Card>
73
+ </>
74
+ `.trim();
75
+
76
+ const result = compile(template);
77
+
78
+ expect(result.errors).toHaveLength(0);
79
+ expect(result.code).toContain(
80
+ '(sections).map((section, sectionIndex) =>'
81
+ );
82
+ expect(result.code).toContain('(section.items).map((item, itemIndex) =>');
83
+ expect(result.code).toContain('(section.showHeader) ?');
84
+ expect(result.code).toContain('(item.visible) ?');
85
+ });
86
+
87
+ it('should compile compound component reference', () => {
88
+ const template = '<a.Children />';
89
+ const result = compile(template);
90
+
91
+ expect(result.errors).toHaveLength(0);
92
+ expect(result.code).toContain('h(a.Children, null)');
93
+ });
94
+ });
95
+
96
+ describe('Attributes', () => {
97
+ it('should compile static attributes', () => {
98
+ const template = '<div class="container" id="app"></div>';
99
+ const result = compile(template);
100
+
101
+ expect(result.errors).toHaveLength(0);
102
+ expect(result.code).toContain("class: 'container'");
103
+ expect(result.code).toContain("id: 'app'");
104
+ });
105
+
106
+ it('should compile dynamic attributes', () => {
107
+ const template = '<div class={className}></div>';
108
+ const result = compile(template);
109
+
110
+ expect(result.errors).toHaveLength(0);
111
+ expect(result.code).toContain('class: className');
112
+ });
113
+
114
+ it('should compile boolean attributes', () => {
115
+ const template = '<input disabled />';
116
+ const result = compile(template);
117
+
118
+ expect(result.errors).toHaveLength(0);
119
+ expect(result.code).toContain('disabled: true');
120
+ });
121
+ });
122
+
123
+ describe('Directives', () => {
124
+ it('should compile l-if directive', () => {
125
+ const template = '<div l-if={isActive}>Active</div>';
126
+ const result = compile(template);
127
+
128
+ expect(result.errors).toHaveLength(0);
129
+ expect(result.code).toContain('(isActive) ?');
130
+ expect(result.code).toContain("'Active'");
131
+ });
132
+
133
+ it('should compile l-if/l-else-if/l-else chain', () => {
134
+ const template = `
135
+ <div l-if={status === 'loading'}>Loading</div>
136
+ <div l-else-if={status === 'error'}>Error</div>
137
+ <div l-else>Success</div>
138
+ `;
139
+ const result = compile(template);
140
+
141
+ expect(result.errors).toHaveLength(0);
142
+ expect(result.code).toContain("status === 'loading'");
143
+ expect(result.code).toContain("status === 'error'");
144
+ expect(result.code).toContain("'Success'");
145
+ });
146
+
147
+ it('should compile l-for directive', () => {
148
+ const template = '<li l-for={item in items}>{item}</li>';
149
+ const result = compile(template);
150
+
151
+ expect(result.errors).toHaveLength(0);
152
+ expect(result.code).toContain('(items).map(item =>');
153
+ });
154
+
155
+ it('should compile l-for with index', () => {
156
+ const template =
157
+ '<li l-for={(item, index) in items}>{index}: {item}</li>';
158
+ const result = compile(template);
159
+
160
+ expect(result.errors).toHaveLength(0);
161
+ expect(result.code).toContain('(items).map((item, index) =>');
162
+ });
163
+
164
+ it('should compile nested l-if blocks with inner loops', () => {
165
+ const template = `
166
+ <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>
167
+ <div l-else>Fallback</div>
168
+ `.trim();
169
+
170
+ const result = compile(template);
171
+
172
+ expect(result.errors).toHaveLength(0);
173
+ expect(result.code).toContain('(outer) ?');
174
+ expect(result.code).toContain('(middle) ?');
175
+ expect(result.code).toContain('(inner) ?');
176
+ expect(result.code).toContain('(items).map((item, index) =>');
177
+ expect(result.code).toContain('(item.visible) ?');
178
+ expect(result.code).toContain("'Fallback'");
179
+ });
180
+ });
181
+
182
+ describe('Complex templates', () => {
183
+ it('should compile nested elements', () => {
184
+ const template = `
185
+ <div class="container">
186
+ <h1>{title}</h1>
187
+ <p>{content}</p>
188
+ </div>
189
+ `;
190
+ const result = compile(template);
191
+
192
+ expect(result.errors).toHaveLength(0);
193
+ expect(result.code).toContain("h('div'");
194
+ expect(result.code).toContain("h('h1'");
195
+ expect(result.code).toContain("h('p'");
196
+ });
197
+
198
+ it('should compile list with conditionals', () => {
199
+ const template = `
200
+ <ul>
201
+ <li l-for={item in items}>
202
+ <span l-if={item.done}>✓</span>
203
+ {item.text}
204
+ </li>
205
+ </ul>
206
+ `;
207
+ const result = compile(template);
208
+
209
+ expect(result.errors).toHaveLength(0);
210
+ expect(result.code).toContain('(items).map(item =>');
211
+ expect(result.code).toContain('(item.done) ?');
212
+ });
213
+
214
+ it('should compile component with props and children', () => {
215
+ const template = `
216
+ <Card title={cardTitle} active={isActive}>
217
+ <p>{cardContent}</p>
218
+ </Card>
219
+ `;
220
+ const result = compile(template);
221
+
222
+ expect(result.errors).toHaveLength(0);
223
+ expect(result.code).toContain('h(Card');
224
+ expect(result.code).toContain('title: cardTitle');
225
+ expect(result.code).toContain('active: isActive');
226
+ expect(result.code).toContain("h('p'");
227
+ });
228
+
229
+ it('should compile expressions containing nested templates', () => {
230
+ const template = `
231
+ <div>
232
+ {items.map(item => <span class={getClass(item)}>{item.value}</span>)}
233
+ </div>
234
+ `;
235
+ const result = compile(template);
236
+
237
+ expect(result.errors).toHaveLength(0);
238
+ expect(result.code).toContain("items.map(item => h('span'");
239
+ expect(result.code).toContain('getClass(item)');
240
+ expect(result.code).toContain('item.value');
241
+ });
242
+ });
243
+
244
+ describe('Error handling', () => {
245
+ it('should handle syntax errors gracefully', () => {
246
+ const template = '<div></span>';
247
+ const result = compile(template);
248
+
249
+ expect(result.errors.length).toBeGreaterThan(0);
250
+ expect(result.code).toBe('');
251
+ });
252
+
253
+ it('should report line and column for syntax errors', () => {
254
+ const template = '<div><span></div>';
255
+ const result = compile(template);
256
+
257
+ expect(result.errors.length).toBeGreaterThan(0);
258
+ const error = result.errors[0];
259
+ expect(error.line).toBeGreaterThan(0);
260
+ expect(error.column).toBeGreaterThan(0);
261
+ });
262
+ });
263
+
264
+ describe('Real-world examples', () => {
265
+ it('should compile todo list', () => {
266
+ const template = `
267
+ <div class="todo-list">
268
+ <h2>Todos ({todos.length})</h2>
269
+ <div l-for={todo in todos} class="todo-item">
270
+ <input type="checkbox" checked={todo.done} />
271
+ <span>{todo.text}</span>
272
+ </div>
273
+ <p l-if={todos.length === 0}>No todos yet</p>
274
+ </div>
275
+ `;
276
+ const result = compile(template);
277
+
278
+ expect(result.errors).toHaveLength(0);
279
+ expect(result.code).toContain('(todos).map(todo =>');
280
+ expect(result.code).toContain('todos.length');
281
+ });
282
+
283
+ it('should compile user profile card', () => {
284
+ const template = `
285
+ <div class="profile-card">
286
+ <img src={user.avatar} />
287
+ <h3>{user.name}</h3>
288
+ <p>{user.email}</p>
289
+ <button l-if={canEdit} onClick={handleEdit}>Edit</button>
290
+ </div>
291
+ `;
292
+ const result = compile(template);
293
+
294
+ expect(result.errors).toHaveLength(0);
295
+ expect(result.code).toContain('user.avatar');
296
+ expect(result.code).toContain('user.name');
297
+ expect(result.code).toContain('(canEdit) ?');
298
+ });
299
+
300
+ it('should compile dashboard with multiple sections', () => {
301
+ const template = `
302
+ <div class="dashboard">
303
+ <header>
304
+ <h1>{title}</h1>
305
+ </header>
306
+ <main>
307
+ <div l-if={isLoading}>
308
+ <Spinner />
309
+ </div>
310
+ <div l-else-if={hasError}>
311
+ <ErrorMessage message={error} />
312
+ </div>
313
+ <div l-else>
314
+ <DataGrid data={data} />
315
+ </div>
316
+ </main>
317
+ </div>
318
+ `;
319
+ const result = compile(template);
320
+
321
+ expect(result.errors).toHaveLength(0);
322
+ expect(result.code).toContain('(isLoading) ?');
323
+ expect(result.code).toContain('h(Spinner');
324
+ expect(result.code).toContain('h(ErrorMessage');
325
+ expect(result.code).toContain('h(DataGrid');
326
+ });
327
+ });
328
+
329
+ describe('Compatibility fixtures', () => {
330
+ const fixtures = [
331
+ {
332
+ name: 'todo section with conditionals and loops',
333
+ template: `
334
+ <section class="todo">
335
+ <header l-if={showHeader}>
336
+ <h2>{title}</h2>
337
+ <p l-if={description}>{description}</p>
338
+ <p l-else>No description</p>
339
+ </header>
340
+ <main>
341
+ <article l-for={(item, index) in items} key={item.id}>
342
+ <header>
343
+ <h3>{index + 1}. {item.name}</h3>
344
+ <span l-if={item.done}>✓</span>
345
+ </header>
346
+ <ul>
347
+ <li l-for={(tag, tagIndex) in item.tags}>
348
+ <span>{tagIndex}</span>
349
+ <span>{tag.label}</span>
350
+ </li>
351
+ </ul>
352
+ </article>
353
+ </main>
354
+ <footer l-else-if={items.length === 0}>
355
+ <p>No items</p>
356
+ </footer>
357
+ </section>
358
+ `.trim(),
359
+ },
360
+ {
361
+ name: 'nested fragments with mixed content',
362
+ template: `
363
+ <>
364
+ <div class="layout">
365
+ <aside l-if={sidebar}>
366
+ <nav>
367
+ <ul>
368
+ <li l-for={link in sidebar.links}>
369
+ <a href={link.href}>{link.label}</a>
370
+ </li>
371
+ </ul>
372
+ </nav>
373
+ </aside>
374
+ <section>
375
+ <>
376
+ <header>
377
+ <h1>{page.title}</h1>
378
+ <p l-if={page.subtitle}>{page.subtitle}</p>
379
+ </header>
380
+ <article>
381
+ <slot-content />
382
+ </article>
383
+ </>
384
+ </section>
385
+ </div>
386
+ <footer l-else>
387
+ <p>Sidebar disabled</p>
388
+ </footer>
389
+ </>
390
+ `.trim(),
391
+ },
392
+ ];
393
+
394
+ for (const { name, template } of fixtures) {
395
+ it(`should compile fixture: ${name}`, () => {
396
+ const result = compile(template);
397
+
398
+ expect(result.errors).toHaveLength(0);
399
+ expect(result.code).toContain('h(');
400
+ });
401
+ }
402
+ });
403
+
404
+ describe('Options', () => {
405
+ it('should respect generate options', () => {
406
+ const template = '<div></div>';
407
+ const result = compile(template, {
408
+ generate: {
409
+ templateFactory: 'createElement',
410
+ },
411
+ });
412
+
413
+ expect(result.errors).toHaveLength(0);
414
+ expect(result.code).toContain('createElement(');
415
+ expect(result.code).not.toContain('import');
416
+ });
417
+
418
+ it('should apply optimizations', () => {
419
+ const template = '<div> Hello World </div>';
420
+ const result = compile(template, {
421
+ transform: {
422
+ optimize: true,
423
+ optimizeOptions: {
424
+ trimText: true,
425
+ },
426
+ },
427
+ });
428
+
429
+ expect(result.errors).toHaveLength(0);
430
+ // Text should be trimmed
431
+ expect(result.code).toContain('Hello World');
432
+ });
433
+ });
434
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { transformDocument } from '../docPipe';
3
+
4
+ const wrapStrong = (content: string): string =>
5
+ `<strong class="todo-status pending">${content}</strong>`;
6
+
7
+ describe('docPipe: transformDocument', () => {
8
+ const cases = [
9
+ {
10
+ name: 'text containing single quote',
11
+ source: wrapStrong("as'ldkgjas"),
12
+ expected: "h('strong', { class: 'todo-status pending' }, 'as\\'ldkgjas')",
13
+ },
14
+ {
15
+ name: 'text containing double quote',
16
+ source: wrapStrong('as"ldkgjas'),
17
+ expected: "h('strong', { class: 'todo-status pending' }, 'as\"ldkgjas')",
18
+ },
19
+ {
20
+ name: 'text containing backtick',
21
+ source: wrapStrong('as`ldkgjas'),
22
+ expected: "h('strong', { class: 'todo-status pending' }, 'as`ldkgjas')",
23
+ },
24
+ ];
25
+
26
+ for (const { name, source, expected } of cases) {
27
+ it(`should transform template with ${name}`, () => {
28
+ const result = transformDocument(source);
29
+ expect(result.errors).toHaveLength(0);
30
+ expect(result.transformed).toBe(true);
31
+ expect(result.code).toContain(expected);
32
+ });
33
+ }
34
+
35
+ it('should report compile error for unterminated expression braces', () => {
36
+ const source = wrapStrong('as{ldkgjas');
37
+ const result = transformDocument(source);
38
+ expect(result.transformed).toBe(false);
39
+ expect(result.errors.length).toBeGreaterThan(0);
40
+ });
41
+ });
@@ -0,0 +1,96 @@
1
+ import {
2
+ TemplateNode,
3
+ ElementNode,
4
+ FragmentNode,
5
+ TextNode,
6
+ InterpolationNode,
7
+ CommentNode,
8
+ DirectiveNode,
9
+ DirectiveForNode,
10
+ DirectiveIfNode,
11
+ DirectiveElseIfNode,
12
+ DirectiveElseNode,
13
+ isElementNode,
14
+ isFragmentNode,
15
+ isTextNode,
16
+ isInterpolationNode,
17
+ isCommentNode,
18
+ NodeType,
19
+ } from '../parser/ast';
20
+
21
+ export function expectElement(node: TemplateNode): ElementNode {
22
+ if (!isElementNode(node)) {
23
+ throw new Error(`Expected ElementNode but received ${node.type}`);
24
+ }
25
+ return node;
26
+ }
27
+
28
+ export function expectFragment(node: TemplateNode): FragmentNode {
29
+ if (!isFragmentNode(node)) {
30
+ throw new Error(`Expected FragmentNode but received ${node.type}`);
31
+ }
32
+ return node;
33
+ }
34
+
35
+ export function expectText(node: TemplateNode): TextNode {
36
+ if (!isTextNode(node)) {
37
+ throw new Error(`Expected TextNode but received ${node.type}`);
38
+ }
39
+ return node;
40
+ }
41
+
42
+ export function expectInterpolation(node: TemplateNode): InterpolationNode {
43
+ if (!isInterpolationNode(node)) {
44
+ throw new Error(`Expected InterpolationNode but received ${node.type}`);
45
+ }
46
+ return node;
47
+ }
48
+
49
+ export function expectComment(node: TemplateNode): CommentNode {
50
+ if (!isCommentNode(node)) {
51
+ throw new Error(`Expected CommentNode but received ${node.type}`);
52
+ }
53
+ return node;
54
+ }
55
+
56
+ export function expectDirective<T extends DirectiveNode>(
57
+ directive: DirectiveNode | undefined,
58
+ predicate: (dir: DirectiveNode) => dir is T,
59
+ expectedType: string
60
+ ): T {
61
+ if (!directive) {
62
+ throw new Error(
63
+ `Expected ${expectedType} directive but received undefined`
64
+ );
65
+ }
66
+ if (!predicate(directive)) {
67
+ throw new Error(
68
+ `Expected ${expectedType} directive but received ${directive.type}`
69
+ );
70
+ }
71
+ return directive;
72
+ }
73
+
74
+ export function isDirectiveFor(
75
+ directive: DirectiveNode
76
+ ): directive is DirectiveForNode {
77
+ return directive.type === NodeType.DIRECTIVE_FOR;
78
+ }
79
+
80
+ export function isDirectiveIf(
81
+ directive: DirectiveNode
82
+ ): directive is DirectiveIfNode {
83
+ return directive.type === NodeType.DIRECTIVE_IF;
84
+ }
85
+
86
+ export function isDirectiveElseIf(
87
+ directive: DirectiveNode
88
+ ): directive is DirectiveElseIfNode {
89
+ return directive.type === NodeType.DIRECTIVE_ELSE_IF;
90
+ }
91
+
92
+ export function isDirectiveElse(
93
+ directive: DirectiveNode
94
+ ): directive is DirectiveElseNode {
95
+ return directive.type === NodeType.DIRECTIVE_ELSE;
96
+ }