@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
@@ -1,3 +1,4 @@
1
+ import { skipJsExpression } from '../docPipe/scan';
1
2
  import {
2
3
  Token,
3
4
  TokenType,
@@ -139,16 +140,23 @@ export class Lexer {
139
140
  // Consume <!--
140
141
  value += this.advance() + this.advance() + this.advance() + this.advance();
141
142
 
143
+ let closed = false;
142
144
  // Read until -->
143
145
  while (!this.isEOF()) {
144
146
  if (this.peek() === '-' && this.peek(1) === '-' && this.peek(2) === '>') {
145
147
  value += this.advance() + this.advance() + this.advance();
148
+ closed = true;
146
149
  break;
147
150
  }
148
151
  value += this.advance();
149
152
  }
150
153
 
151
154
  const end = this.getCurrentPosition();
155
+ if (!closed)
156
+ throw Object.assign(new Error('Unclosed comment'), {
157
+ line: start.line,
158
+ column: start.column,
159
+ });
152
160
  this.tokens.push(createToken(TokenType.COMMENT, value, start, end));
153
161
  }
154
162
 
@@ -324,6 +332,7 @@ export class Lexer {
324
332
  const quote = this.advance(); // " or '
325
333
  let value = quote;
326
334
  let escaped = false;
335
+ let closed = false;
327
336
 
328
337
  while (!this.isEOF()) {
329
338
  const char = this.peek();
@@ -342,6 +351,7 @@ export class Lexer {
342
351
 
343
352
  if (char === quote) {
344
353
  value += this.advance();
354
+ closed = true;
345
355
  break;
346
356
  }
347
357
 
@@ -349,6 +359,11 @@ export class Lexer {
349
359
  }
350
360
 
351
361
  const end = this.getCurrentPosition();
362
+ if (!closed)
363
+ throw Object.assign(new Error('Unclosed attribute string'), {
364
+ line: start.line,
365
+ column: start.column,
366
+ });
352
367
  this.tokens.push(createToken(TokenType.STRING_LITERAL, value, start, end));
353
368
  }
354
369
 
@@ -386,100 +401,35 @@ export class Lexer {
386
401
  createToken(TokenType.EXPRESSION_START, '{', startPos, openEnd)
387
402
  );
388
403
 
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();
404
+ const contentStart = this.getCurrentPosition();
405
+ const end = skipJsExpression(this.input, this.position);
406
+ if (end === null) {
407
+ throw Object.assign(new Error('Unclosed expression'), {
408
+ line: startPos.line,
409
+ column: startPos.column,
410
+ });
472
411
  }
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();
412
+ const value = this.input.slice(this.position, end - 1);
413
+ while (this.position < end - 1) this.advance();
414
+ const contentEnd = this.getCurrentPosition();
415
+ if (value.trim())
479
416
  this.tokens.push(
480
- createToken(TokenType.EXPRESSION_CONTENT, value, start, end)
417
+ createToken(
418
+ TokenType.EXPRESSION_CONTENT,
419
+ value,
420
+ contentStart,
421
+ contentEnd
422
+ )
481
423
  );
482
- }
424
+ this.advance();
425
+ this.tokens.push(
426
+ createToken(
427
+ TokenType.EXPRESSION_END,
428
+ '}',
429
+ contentEnd,
430
+ this.getCurrentPosition()
431
+ )
432
+ );
483
433
  }
484
434
 
485
435
  /**
@@ -25,9 +25,11 @@ export class Parser {
25
25
  private tokens: Token[];
26
26
  private position: number = 0;
27
27
 
28
- constructor(tokens: Token[]) {
28
+ constructor(tokens: Token[], preserveWhitespace = false) {
29
29
  // Filter out whitespace tokens for easier parsing
30
- this.tokens = tokens.filter(t => t.type !== TokenType.WHITESPACE);
30
+ this.tokens = preserveWhitespace
31
+ ? tokens
32
+ : tokens.filter(t => t.type !== TokenType.WHITESPACE);
31
33
  }
32
34
 
33
35
  /**
@@ -67,18 +69,16 @@ export class Parser {
67
69
  case TokenType.FRAGMENT_OPEN:
68
70
  return this.parseFragment();
69
71
  case TokenType.TEXT:
72
+ case TokenType.WHITESPACE:
70
73
  return this.parseText();
71
74
  case TokenType.EXPRESSION_START:
72
75
  return this.parseInterpolation();
73
76
  case TokenType.COMMENT:
74
77
  return this.parseComment();
75
78
  case TokenType.TAG_CLOSE_START:
76
- // Unexpected closing tag - skip it
77
- this.advance();
78
- return null;
79
+ throw this.error('Unexpected closing tag', token);
79
80
  case TokenType.FRAGMENT_CLOSE:
80
- this.advance();
81
- return null;
81
+ throw this.error('Unexpected fragment closing tag', token);
82
82
  case TokenType.EOF:
83
83
  return null;
84
84
  default:
@@ -108,9 +108,10 @@ export class Parser {
108
108
  return createFragmentNode(children, start, closeToken.end);
109
109
  }
110
110
 
111
- throw new Error(
112
- `Unclosed fragment starting at line ${start.line}, column ${start.column}`
113
- );
111
+ throw Object.assign(new Error('Unclosed fragment'), {
112
+ line: start.line,
113
+ column: start.column,
114
+ });
114
115
  }
115
116
 
116
117
  /**
@@ -204,17 +205,7 @@ export class Parser {
204
205
  );
205
206
  }
206
207
 
207
- // No closing tag found
208
- const end = children.length > 0 ? children[children.length - 1].end : start;
209
- return createElementNode(
210
- tag,
211
- attributes,
212
- directives,
213
- children,
214
- false,
215
- start,
216
- end
217
- );
208
+ throw this.error(`Unclosed tag <${tag}>`, tagToken);
218
209
  }
219
210
 
220
211
  /**
@@ -298,17 +289,14 @@ export class Parser {
298
289
  isDynamic = true;
299
290
  this.advance();
300
291
  } else {
301
- value = { expression: '' };
302
- isDynamic = true;
303
- }
304
-
305
- if (this.check(TokenType.EXPRESSION_END)) {
306
- end = this.peek().end;
307
- this.advance(); // consume }
292
+ throw this.error(
293
+ 'Expected a non-empty attribute expression',
294
+ exprToken
295
+ );
308
296
  }
297
+ end = this.expect(TokenType.EXPRESSION_END).end;
309
298
  } else {
310
- // Unexpected token
311
- return createAttributeNode(name, null, false, start, nameToken.end);
299
+ throw this.error('Expected a quoted attribute or expression', valueToken);
312
300
  }
313
301
 
314
302
  return createAttributeNode(name, value, isDynamic, start, end);
@@ -335,6 +323,8 @@ export class Parser {
335
323
  this.advance();
336
324
  }
337
325
 
326
+ if (!condition)
327
+ throw this.error('Expected a non-empty condition', exprToken);
338
328
  const end = this.peek().end;
339
329
  this.expect(TokenType.EXPRESSION_END);
340
330
 
@@ -364,7 +354,10 @@ export class Parser {
364
354
  this.expect(TokenType.EXPRESSION_END);
365
355
 
366
356
  // Parse for expression: "item in list" or "(item, index) in list"
367
- const { item, index, list } = this.parseForExpression(expression);
357
+ const { item, index, list } = this.parseForExpression(
358
+ expression,
359
+ exprToken
360
+ );
368
361
 
369
362
  return createDirectiveForNode(item, list, index, start, end);
370
363
  }
@@ -377,7 +370,10 @@ export class Parser {
377
370
  /**
378
371
  * Parse l-for expression
379
372
  */
380
- private parseForExpression(expression: string): {
373
+ private parseForExpression(
374
+ expression: string,
375
+ token: Token
376
+ ): {
381
377
  item: string;
382
378
  index?: string;
383
379
  list: string;
@@ -408,11 +404,10 @@ export class Parser {
408
404
  };
409
405
  }
410
406
 
411
- // Fallback
412
- return {
413
- item: 'item',
414
- list: expression,
415
- };
407
+ throw this.error(
408
+ 'Invalid l-for: expected item in list or (item, index) in list',
409
+ token
410
+ );
416
411
  }
417
412
 
418
413
  /**
@@ -439,6 +434,8 @@ export class Parser {
439
434
  this.advance();
440
435
  }
441
436
 
437
+ if (!expression)
438
+ throw this.error('Expected a non-empty interpolation', exprToken);
442
439
  const end = this.peek().end;
443
440
  this.expect(TokenType.EXPRESSION_END);
444
441
 
@@ -455,6 +452,15 @@ export class Parser {
455
452
  return createCommentNode(content, token.start, token.end);
456
453
  }
457
454
 
455
+ private error(message: string, token: Token): Error {
456
+ return Object.assign(
457
+ new Error(
458
+ `${message} at line ${token.start.line}, column ${token.start.column}`
459
+ ),
460
+ { line: token.start.line, column: token.start.column }
461
+ );
462
+ }
463
+
458
464
  /**
459
465
  * Peek at current token without consuming
460
466
  */
@@ -508,7 +514,7 @@ export class Parser {
508
514
  /**
509
515
  * Parse template string into AST
510
516
  */
511
- export function parse(tokens: Token[]): RootNode {
512
- const parser = new Parser(tokens);
517
+ export function parse(tokens: Token[], preserveWhitespace = false): RootNode {
518
+ const parser = new Parser(tokens, preserveWhitespace);
513
519
  return parser.parse();
514
520
  }
@@ -351,7 +351,7 @@ describe('Compiler', () => {
351
351
  </ul>
352
352
  </article>
353
353
  </main>
354
- <footer l-else-if={items.length === 0}>
354
+ <footer l-if={items.length === 0}>
355
355
  <p>No items</p>
356
356
  </footer>
357
357
  </section>
@@ -383,7 +383,7 @@ describe('Compiler', () => {
383
383
  </>
384
384
  </section>
385
385
  </div>
386
- <footer l-else>
386
+ <footer l-if={!sidebar}>
387
387
  <p>Sidebar disabled</p>
388
388
  </footer>
389
389
  </>
@@ -622,7 +622,7 @@ describe('Parser', () => {
622
622
  </ul>
623
623
  </article>
624
624
  </main>
625
- <footer l-else-if={items.length === 0}>
625
+ <footer l-if={items.length === 0}>
626
626
  <p>No items</p>
627
627
  </footer>
628
628
  </section>
@@ -654,7 +654,7 @@ describe('Parser', () => {
654
654
  </>
655
655
  </section>
656
656
  </div>
657
- <footer l-else>
657
+ <footer l-if={!sidebar}>
658
658
  <p>Sidebar disabled</p>
659
659
  </footer>
660
660
  </>
@@ -0,0 +1,200 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { h, Fragment } from 'lithent';
3
+ import { renderToString } from '../../../../ssr/src/renderToString';
4
+ import { compile, type CompileOptions } from '../compiler';
5
+ import { transformDocument } from '../docPipe';
6
+
7
+ function evaluate(
8
+ template: string,
9
+ scope: Record<string, unknown> = {},
10
+ options?: CompileOptions
11
+ ) {
12
+ const result = compile(template, options);
13
+ expect(result.errors).toEqual([]);
14
+ return new Function(
15
+ 'h',
16
+ 'Fragment',
17
+ ...Object.keys(scope),
18
+ `return (${result.code});`
19
+ )(h, Fragment, ...Object.values(scope));
20
+ }
21
+
22
+ function render(
23
+ template: string,
24
+ scope: Record<string, unknown> = {},
25
+ options?: CompileOptions
26
+ ) {
27
+ return renderToString(evaluate(template, scope, options));
28
+ }
29
+
30
+ describe('executable template regressions', () => {
31
+ it('renders hyphenated and namespaced attributes', () => {
32
+ expect(render('<div data-id="x" aria-label="hello" />')).toBe(
33
+ '<div data-id="x" aria-label="hello"></div>'
34
+ );
35
+ expect(() => evaluate('<svg xml:lang="en" />')).not.toThrow();
36
+ });
37
+
38
+ it('preserves backslashes and line breaks in static properties', () => {
39
+ expect(render(String.raw`<div title="C:\temp" />`)).toContain(
40
+ String.raw`title="C:\temp"`
41
+ );
42
+ expect(render('<div title="hello\nworld" />')).toContain(
43
+ 'title="hello\nworld"'
44
+ );
45
+ });
46
+
47
+ it('loops before evaluating conditions that use the loop variable', () => {
48
+ expect(
49
+ render(
50
+ '<ul><li l-for={item in items} l-if={item.visible}>{item.label}</li></ul>',
51
+ {
52
+ items: [
53
+ { visible: true, label: 'yes' },
54
+ { visible: false, label: 'no' },
55
+ ],
56
+ }
57
+ )
58
+ ).toBe('<ul><li>yes</li></ul>');
59
+ });
60
+
61
+ it('preserves loops on each branch of a conditional chain', () => {
62
+ const template =
63
+ '<ul><li l-if={ok}>primary</li><li l-else l-for={other in fallback}>{other}</li></ul>';
64
+ expect(
65
+ render(template, { ok: true, items: ['a', 'b'], fallback: ['c'] })
66
+ ).toBe('<ul><li>primary</li></ul>');
67
+ expect(
68
+ render(template, { ok: false, items: ['a', 'b'], fallback: ['c'] })
69
+ ).toBe('<ul><li>c</li></ul>');
70
+ });
71
+
72
+ it('allows comments and whitespace between conditional branches', () => {
73
+ const template =
74
+ '<main><div l-if={ok}>yes</div>\n<!-- comment --><div l-else>no</div></main>';
75
+ expect(render(template, { ok: true })).toBe('<main><div>yes</div></main>');
76
+ expect(render(template, { ok: false })).toBe('<main><div>no</div></main>');
77
+ });
78
+
79
+ it('preserves meaningful spaces around interpolations and inline elements', () => {
80
+ expect(render('<p>Hello {name}, welcome!</p>', { name: 'Ada' })).toBe(
81
+ '<p>Hello Ada, welcome!</p>'
82
+ );
83
+ expect(render('<p><b>Hello</b> <i>world</i></p>')).toBe(
84
+ '<p><b>Hello</b> <i>world</i></p>'
85
+ );
86
+ expect(
87
+ render('<p> Hello</p>', {}, { transform: { optimize: false } })
88
+ ).toBe('<p> Hello</p>');
89
+ });
90
+
91
+ it('preserves preformatted content and explicitly disabled whitespace trimming', () => {
92
+ expect(render('<pre>\n Hello {name}\n</pre>', { name: 'Ada' })).toBe(
93
+ '<pre>\n Hello Ada\n</pre>'
94
+ );
95
+ expect(
96
+ render(
97
+ '<p> Hello </p>',
98
+ {},
99
+ {
100
+ transform: {
101
+ optimizeOptions: { trimText: false, removeWhitespace: false },
102
+ },
103
+ }
104
+ )
105
+ ).toBe('<p> Hello </p>');
106
+ });
107
+
108
+ it('preserves comma-expression values in children and properties', () => {
109
+ expect(render('<div>{a, b}</div>', { a: 'first', b: 'second' })).toBe(
110
+ '<div>second</div>'
111
+ );
112
+ expect(render('<div title={a, b} />', { a: 'first', b: 'second' })).toBe(
113
+ '<div title="second"></div>'
114
+ );
115
+ });
116
+
117
+ it.each([
118
+ ['1 /* } */ + 2', '3'],
119
+ ['1 // }\n + 2', '3'],
120
+ ['String(/}/.test(value))', 'true'],
121
+ ['String(/[/{}]/.test(value))', 'true'],
122
+ ['8 / 2 / 2', '2'],
123
+ ['`hello ${`${value}`}`', 'hello }'],
124
+ ])('handles JavaScript lexical contexts in %s', (expression, expected) => {
125
+ expect(render(`<p>{${expression}}</p>`, { value: '}' })).toBe(
126
+ `<p>${expected}</p>`
127
+ );
128
+ });
129
+
130
+ it('compiles embedded templates after return', () => {
131
+ expect(
132
+ render(
133
+ '<div>{items.map(item => { return <span>{item}</span>; })}</div>',
134
+ { items: ['a', 'b'] }
135
+ )
136
+ ).toBe('<div><span>a</span><span>b</span></div>');
137
+ });
138
+
139
+ it.each([
140
+ '<div>hello',
141
+ '</div>',
142
+ '<!-- unclosed',
143
+ '<div title="unclosed',
144
+ '<div l-else>fallback</div>',
145
+ '<div l-else-if={ok} />',
146
+ '<li l-for={item of items}>{item}</li>',
147
+ '<div value={} />',
148
+ '<div l-if={} />',
149
+ '<div value={foo />',
150
+ '<div l-if={ok} /><span /><div l-else />',
151
+ '<><div /></div>',
152
+ '<>',
153
+ ])('reports positioned errors for malformed input %s', template => {
154
+ const result = compile(template);
155
+ expect(result.code).toBe('');
156
+ expect(result.errors.length).toBeGreaterThan(0);
157
+ expect(result.errors[0].line).toBeGreaterThan(0);
158
+ expect(result.errors[0].column).toBeGreaterThan(0);
159
+ });
160
+ });
161
+
162
+ describe('document scanning regressions', () => {
163
+ it('transforms bare return templates', () => {
164
+ const result = transformDocument('function View() { return <div />; }');
165
+ expect(result.errors).toEqual([]);
166
+ expect(result.transformed).toBe(true);
167
+ expect(new Function('h', `${result.code}; return View();`)(h).tag).toBe(
168
+ 'div'
169
+ );
170
+ });
171
+
172
+ it('does not interpret comment-looking template text as JavaScript', () => {
173
+ const result = transformDocument(
174
+ 'const View=()=> <p>https://example.com /* hello */</p>;'
175
+ );
176
+ expect(result.transformed).toBe(true);
177
+ expect(
178
+ renderToString(new Function('h', `${result.code}; return View();`)(h))
179
+ ).toBe('<p>https://example.com /* hello */</p>');
180
+ });
181
+
182
+ it('preserves regexes, strings, comments, comparisons and TypeScript generics', () => {
183
+ const source = String.raw`const re = /<img \/>/; const text = '<div />'; // <a />
184
+ const less = a < b; const identity = <T>(value: T) => value;`;
185
+ expect(transformDocument(source).code).toBe(source);
186
+ });
187
+
188
+ it('preserves regex expression statements after control parentheses and blocks', () => {
189
+ const source = String.raw`if (ok) /<img \/>/.test(html); if (ok) {} /<div \/>/.test(html);`;
190
+ expect(transformDocument(source).code).toBe(source);
191
+ });
192
+
193
+ it('transforms templates after division and preserves regexes elsewhere', () => {
194
+ const source = String.raw`const re = /<img \/>/; const n = 8 / 2; const View = () => <p>{n}</p>;`;
195
+ const result = transformDocument(source);
196
+ expect(result.transformed).toBe(true);
197
+ const view = new Function('h', `${result.code}; return View();`)(h);
198
+ expect(renderToString(view)).toBe('<p>4</p>');
199
+ });
200
+ });
@@ -46,6 +46,18 @@ function transformNodeList(nodes: TemplateNode[]): TemplateNode[] {
46
46
  children: transformNodeList(node.children),
47
47
  };
48
48
 
49
+ const orphan = node.directives.find(
50
+ d =>
51
+ d.type === NodeType.DIRECTIVE_ELSE_IF ||
52
+ d.type === NodeType.DIRECTIVE_ELSE
53
+ );
54
+ if (orphan) {
55
+ throw Object.assign(
56
+ new Error('l-else-if/l-else must follow l-if or l-else-if'),
57
+ { line: orphan.start.line, column: orphan.start.column }
58
+ );
59
+ }
60
+
49
61
  // Check if this starts a conditional chain
50
62
  const ifDirective = node.directives.find(
51
63
  d => d.type === NodeType.DIRECTIVE_IF
@@ -54,15 +66,24 @@ function transformNodeList(nodes: TemplateNode[]): TemplateNode[] {
54
66
  if (ifDirective) {
55
67
  // This starts a conditional chain, look ahead for else-if/else
56
68
  const conditionalGroup: ConditionalGroup = {
57
- if: transformedElement,
69
+ if: { ...transformedElement },
58
70
  elseIfs: [],
59
71
  };
60
72
 
61
73
  // Look ahead for else-if and else
62
74
  let j = i + 1;
75
+ let consumed = i;
63
76
  while (j < nodes.length) {
64
77
  const nextNode = nodes[j];
65
78
 
79
+ if (
80
+ nextNode.type === NodeType.COMMENT ||
81
+ (nextNode.type === NodeType.TEXT && !nextNode.content.trim())
82
+ ) {
83
+ j++;
84
+ continue;
85
+ }
86
+
66
87
  if (nextNode.type === NodeType.ELEMENT) {
67
88
  const elseIfDirective = nextNode.directives.find(
68
89
  d => d.type === NodeType.DIRECTIVE_ELSE_IF
@@ -77,6 +98,7 @@ function transformNodeList(nodes: TemplateNode[]): TemplateNode[] {
77
98
  ...nextNode,
78
99
  children: transformNodeList(nextNode.children),
79
100
  });
101
+ consumed = j;
80
102
  j++;
81
103
  continue;
82
104
  }
@@ -86,6 +108,7 @@ function transformNodeList(nodes: TemplateNode[]): TemplateNode[] {
86
108
  ...nextNode,
87
109
  children: transformNodeList(nextNode.children),
88
110
  };
111
+ consumed = j;
89
112
  j++;
90
113
  break;
91
114
  }
@@ -100,7 +123,7 @@ function transformNodeList(nodes: TemplateNode[]): TemplateNode[] {
100
123
  (transformedElement as any).__conditionalGroup = conditionalGroup;
101
124
 
102
125
  // Skip the else-if/else nodes we've processed
103
- i = j - 1;
126
+ i = consumed;
104
127
  }
105
128
 
106
129
  result.push(transformedElement);