@lithent/lithent-template-parser 0.1.0 → 0.1.1

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/README.md +1 -1
  2. package/dist/codegen/createElement.d.cts +10 -0
  3. package/dist/codegen/createElement.d.ts +1 -1
  4. package/dist/codegen/index.d.cts +1 -0
  5. package/dist/codegen/index.d.ts +1 -1
  6. package/dist/compiler.d.cts +26 -0
  7. package/dist/compiler.d.ts +8 -8
  8. package/dist/docPipe/index.d.cts +40 -0
  9. package/dist/docPipe/index.d.ts +2 -2
  10. package/dist/docPipe/scan.d.cts +8 -0
  11. package/dist/docPipe/scan.d.ts +2 -0
  12. package/dist/index.d.cts +13 -0
  13. package/dist/index.d.ts +9 -9
  14. package/dist/index.mjs +955 -1192
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/index.umd.cjs +12 -12
  17. package/dist/index.umd.cjs.map +1 -1
  18. package/dist/parser/ast.d.cts +207 -0
  19. package/dist/parser/ast.d.ts +1 -1
  20. package/dist/parser/lexer.d.cts +109 -0
  21. package/dist/parser/lexer.d.ts +1 -1
  22. package/dist/parser/parser.d.cts +80 -0
  23. package/dist/parser/parser.d.ts +5 -4
  24. package/dist/parser/tokens.d.cts +51 -0
  25. package/dist/transform/directives.d.cts +30 -0
  26. package/dist/transform/directives.d.ts +1 -1
  27. package/dist/transform/index.d.cts +14 -0
  28. package/dist/transform/index.d.ts +5 -5
  29. package/dist/transform/optimize.d.cts +21 -0
  30. package/dist/transform/optimize.d.ts +1 -1
  31. package/package.json +2 -2
  32. package/src/codegen/createElement.ts +48 -332
  33. package/src/compiler.ts +1 -1
  34. package/src/docPipe/scan.ts +209 -246
  35. package/src/parser/lexer.ts +41 -91
  36. package/src/parser/parser.ts +45 -39
  37. package/src/tests/compiler.test.ts +2 -2
  38. package/src/tests/parser.test.ts +2 -2
  39. package/src/tests/regressions.test.ts +200 -0
  40. package/src/transform/directives.ts +25 -2
  41. package/src/transform/optimize.ts +37 -25
@@ -11,6 +11,7 @@ import {
11
11
  import { tokenize } from '../parser/lexer';
12
12
  import { parse } from '../parser/parser';
13
13
  import { getConditionalGroup, getDirective, transform } from '../transform';
14
+ import { scanTemplates } from '../docPipe/scan';
14
15
 
15
16
  /**
16
17
  * Generate JavaScript code from AST
@@ -86,12 +87,6 @@ function generateElement(
86
87
  node: ElementNode,
87
88
  options: Required<GenerateOptions>
88
89
  ): 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
90
  // Check if this element has l-for directive
96
91
  const forDirective = node.directives.find(
97
92
  d => d.type === NodeType.DIRECTIVE_FOR
@@ -101,7 +96,11 @@ function generateElement(
101
96
  return generateForLoop(node, forDirective, options);
102
97
  }
103
98
 
104
- // Normal element
99
+ const conditionalGroup = getConditionalGroup(node);
100
+ if (conditionalGroup) {
101
+ return generateConditionalGroup(conditionalGroup, options);
102
+ }
103
+
105
104
  return generateNormalElement(node, options);
106
105
  }
107
106
 
@@ -166,7 +165,10 @@ function generateProps(
166
165
  }
167
166
 
168
167
  const props = node.attributes.map(attr => {
169
- const key = attr.name;
168
+ const key =
169
+ /^[A-Za-z_$][\w$]*$/.test(attr.name) && attr.name !== '__proto__'
170
+ ? attr.name
171
+ : `[${stringLiteral(attr.name)}]`;
170
172
  let value: string;
171
173
 
172
174
  if (attr.value === null) {
@@ -174,10 +176,12 @@ function generateProps(
174
176
  value = 'true';
175
177
  } else if (typeof attr.value === 'string') {
176
178
  // Static string value
177
- value = `'${attr.value.replace(/'/g, "\\'")}'`;
179
+ value = stringLiteral(attr.value);
178
180
  } else {
179
181
  // Dynamic expression
180
- value = transformExpression(attr.value.expression, options);
182
+ value = expressionCode(
183
+ transformExpression(attr.value.expression, options)
184
+ );
181
185
  }
182
186
 
183
187
  return `${key}: ${value}`;
@@ -190,13 +194,7 @@ function generateProps(
190
194
  * Generate code for text node
191
195
  */
192
196
  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}'`;
197
+ return stringLiteral(node.content);
200
198
  }
201
199
 
202
200
  /**
@@ -206,7 +204,7 @@ function generateInterpolation(
206
204
  node: InterpolationNode,
207
205
  options: Required<GenerateOptions>
208
206
  ): string {
209
- return transformExpression(node.expression, options);
207
+ return expressionCode(transformExpression(node.expression, options));
210
208
  }
211
209
 
212
210
  /**
@@ -218,6 +216,7 @@ function generateForLoop(
218
216
  options: Required<GenerateOptions>
219
217
  ): string {
220
218
  const { item, index, list } = directive;
219
+ const listCode = transformExpression(list, options);
221
220
 
222
221
  // Generate the element code (without the for directive)
223
222
  const elementWithoutFor: ElementNode = {
@@ -225,13 +224,25 @@ function generateForLoop(
225
224
  directives: node.directives.filter(d => d.type !== NodeType.DIRECTIVE_FOR),
226
225
  };
227
226
 
228
- const elementCode = generateNormalElement(elementWithoutFor, options);
227
+ const group = getConditionalGroup(node);
228
+ if (group) {
229
+ (elementWithoutFor as any).__conditionalGroup = {
230
+ ...group,
231
+ if: {
232
+ ...group.if,
233
+ directives: group.if.directives.filter(
234
+ d => d.type !== NodeType.DIRECTIVE_FOR
235
+ ),
236
+ },
237
+ };
238
+ }
239
+ const elementCode = generateElement(elementWithoutFor, options);
229
240
 
230
241
  // Generate map function
231
242
  if (index) {
232
- return `(${list}).map((${item}, ${index}) => ${elementCode})`;
243
+ return `(${listCode}).map((${item}, ${index}) => ${elementCode})`;
233
244
  } else {
234
- return `(${list}).map(${item} => ${elementCode})`;
245
+ return `(${listCode}).map(${item} => ${elementCode})`;
235
246
  }
236
247
  }
237
248
 
@@ -255,7 +266,8 @@ function generateConditionalGroup(
255
266
  ),
256
267
  };
257
268
 
258
- let code = `(${ifDirective.condition}) ? ${generateNormalElement(ifElementWithoutDirective, options)}`;
269
+ delete (ifElementWithoutDirective as any).__conditionalGroup;
270
+ let code = `(${transformExpression(ifDirective.condition, options)}) ? ${generateElement(ifElementWithoutDirective, options)}`;
259
271
 
260
272
  // Add else-if conditions
261
273
  for (const elseIfNode of group.elseIfs || []) {
@@ -272,7 +284,7 @@ function generateConditionalGroup(
272
284
  ),
273
285
  };
274
286
 
275
- code += ` : (${elseIfDirective.condition}) ? ${generateNormalElement(elseIfElementWithoutDirective, options)}`;
287
+ code += ` : (${transformExpression(elseIfDirective.condition, options)}) ? ${generateElement(elseIfElementWithoutDirective, options)}`;
276
288
  }
277
289
 
278
290
  // Add else condition
@@ -284,7 +296,7 @@ function generateConditionalGroup(
284
296
  ),
285
297
  };
286
298
 
287
- code += ` : ${generateNormalElement(elseElementWithoutDirective, options)}`;
299
+ code += ` : ${generateElement(elseElementWithoutDirective, options)}`;
288
300
  } else {
289
301
  code += ' : null';
290
302
  }
@@ -298,317 +310,21 @@ export function formatCode(code: string): string {
298
310
  return code;
299
311
  }
300
312
 
301
- interface TemplateMatch {
302
- start: number;
303
- end: number;
304
- snippet: string;
313
+ function stringLiteral(value: string): string {
314
+ return `'${value
315
+ .replace(/\\/g, '\\\\')
316
+ .replace(/'/g, "\\'")
317
+ .replace(/\r/g, '\\r')
318
+ .replace(/\n/g, '\\n')
319
+ .replace(/\u2028/g, '\\u2028')
320
+ .replace(/\u2029/g, '\\u2029')}'`;
305
321
  }
306
322
 
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;
323
+ function expressionCode(expression: string): string {
324
+ // Preserve expression boundaries when inserting into arguments or properties.
325
+ return /^[A-Za-z_$][\w$]*$/.test(expression) ? expression : `(${expression})`;
457
326
  }
458
327
 
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
328
  function transformExpression(
613
329
  expression: string,
614
330
  options: Required<GenerateOptions>
@@ -655,7 +371,7 @@ function compileEmbeddedTemplate(
655
371
  options: Required<GenerateOptions>
656
372
  ): string {
657
373
  const tokens = tokenize(templateSource);
658
- const ast = parse(tokens);
374
+ const ast = parse(tokens, true);
659
375
  const transformedAst = transform(ast);
660
376
  return generate(transformedAst, options);
661
377
  }
package/src/compiler.ts CHANGED
@@ -15,7 +15,7 @@ export function compile(
15
15
  const tokens = tokenize(template);
16
16
 
17
17
  // 2. Parse
18
- const ast = parse(tokens);
18
+ const ast = parse(tokens, true);
19
19
 
20
20
  // 3. Transform
21
21
  const transformedAst = transform(ast, options?.transform);