@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,561 @@
1
+ import {
2
+ Token,
3
+ TokenType,
4
+ Position,
5
+ createPosition,
6
+ createToken,
7
+ } from './tokens';
8
+
9
+ /**
10
+ * Lexer for Lithent templates
11
+ * Converts template string into a stream of tokens
12
+ */
13
+ export class Lexer {
14
+ private input: string;
15
+ private position: number = 0;
16
+ private line: number = 1;
17
+ private column: number = 1;
18
+ private tokens: Token[] = [];
19
+
20
+ constructor(input: string) {
21
+ this.input = input;
22
+ }
23
+
24
+ /**
25
+ * Tokenize the entire input
26
+ */
27
+ public tokenize(): Token[] {
28
+ while (this.position < this.input.length) {
29
+ this.readToken();
30
+ }
31
+
32
+ // Add EOF token
33
+ const pos = this.getCurrentPosition();
34
+ this.tokens.push(createToken(TokenType.EOF, '', pos, pos));
35
+
36
+ return this.tokens;
37
+ }
38
+
39
+ /**
40
+ * Get current position
41
+ */
42
+ private getCurrentPosition(): Position {
43
+ return createPosition(this.line, this.column, this.position);
44
+ }
45
+
46
+ /**
47
+ * Peek at character without consuming
48
+ */
49
+ private peek(offset: number = 0): string {
50
+ return this.input[this.position + offset] || '';
51
+ }
52
+
53
+ /**
54
+ * Consume and return current character
55
+ */
56
+ private advance(): string {
57
+ const char = this.input[this.position];
58
+ this.position++;
59
+
60
+ if (char === '\n') {
61
+ this.line++;
62
+ this.column = 1;
63
+ } else {
64
+ this.column++;
65
+ }
66
+
67
+ return char;
68
+ }
69
+
70
+ /**
71
+ * Check if we've reached the end
72
+ */
73
+ private isEOF(): boolean {
74
+ return this.position >= this.input.length;
75
+ }
76
+
77
+ /**
78
+ * Read next token
79
+ */
80
+ private readToken(): void {
81
+ const char = this.peek();
82
+
83
+ // Fragment open <>
84
+ if (char === '<' && this.peek(1) === '>') {
85
+ this.readFragmentOpen();
86
+ return;
87
+ }
88
+
89
+ // Fragment close </>
90
+ if (char === '<' && this.peek(1) === '/' && this.peek(2) === '>') {
91
+ this.readFragmentClose();
92
+ return;
93
+ }
94
+
95
+ // Check for comments first
96
+ if (
97
+ char === '<' &&
98
+ this.peek(1) === '!' &&
99
+ this.peek(2) === '-' &&
100
+ this.peek(3) === '-'
101
+ ) {
102
+ this.readComment();
103
+ return;
104
+ }
105
+
106
+ // Check for tags
107
+ if (char === '<') {
108
+ if (this.peek(1) === '/') {
109
+ this.readCloseTag();
110
+ } else {
111
+ this.readOpenTag();
112
+ }
113
+ return;
114
+ }
115
+
116
+ // Check for expressions
117
+ if (char === '{') {
118
+ this.readExpression();
119
+ return;
120
+ }
121
+
122
+ // Check for whitespace
123
+ if (this.isWhitespace(char)) {
124
+ this.readWhitespace();
125
+ return;
126
+ }
127
+
128
+ // Otherwise it's text content
129
+ this.readText();
130
+ }
131
+
132
+ /**
133
+ * Read HTML comment
134
+ */
135
+ private readComment(): void {
136
+ const start = this.getCurrentPosition();
137
+ let value = '';
138
+
139
+ // Consume <!--
140
+ value += this.advance() + this.advance() + this.advance() + this.advance();
141
+
142
+ // Read until -->
143
+ while (!this.isEOF()) {
144
+ if (this.peek() === '-' && this.peek(1) === '-' && this.peek(2) === '>') {
145
+ value += this.advance() + this.advance() + this.advance();
146
+ break;
147
+ }
148
+ value += this.advance();
149
+ }
150
+
151
+ const end = this.getCurrentPosition();
152
+ this.tokens.push(createToken(TokenType.COMMENT, value, start, end));
153
+ }
154
+
155
+ /**
156
+ * Read opening tag <
157
+ */
158
+ private readOpenTag(): void {
159
+ const start = this.getCurrentPosition();
160
+ this.advance(); // consume <
161
+ const end = this.getCurrentPosition();
162
+ this.tokens.push(createToken(TokenType.TAG_OPEN_START, '<', start, end));
163
+
164
+ // Now read attributes until we hit > or />
165
+ this.readTagContent();
166
+ }
167
+
168
+ /**
169
+ * Read closing tag </
170
+ */
171
+ private readCloseTag(): void {
172
+ const start = this.getCurrentPosition();
173
+ this.advance(); // consume <
174
+ this.advance(); // consume /
175
+ const end = this.getCurrentPosition();
176
+ this.tokens.push(createToken(TokenType.TAG_CLOSE_START, '</', start, end));
177
+
178
+ // Read tag name
179
+ this.skipWhitespace();
180
+ this.readIdentifier();
181
+
182
+ // Read closing >
183
+ this.skipWhitespace();
184
+ if (this.peek() === '>') {
185
+ const start = this.getCurrentPosition();
186
+ this.advance();
187
+ const end = this.getCurrentPosition();
188
+ this.tokens.push(createToken(TokenType.TAG_OPEN_END, '>', start, end));
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Read tag content (tag name and attributes)
194
+ */
195
+ private readTagContent(): void {
196
+ this.skipWhitespace();
197
+
198
+ // Read tag name
199
+ this.readIdentifier();
200
+
201
+ // Read attributes
202
+ while (!this.isEOF()) {
203
+ this.skipWhitespace();
204
+
205
+ const char = this.peek();
206
+
207
+ // Self-closing tag
208
+ if (char === '/' && this.peek(1) === '>') {
209
+ const start = this.getCurrentPosition();
210
+ this.advance();
211
+ this.advance();
212
+ const end = this.getCurrentPosition();
213
+ this.tokens.push(
214
+ createToken(TokenType.TAG_SELF_CLOSE, '/>', start, end)
215
+ );
216
+ return;
217
+ }
218
+
219
+ // End of opening tag
220
+ if (char === '>') {
221
+ const start = this.getCurrentPosition();
222
+ this.advance();
223
+ const end = this.getCurrentPosition();
224
+ this.tokens.push(createToken(TokenType.TAG_OPEN_END, '>', start, end));
225
+ return;
226
+ }
227
+
228
+ // Read attribute
229
+ this.readAttribute();
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Read an attribute (name and optional value)
235
+ */
236
+ private readAttribute(): void {
237
+ const identifier = this.readIdentifier();
238
+ if (!identifier) return;
239
+
240
+ this.skipWhitespace();
241
+
242
+ // Check for = sign
243
+ if (this.peek() === '=') {
244
+ const start = this.getCurrentPosition();
245
+ this.advance();
246
+ const end = this.getCurrentPosition();
247
+ this.tokens.push(
248
+ createToken(TokenType.ATTRIBUTE_EQUALS, '=', start, end)
249
+ );
250
+
251
+ this.skipWhitespace();
252
+
253
+ // Read attribute value
254
+ const char = this.peek();
255
+ if (char === '"' || char === "'") {
256
+ this.readStringLiteral();
257
+ } else if (char === '{') {
258
+ this.readExpression();
259
+ }
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Read identifier (tag name or attribute name)
265
+ */
266
+ private readIdentifier(): string {
267
+ const start = this.getCurrentPosition();
268
+ let value = '';
269
+
270
+ // First character must be letter, underscore, or $ (or : for namespaced attributes)
271
+ const first = this.peek();
272
+ if (this.isIdentifierStart(first)) {
273
+ value += this.advance();
274
+ } else {
275
+ // Consume unexpected character to avoid infinite loops
276
+ const unexpectedStart = this.getCurrentPosition();
277
+ const char = this.advance();
278
+ const unexpectedEnd = this.getCurrentPosition();
279
+ this.tokens.push(
280
+ createToken(TokenType.TEXT, char, unexpectedStart, unexpectedEnd)
281
+ );
282
+ return '';
283
+ }
284
+
285
+ // Read rest of identifier
286
+ while (!this.isEOF() && this.isIdentifierPart(this.peek())) {
287
+ value += this.advance();
288
+ }
289
+
290
+ const end = this.getCurrentPosition();
291
+
292
+ // Check if this is a directive or special keyword
293
+ const tokenType = this.getIdentifierTokenType(value);
294
+ this.tokens.push(createToken(tokenType, value, start, end));
295
+
296
+ return value;
297
+ }
298
+
299
+ /**
300
+ * Determine token type for identifier
301
+ */
302
+ private getIdentifierTokenType(value: string): TokenType {
303
+ switch (value) {
304
+ case 'l-if':
305
+ return TokenType.DIRECTIVE_IF;
306
+ case 'l-else-if':
307
+ return TokenType.DIRECTIVE_ELSE_IF;
308
+ case 'l-else':
309
+ return TokenType.DIRECTIVE_ELSE;
310
+ case 'l-for':
311
+ return TokenType.DIRECTIVE_FOR;
312
+ case 'ref':
313
+ return TokenType.KEYWORD_REF;
314
+ default:
315
+ return TokenType.IDENTIFIER;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Read string literal
321
+ */
322
+ private readStringLiteral(): void {
323
+ const start = this.getCurrentPosition();
324
+ const quote = this.advance(); // " or '
325
+ let value = quote;
326
+ let escaped = false;
327
+
328
+ while (!this.isEOF()) {
329
+ const char = this.peek();
330
+
331
+ if (escaped) {
332
+ value += this.advance();
333
+ escaped = false;
334
+ continue;
335
+ }
336
+
337
+ if (char === '\\') {
338
+ escaped = true;
339
+ value += this.advance();
340
+ continue;
341
+ }
342
+
343
+ if (char === quote) {
344
+ value += this.advance();
345
+ break;
346
+ }
347
+
348
+ value += this.advance();
349
+ }
350
+
351
+ const end = this.getCurrentPosition();
352
+ this.tokens.push(createToken(TokenType.STRING_LITERAL, value, start, end));
353
+ }
354
+
355
+ /**
356
+ * Read fragment open <>
357
+ */
358
+ private readFragmentOpen(): void {
359
+ const start = this.getCurrentPosition();
360
+ this.advance(); // <
361
+ this.advance(); // >
362
+ const end = this.getCurrentPosition();
363
+ this.tokens.push(createToken(TokenType.FRAGMENT_OPEN, '<>', start, end));
364
+ }
365
+
366
+ /**
367
+ * Read fragment close </>
368
+ */
369
+ private readFragmentClose(): void {
370
+ const start = this.getCurrentPosition();
371
+ this.advance(); // <
372
+ this.advance(); // /
373
+ this.advance(); // >
374
+ const end = this.getCurrentPosition();
375
+ this.tokens.push(createToken(TokenType.FRAGMENT_CLOSE, '</>', start, end));
376
+ }
377
+
378
+ /**
379
+ * Read expression content inside {}
380
+ */
381
+ private readExpression(): void {
382
+ const startPos = this.getCurrentPosition();
383
+ this.advance(); // consume {
384
+ const openEnd = this.getCurrentPosition();
385
+ this.tokens.push(
386
+ createToken(TokenType.EXPRESSION_START, '{', startPos, openEnd)
387
+ );
388
+
389
+ let value = '';
390
+ let depth = 1;
391
+ let inString: string | null = null;
392
+ let escaped = false;
393
+ let contentStart: Position | null = null;
394
+ let contentEnd: Position = openEnd;
395
+
396
+ const consumeChar = (): string => {
397
+ const charStart = this.getCurrentPosition();
398
+ const char = this.advance();
399
+ const charEnd = this.getCurrentPosition();
400
+
401
+ value += char;
402
+ if (!contentStart) {
403
+ contentStart = charStart;
404
+ }
405
+ contentEnd = charEnd;
406
+
407
+ return char;
408
+ };
409
+
410
+ while (!this.isEOF() && depth > 0) {
411
+ const char = this.peek();
412
+
413
+ // Handle string literals inside expressions
414
+ if (inString) {
415
+ if (escaped) {
416
+ consumeChar();
417
+ escaped = false;
418
+ continue;
419
+ }
420
+ if (char === '\\') {
421
+ escaped = true;
422
+ consumeChar();
423
+ continue;
424
+ }
425
+ if (char === inString) {
426
+ consumeChar();
427
+ inString = null;
428
+ continue;
429
+ }
430
+ consumeChar();
431
+ continue;
432
+ }
433
+
434
+ // Not in string
435
+ if (char === '"' || char === "'" || char === '`') {
436
+ inString = char;
437
+ consumeChar();
438
+ continue;
439
+ }
440
+
441
+ if (char === '{') {
442
+ depth++;
443
+ consumeChar();
444
+ continue;
445
+ }
446
+
447
+ if (char === '}') {
448
+ depth--;
449
+ if (depth === 0) {
450
+ // End of expression
451
+ if (value.trim()) {
452
+ const start = contentStart ?? openEnd;
453
+ const end = contentEnd;
454
+ this.tokens.push(
455
+ createToken(TokenType.EXPRESSION_CONTENT, value, start, end)
456
+ );
457
+ }
458
+
459
+ const closeStart = this.getCurrentPosition();
460
+ this.advance(); // consume }
461
+ const closeEnd = this.getCurrentPosition();
462
+ this.tokens.push(
463
+ createToken(TokenType.EXPRESSION_END, '}', closeStart, closeEnd)
464
+ );
465
+ return;
466
+ }
467
+ consumeChar();
468
+ continue;
469
+ }
470
+
471
+ consumeChar();
472
+ }
473
+
474
+ // If we get here, expression wasn't closed properly
475
+ // Still add the content we have
476
+ if (value.trim()) {
477
+ const start = contentStart ?? openEnd;
478
+ const end = contentStart ? contentEnd : this.getCurrentPosition();
479
+ this.tokens.push(
480
+ createToken(TokenType.EXPRESSION_CONTENT, value, start, end)
481
+ );
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Read text content
487
+ */
488
+ private readText(): void {
489
+ const start = this.getCurrentPosition();
490
+ let value = '';
491
+
492
+ while (!this.isEOF()) {
493
+ const char = this.peek();
494
+
495
+ // Stop at tag or expression start
496
+ if (char === '<' || char === '{') {
497
+ break;
498
+ }
499
+
500
+ value += this.advance();
501
+ }
502
+
503
+ if (value) {
504
+ const end = this.getCurrentPosition();
505
+ this.tokens.push(createToken(TokenType.TEXT, value, start, end));
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Read whitespace
511
+ */
512
+ private readWhitespace(): void {
513
+ const start = this.getCurrentPosition();
514
+ let value = '';
515
+
516
+ while (!this.isEOF() && this.isWhitespace(this.peek())) {
517
+ value += this.advance();
518
+ }
519
+
520
+ const end = this.getCurrentPosition();
521
+ this.tokens.push(createToken(TokenType.WHITESPACE, value, start, end));
522
+ }
523
+
524
+ /**
525
+ * Skip whitespace without creating tokens
526
+ */
527
+ private skipWhitespace(): void {
528
+ while (!this.isEOF() && this.isWhitespace(this.peek())) {
529
+ this.advance();
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Check if character is whitespace
535
+ */
536
+ private isWhitespace(char: string): boolean {
537
+ return /\s/.test(char);
538
+ }
539
+
540
+ /**
541
+ * Check if character can start an identifier
542
+ */
543
+ private isIdentifierStart(char: string): boolean {
544
+ return /[a-zA-Z_$:]/.test(char);
545
+ }
546
+
547
+ /**
548
+ * Check if character can be part of an identifier
549
+ */
550
+ private isIdentifierPart(char: string): boolean {
551
+ return /[a-zA-Z0-9_$:.-]/.test(char);
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Tokenize a template string
557
+ */
558
+ export function tokenize(input: string): Token[] {
559
+ const lexer = new Lexer(input);
560
+ return lexer.tokenize();
561
+ }