@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,661 @@
1
+ import {
2
+ RootNode,
3
+ TemplateNode,
4
+ ElementNode,
5
+ FragmentNode,
6
+ TextNode,
7
+ InterpolationNode,
8
+ NodeType,
9
+ DirectiveForNode,
10
+ } from '../parser/ast';
11
+ import { tokenize } from '../parser/lexer';
12
+ import { parse } from '../parser/parser';
13
+ import { getConditionalGroup, getDirective, transform } from '../transform';
14
+
15
+ /**
16
+ * Generate JavaScript code from AST
17
+ */
18
+ export function generate(ast: RootNode, options?: GenerateOptions): string {
19
+ const opts: Required<GenerateOptions> = {
20
+ templateFactory: 'h',
21
+ templateFragmentFactory: 'Fragment',
22
+ ...options,
23
+ };
24
+
25
+ const code = generateChildren(ast.children, opts);
26
+
27
+ return code;
28
+ }
29
+
30
+ export interface GenerateOptions {
31
+ templateFactory?: string;
32
+ templateFragmentFactory?: string;
33
+ }
34
+
35
+ /**
36
+ * Generate code for children nodes
37
+ */
38
+ function generateChildren(
39
+ children: TemplateNode[],
40
+ options: Required<GenerateOptions>
41
+ ): string {
42
+ const childrenCode = children
43
+ .map(child => generateNode(child, options))
44
+ .filter(code => code !== '');
45
+
46
+ if (childrenCode.length === 0) {
47
+ return 'null';
48
+ }
49
+
50
+ if (childrenCode.length === 1) {
51
+ return childrenCode[0];
52
+ }
53
+
54
+ // Multiple children - wrap in array or Fragment
55
+ return `[${childrenCode.join(', ')}]`;
56
+ }
57
+
58
+ /**
59
+ * Generate code for a single node
60
+ */
61
+ function generateNode(
62
+ node: TemplateNode,
63
+ options: Required<GenerateOptions>
64
+ ): string {
65
+ switch (node.type) {
66
+ case NodeType.ELEMENT:
67
+ return generateElement(node, options);
68
+ case NodeType.FRAGMENT:
69
+ return generateFragment(node, options);
70
+ case NodeType.TEXT:
71
+ return generateText(node);
72
+ case NodeType.INTERPOLATION:
73
+ return generateInterpolation(node, options);
74
+ case NodeType.COMMENT:
75
+ // Comments are not rendered
76
+ return '';
77
+ default:
78
+ return '';
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Generate code for an element node
84
+ */
85
+ function generateElement(
86
+ node: ElementNode,
87
+ options: Required<GenerateOptions>
88
+ ): string {
89
+ // Check if this element is part of a conditional group
90
+ const conditionalGroup = getConditionalGroup(node);
91
+ if (conditionalGroup) {
92
+ return generateConditionalGroup(conditionalGroup, options);
93
+ }
94
+
95
+ // Check if this element has l-for directive
96
+ const forDirective = node.directives.find(
97
+ d => d.type === NodeType.DIRECTIVE_FOR
98
+ ) as DirectiveForNode | undefined;
99
+
100
+ if (forDirective) {
101
+ return generateForLoop(node, forDirective, options);
102
+ }
103
+
104
+ // Normal element
105
+ return generateNormalElement(node, options);
106
+ }
107
+
108
+ /**
109
+ * Generate code for a normal element (without directives)
110
+ */
111
+ function generateNormalElement(
112
+ node: ElementNode,
113
+ options: Required<GenerateOptions>
114
+ ): string {
115
+ const factory = options.templateFactory;
116
+
117
+ // Tag name (component or string)
118
+ const tag = node.isComponent ? node.tag : `'${node.tag}'`;
119
+
120
+ // Props
121
+ const props = generateProps(node, options);
122
+
123
+ // Children
124
+ const children = node.children
125
+ .map(child => generateNode(child, options))
126
+ .filter(code => code !== '');
127
+
128
+ // Build h() call
129
+ if (children.length === 0) {
130
+ return `${factory}(${tag}, ${props})`;
131
+ }
132
+
133
+ return `${factory}(${tag}, ${props}, ${children.join(', ')})`;
134
+ }
135
+
136
+ /**
137
+ * Generate code for a fragment node
138
+ */
139
+ function generateFragment(
140
+ node: FragmentNode,
141
+ options: Required<GenerateOptions>
142
+ ): string {
143
+ const factory = options.templateFactory;
144
+ const fragment = options.templateFragmentFactory;
145
+
146
+ const children = node.children
147
+ .map(child => generateNode(child, options))
148
+ .filter(code => code !== '');
149
+
150
+ if (children.length === 0) {
151
+ return `${factory}(${fragment}, null)`;
152
+ }
153
+
154
+ return `${factory}(${fragment}, null, ${children.join(', ')})`;
155
+ }
156
+
157
+ /**
158
+ * Generate props object
159
+ */
160
+ function generateProps(
161
+ node: ElementNode,
162
+ options: Required<GenerateOptions>
163
+ ): string {
164
+ if (node.attributes.length === 0) {
165
+ return 'null';
166
+ }
167
+
168
+ const props = node.attributes.map(attr => {
169
+ const key = attr.name;
170
+ let value: string;
171
+
172
+ if (attr.value === null) {
173
+ // Boolean attribute (e.g., disabled, checked)
174
+ value = 'true';
175
+ } else if (typeof attr.value === 'string') {
176
+ // Static string value
177
+ value = `'${attr.value.replace(/'/g, "\\'")}'`;
178
+ } else {
179
+ // Dynamic expression
180
+ value = transformExpression(attr.value.expression, options);
181
+ }
182
+
183
+ return `${key}: ${value}`;
184
+ });
185
+
186
+ return `{ ${props.join(', ')} }`;
187
+ }
188
+
189
+ /**
190
+ * Generate code for text node
191
+ */
192
+ function generateText(node: TextNode): string {
193
+ // Escape single quotes in text
194
+ const escaped = node.content
195
+ .replace(/\\/g, '\\\\')
196
+ .replace(/\r/g, '\\r')
197
+ .replace(/\n/g, '\\n')
198
+ .replace(/'/g, "\\'");
199
+ return `'${escaped}'`;
200
+ }
201
+
202
+ /**
203
+ * Generate code for interpolation node
204
+ */
205
+ function generateInterpolation(
206
+ node: InterpolationNode,
207
+ options: Required<GenerateOptions>
208
+ ): string {
209
+ return transformExpression(node.expression, options);
210
+ }
211
+
212
+ /**
213
+ * Generate code for l-for loop
214
+ */
215
+ function generateForLoop(
216
+ node: ElementNode,
217
+ directive: DirectiveForNode,
218
+ options: Required<GenerateOptions>
219
+ ): string {
220
+ const { item, index, list } = directive;
221
+
222
+ // Generate the element code (without the for directive)
223
+ const elementWithoutFor: ElementNode = {
224
+ ...node,
225
+ directives: node.directives.filter(d => d.type !== NodeType.DIRECTIVE_FOR),
226
+ };
227
+
228
+ const elementCode = generateNormalElement(elementWithoutFor, options);
229
+
230
+ // Generate map function
231
+ if (index) {
232
+ return `(${list}).map((${item}, ${index}) => ${elementCode})`;
233
+ } else {
234
+ return `(${list}).map(${item} => ${elementCode})`;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Generate code for conditional group (l-if/l-else-if/l-else)
240
+ */
241
+ function generateConditionalGroup(
242
+ group: any,
243
+ options: Required<GenerateOptions>
244
+ ): string {
245
+ // Start with if condition
246
+ const ifDirective = getDirective(group.if, NodeType.DIRECTIVE_IF);
247
+ if (!ifDirective) {
248
+ return generateNormalElement(group.if, options);
249
+ }
250
+
251
+ const ifElementWithoutDirective: ElementNode = {
252
+ ...group.if,
253
+ directives: group.if.directives.filter(
254
+ (d: any) => d.type !== NodeType.DIRECTIVE_IF
255
+ ),
256
+ };
257
+
258
+ let code = `(${ifDirective.condition}) ? ${generateNormalElement(ifElementWithoutDirective, options)}`;
259
+
260
+ // Add else-if conditions
261
+ for (const elseIfNode of group.elseIfs || []) {
262
+ const elseIfDirective = getDirective(
263
+ elseIfNode,
264
+ NodeType.DIRECTIVE_ELSE_IF
265
+ );
266
+ if (!elseIfDirective) continue;
267
+
268
+ const elseIfElementWithoutDirective: ElementNode = {
269
+ ...elseIfNode,
270
+ directives: elseIfNode.directives.filter(
271
+ (d: any) => d.type !== NodeType.DIRECTIVE_ELSE_IF
272
+ ),
273
+ };
274
+
275
+ code += ` : (${elseIfDirective.condition}) ? ${generateNormalElement(elseIfElementWithoutDirective, options)}`;
276
+ }
277
+
278
+ // Add else condition
279
+ if (group.else) {
280
+ const elseElementWithoutDirective: ElementNode = {
281
+ ...group.else,
282
+ directives: group.else.directives.filter(
283
+ (d: any) => d.type !== NodeType.DIRECTIVE_ELSE
284
+ ),
285
+ };
286
+
287
+ code += ` : ${generateNormalElement(elseElementWithoutDirective, options)}`;
288
+ } else {
289
+ code += ' : null';
290
+ }
291
+
292
+ return code;
293
+ }
294
+
295
+ export function formatCode(code: string): string {
296
+ // Basic formatting - this is a simple implementation
297
+ // For production, you might want to use prettier or similar
298
+ return code;
299
+ }
300
+
301
+ interface TemplateMatch {
302
+ start: number;
303
+ end: number;
304
+ snippet: string;
305
+ }
306
+
307
+ const isAlphaNumeric = (code: number): boolean => {
308
+ return (
309
+ (code >= 48 && code <= 57) || // 0-9
310
+ (code >= 65 && code <= 90) || // A-Z
311
+ (code >= 97 && code <= 122) || // a-z
312
+ code === 95 || // _
313
+ code === 36 // $
314
+ );
315
+ };
316
+
317
+ const shouldAttemptTemplate = (source: string, index: number): boolean => {
318
+ if (source[index] !== '<') {
319
+ return false;
320
+ }
321
+
322
+ const next = source.charCodeAt(index + 1);
323
+ if (Number.isNaN(next)) {
324
+ return false;
325
+ }
326
+
327
+ const isValidNext =
328
+ next === 47 || // /
329
+ next === 62 || // >
330
+ next === 33 || // !
331
+ isAlphaNumeric(next);
332
+
333
+ if (!isValidNext) {
334
+ return false;
335
+ }
336
+
337
+ let i = index - 1;
338
+ while (i >= 0) {
339
+ const code = source.charCodeAt(i);
340
+ if (code === 32 || code === 9 || code === 10 || code === 13) {
341
+ i--;
342
+ continue;
343
+ }
344
+
345
+ if (isAlphaNumeric(code)) {
346
+ return false;
347
+ }
348
+
349
+ break;
350
+ }
351
+
352
+ return true;
353
+ };
354
+
355
+ const skipQuoted = (source: string, index: number, quote: string): number => {
356
+ let i = index + 1;
357
+ while (i < source.length) {
358
+ const ch = source[i];
359
+ if (ch === '\\') {
360
+ i += 2;
361
+ continue;
362
+ }
363
+ if (ch === quote) {
364
+ return i + 1;
365
+ }
366
+ i++;
367
+ }
368
+ return source.length;
369
+ };
370
+
371
+ const skipTemplateLiteral = (source: string, index: number): number => {
372
+ let i = index + 1;
373
+ while (i < source.length) {
374
+ const ch = source[i];
375
+ if (ch === '\\') {
376
+ i += 2;
377
+ continue;
378
+ }
379
+ if (ch === '`') {
380
+ return i + 1;
381
+ }
382
+ if (ch === '$' && source[i + 1] === '{') {
383
+ i = skipJsExpression(source, i + 2);
384
+ continue;
385
+ }
386
+ i++;
387
+ }
388
+ return source.length;
389
+ };
390
+
391
+ const skipLineComment = (source: string, index: number): number => {
392
+ let i = index;
393
+ while (i < source.length && source[i] !== '\\n') {
394
+ i++;
395
+ }
396
+ return i;
397
+ };
398
+
399
+ const skipBlockComment = (source: string, index: number): number => {
400
+ const end = source.indexOf('*/', index);
401
+ return end === -1 ? source.length : end + 2;
402
+ };
403
+
404
+ const skipHtmlComment = (source: string, index: number): number => {
405
+ const end = source.indexOf('-->', index);
406
+ return end === -1 ? source.length : end + 3;
407
+ };
408
+
409
+ const skipJsExpression = (source: string, index: number): number => {
410
+ let depth = 1;
411
+ let i = index;
412
+
413
+ while (i < source.length) {
414
+ const ch = source[i];
415
+ if (ch === '\\') {
416
+ i += 2;
417
+ continue;
418
+ }
419
+ if (ch === "'" || ch === '"') {
420
+ i = skipQuoted(source, i, ch);
421
+ continue;
422
+ }
423
+ if (ch === '`') {
424
+ i = skipTemplateLiteral(source, i);
425
+ continue;
426
+ }
427
+ if (ch === '/' && source[i + 1] === '*') {
428
+ i = skipBlockComment(source, i + 2);
429
+ continue;
430
+ }
431
+ if (ch === '/' && source[i + 1] === '/') {
432
+ i = skipLineComment(source, i + 2);
433
+ continue;
434
+ }
435
+ if (ch === '{') {
436
+ depth++;
437
+ i++;
438
+ continue;
439
+ }
440
+ if (ch === '}') {
441
+ depth--;
442
+ i++;
443
+ if (depth === 0) {
444
+ return i;
445
+ }
446
+ continue;
447
+ }
448
+ i++;
449
+ }
450
+
451
+ return source.length;
452
+ };
453
+
454
+ interface SkipTagResult {
455
+ position: number;
456
+ selfClosing: boolean;
457
+ }
458
+
459
+ const skipTag = (
460
+ source: string,
461
+ index: number,
462
+ closing: boolean
463
+ ): SkipTagResult | null => {
464
+ let i = index;
465
+ while (i < source.length) {
466
+ const ch = source[i];
467
+ if (ch === "'" || ch === '"') {
468
+ i = skipQuoted(source, i, ch);
469
+ continue;
470
+ }
471
+ if (ch === '`') {
472
+ i = skipTemplateLiteral(source, i);
473
+ continue;
474
+ }
475
+ if (ch === '{') {
476
+ i = skipJsExpression(source, i + 1);
477
+ continue;
478
+ }
479
+ if (ch === '/' && source[i + 1] === '*') {
480
+ i = skipBlockComment(source, i + 2);
481
+ continue;
482
+ }
483
+ if (ch === '/' && source[i + 1] === '/') {
484
+ i = skipLineComment(source, i + 2);
485
+ continue;
486
+ }
487
+ if (ch === '>') {
488
+ const selfClosing = !closing && source[i - 1] === '/';
489
+ return { position: i + 1, selfClosing };
490
+ }
491
+ i++;
492
+ }
493
+ return null;
494
+ };
495
+
496
+ const extractTemplate = (
497
+ source: string,
498
+ start: number
499
+ ): TemplateMatch | null => {
500
+ const len = source.length;
501
+ let pos = start;
502
+ let depth = 0;
503
+
504
+ while (pos < len) {
505
+ const ch = source[pos];
506
+
507
+ if (ch === "'" || ch === '"') {
508
+ pos = skipQuoted(source, pos, ch);
509
+ continue;
510
+ }
511
+ if (ch === '`') {
512
+ pos = skipTemplateLiteral(source, pos);
513
+ continue;
514
+ }
515
+ if (ch === '/' && source[pos + 1] === '*') {
516
+ pos = skipBlockComment(source, pos + 2);
517
+ continue;
518
+ }
519
+ if (ch === '/' && source[pos + 1] === '/') {
520
+ pos = skipLineComment(source, pos + 2);
521
+ continue;
522
+ }
523
+
524
+ if (ch === '<') {
525
+ if (source.startsWith('<!--', pos)) {
526
+ pos = skipHtmlComment(source, pos + 4);
527
+ continue;
528
+ }
529
+
530
+ if (source[pos + 1] === '/') {
531
+ depth--;
532
+ const res = skipTag(source, pos + 2, true);
533
+ if (!res) {
534
+ return null;
535
+ }
536
+ pos = res.position;
537
+ if (depth === 0) {
538
+ return {
539
+ start,
540
+ end: pos,
541
+ snippet: source.slice(start, pos),
542
+ };
543
+ }
544
+ continue;
545
+ } else {
546
+ depth++;
547
+ const res = skipTag(source, pos + 1, false);
548
+ if (!res) {
549
+ return null;
550
+ }
551
+ pos = res.position;
552
+ if (res.selfClosing) {
553
+ depth--;
554
+ if (depth === 0) {
555
+ return {
556
+ start,
557
+ end: pos,
558
+ snippet: source.slice(start, pos),
559
+ };
560
+ }
561
+ }
562
+ continue;
563
+ }
564
+ }
565
+
566
+ pos++;
567
+ }
568
+
569
+ return null;
570
+ };
571
+
572
+ const scanTemplates = (code: string): TemplateMatch[] => {
573
+ const matches: TemplateMatch[] = [];
574
+ const len = code.length;
575
+ let index = 0;
576
+
577
+ while (index < len) {
578
+ const ch = code[index];
579
+
580
+ if (ch === "'" || ch === '"') {
581
+ index = skipQuoted(code, index, ch);
582
+ continue;
583
+ }
584
+ if (ch === '`') {
585
+ index = skipTemplateLiteral(code, index);
586
+ continue;
587
+ }
588
+ if (ch === '/' && code[index + 1] === '*') {
589
+ index = skipBlockComment(code, index + 2);
590
+ continue;
591
+ }
592
+ if (ch === '/' && code[index + 1] === '/') {
593
+ index = skipLineComment(code, index + 2);
594
+ continue;
595
+ }
596
+
597
+ if (ch === '<' && shouldAttemptTemplate(code, index)) {
598
+ const match = extractTemplate(code, index);
599
+ if (match) {
600
+ matches.push(match);
601
+ index = match.end;
602
+ continue;
603
+ }
604
+ }
605
+
606
+ index++;
607
+ }
608
+
609
+ return matches;
610
+ };
611
+
612
+ function transformExpression(
613
+ expression: string,
614
+ options: Required<GenerateOptions>
615
+ ): string {
616
+ if (!expression.includes('<')) {
617
+ return expression;
618
+ }
619
+
620
+ const matches = scanTemplates(expression);
621
+ if (matches.length === 0) {
622
+ return expression;
623
+ }
624
+
625
+ let result = '';
626
+ let lastIndex = 0;
627
+ let changed = false;
628
+
629
+ for (const match of matches) {
630
+ let replacement: string | null = null;
631
+ try {
632
+ replacement = compileEmbeddedTemplate(match.snippet, options);
633
+ } catch {
634
+ replacement = null;
635
+ }
636
+
637
+ if (!replacement) {
638
+ result += expression.slice(lastIndex, match.end);
639
+ lastIndex = match.end;
640
+ continue;
641
+ }
642
+
643
+ result += expression.slice(lastIndex, match.start) + replacement;
644
+ lastIndex = match.end;
645
+ changed = true;
646
+ }
647
+
648
+ result += expression.slice(lastIndex);
649
+
650
+ return changed ? result : expression;
651
+ }
652
+
653
+ function compileEmbeddedTemplate(
654
+ templateSource: string,
655
+ options: Required<GenerateOptions>
656
+ ): string {
657
+ const tokens = tokenize(templateSource);
658
+ const ast = parse(tokens);
659
+ const transformedAst = transform(ast);
660
+ return generate(transformedAst, options);
661
+ }
@@ -0,0 +1 @@
1
+ export * from './createElement';
@@ -0,0 +1,70 @@
1
+ import { tokenize } from './parser/lexer';
2
+ import { parse } from './parser/parser';
3
+ import { transform, TransformOptions } from './transform';
4
+ import { generate, GenerateOptions } from './codegen';
5
+
6
+ /**
7
+ * Compile a Lithent template to JavaScript code
8
+ */
9
+ export function compile(
10
+ template: string,
11
+ options?: CompileOptions
12
+ ): CompileResult {
13
+ try {
14
+ // 1. Tokenize
15
+ const tokens = tokenize(template);
16
+
17
+ // 2. Parse
18
+ const ast = parse(tokens);
19
+
20
+ // 3. Transform
21
+ const transformedAst = transform(ast, options?.transform);
22
+
23
+ // 4. Generate
24
+ const code = generate(transformedAst, options?.generate);
25
+
26
+ return {
27
+ code,
28
+ ast: transformedAst,
29
+ errors: [],
30
+ };
31
+ } catch (error) {
32
+ const err = error as Error & { line?: number; column?: number };
33
+ return {
34
+ code: '',
35
+ ast: null,
36
+ errors: [
37
+ {
38
+ message: error instanceof Error ? error.message : String(error),
39
+ line: err.line ?? 0,
40
+ column: err.column ?? 0,
41
+ },
42
+ ],
43
+ };
44
+ }
45
+ }
46
+
47
+ export interface CompileOptions {
48
+ transform?: TransformOptions;
49
+ generate?: GenerateOptions;
50
+ }
51
+
52
+ export interface CompileResult {
53
+ code: string;
54
+ ast: any;
55
+ errors: CompileError[];
56
+ }
57
+
58
+ export interface CompileError {
59
+ message: string;
60
+ line: number;
61
+ column: number;
62
+ }
63
+
64
+ // Re-export all modules
65
+ export * from './parser/tokens';
66
+ export * from './parser/lexer';
67
+ export * from './parser/parser';
68
+ export * from './parser/ast';
69
+ export * from './transform';
70
+ export * from './codegen';