@dom-expressions/tagged-jsx 0.50.0-next.15

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.
@@ -0,0 +1,971 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ BOOLEAN_PROP,
4
+ COMPONENT_NODE,
5
+ ELEMENT_NODE,
6
+ EXPRESSION_NODE,
7
+ ROOT_NODE,
8
+ STATIC_PROP,
9
+ TEXT_NODE,
10
+ EXPRESSION_PROP,
11
+ SPREAD_PROP,
12
+ parse
13
+ } from "../src/parse";
14
+ import { MathMLElements } from "../../runtime/src/constants";
15
+ import { tokenize } from "../src/tokenize";
16
+ import { RawTextElements, VoidElements } from "./core";
17
+
18
+ function jsx(strings: TemplateStringsArray, ...values: any[]) {
19
+ return parse(tokenize(strings, RawTextElements), VoidElements);
20
+ }
21
+
22
+ describe("Simple AST", () => {
23
+ it("simple text", () => {
24
+ const ast = jsx`Hello World!`;
25
+ expect(ast).toEqual({
26
+ type: ROOT_NODE,
27
+ children: [{ type: TEXT_NODE, value: "Hello World!" }]
28
+ });
29
+ });
30
+
31
+ it("simple element", () => {
32
+ const ast = jsx`<div></div>`;
33
+ expect(ast).toEqual({
34
+ type: ROOT_NODE,
35
+ children: [{ type: ELEMENT_NODE, name: "div", props: [], children: [] }]
36
+ });
37
+ });
38
+
39
+ it("text content", () => {
40
+ const ast = jsx`<div>Hello</div>`;
41
+ expect(ast).toEqual({
42
+ type: ROOT_NODE,
43
+ children: [
44
+ {
45
+ type: ELEMENT_NODE,
46
+ name: "div",
47
+ props: [],
48
+ children: [{ type: TEXT_NODE, value: "Hello" }]
49
+ }
50
+ ]
51
+ });
52
+ });
53
+
54
+ it("expression inside text", () => {
55
+ const name = "World";
56
+ const ast = jsx`<div>Hello ${name}</div>`;
57
+ expect(ast).toEqual({
58
+ type: ROOT_NODE,
59
+ children: [
60
+ {
61
+ type: ELEMENT_NODE,
62
+ name: "div",
63
+ props: [],
64
+ children: [
65
+ { type: TEXT_NODE, value: "Hello " },
66
+ { type: EXPRESSION_NODE, value: 0 }
67
+ ]
68
+ }
69
+ ]
70
+ });
71
+ });
72
+
73
+ it("self-closing", () => {
74
+ const ast = jsx`<input />`;
75
+ expect(ast).toEqual({
76
+ type: ROOT_NODE,
77
+ children: [{ type: ELEMENT_NODE, name: "input", props: [], children: [] }]
78
+ });
79
+ });
80
+
81
+ it("nested elements", () => {
82
+ const ast = jsx`
83
+ <div>
84
+ <span>text</span>
85
+ </div>
86
+ `;
87
+ expect(ast).toEqual({
88
+ type: ROOT_NODE,
89
+ children: [
90
+ {
91
+ type: ELEMENT_NODE,
92
+ name: "div",
93
+ props: [],
94
+ children: [
95
+ {
96
+ type: ELEMENT_NODE,
97
+ name: "span",
98
+ props: [],
99
+ children: [{ type: TEXT_NODE, value: "text" }]
100
+ }
101
+ ]
102
+ }
103
+ ]
104
+ });
105
+ });
106
+ });
107
+
108
+ describe("Attributes", () => {
109
+ it("string attribute", () => {
110
+ const ast = jsx`<div id="app"></div>`;
111
+ expect(ast).toEqual({
112
+ type: ROOT_NODE,
113
+ children: [
114
+ {
115
+ type: ELEMENT_NODE,
116
+ name: "div",
117
+ props: [{ name: "id", type: STATIC_PROP, value: "app", quote: '"' }],
118
+ children: []
119
+ }
120
+ ]
121
+ });
122
+ });
123
+
124
+ it("string attribute single quoted", () => {
125
+ const ast = jsx`<div id='app'></div>`;
126
+ expect(ast).toEqual({
127
+ type: ROOT_NODE,
128
+ children: [
129
+ {
130
+ type: ELEMENT_NODE,
131
+ name: "div",
132
+ props: [{ name: "id", type: STATIC_PROP, value: "app", quote: "'" }],
133
+ children: []
134
+ }
135
+ ]
136
+ });
137
+ });
138
+
139
+ it("boolean attribute", () => {
140
+ const ast = jsx`<input checked />`;
141
+ expect(ast).toEqual({
142
+ type: ROOT_NODE,
143
+ children: [
144
+ {
145
+ type: ELEMENT_NODE,
146
+
147
+ name: "input",
148
+ props: [{ name: "checked", type: BOOLEAN_PROP, value: true }],
149
+ children: []
150
+ }
151
+ ]
152
+ });
153
+ });
154
+
155
+ it("boolean attribute", () => {
156
+ const ast = jsx`<button checked></button>`;
157
+ expect(ast).toEqual({
158
+ type: ROOT_NODE,
159
+ children: [
160
+ {
161
+ type: ELEMENT_NODE,
162
+
163
+ name: "button",
164
+ props: [{ name: "checked", type: BOOLEAN_PROP, value: true }],
165
+ children: []
166
+ }
167
+ ]
168
+ });
169
+ });
170
+
171
+ it("expression attribute", () => {
172
+ const id = "my-id";
173
+ const ast = jsx`<div id=${id}></div>`;
174
+ expect(ast).toEqual({
175
+ type: ROOT_NODE,
176
+ children: [
177
+ {
178
+ type: ELEMENT_NODE,
179
+
180
+ name: "div",
181
+ props: [{ name: "id", type: EXPRESSION_PROP, value: 0 }],
182
+ children: []
183
+ }
184
+ ]
185
+ });
186
+ });
187
+
188
+ it("multiple attributes", () => {
189
+ const value = "test";
190
+ const ast = jsx`<input type="text" value=${value} disabled />`;
191
+ expect(ast).toEqual({
192
+ type: ROOT_NODE,
193
+ children: [
194
+ {
195
+ type: ELEMENT_NODE,
196
+ name: "input",
197
+ props: [
198
+ { name: "type", type: STATIC_PROP, value: "text", quote: '"' },
199
+ { name: "value", type: EXPRESSION_PROP, value: 0 },
200
+ { name: "disabled", type: BOOLEAN_PROP, value: true }
201
+ ],
202
+ children: []
203
+ }
204
+ ]
205
+ });
206
+ });
207
+
208
+ it("spread attribute with ...", () => {
209
+ const id = "my-id";
210
+ const ast = jsx`<div ...${id}></div>`;
211
+ expect(ast).toEqual({
212
+ type: ROOT_NODE,
213
+ children: [
214
+ {
215
+ type: ELEMENT_NODE,
216
+ name: "div",
217
+ props: [{ type: SPREAD_PROP, value: 0 }],
218
+ children: []
219
+ }
220
+ ]
221
+ });
222
+ });
223
+ });
224
+
225
+ describe("whitespace handling", () => {
226
+ it("preserves whitespace in text nodes in root", () => {
227
+ const ast = jsx` Hello <div> Hello World </div> ! `;
228
+ expect(ast).toEqual({
229
+ type: ROOT_NODE,
230
+ children: [
231
+ { type: TEXT_NODE, value: " Hello " },
232
+ {
233
+ type: ELEMENT_NODE,
234
+ name: "div",
235
+ props: [],
236
+ children: [{ type: TEXT_NODE, value: " Hello World " }]
237
+ },
238
+ { type: TEXT_NODE, value: " ! " }
239
+ ]
240
+ });
241
+ });
242
+
243
+ it("trims leading and trailing whitespace-only text nodes at root", () => {
244
+ const ast = jsx`
245
+ <div>Hello World</div>
246
+ `;
247
+ expect(ast).toEqual({
248
+ type: ROOT_NODE,
249
+ children: [
250
+ {
251
+ type: ELEMENT_NODE,
252
+ name: "div",
253
+ props: [],
254
+ children: [{ type: TEXT_NODE, value: "Hello World" }]
255
+ }
256
+ ]
257
+ });
258
+ });
259
+
260
+ it("preserves whitespace in text nodes", () => {
261
+ const ast = jsx`<div> Hello World </div>`;
262
+ expect(ast).toEqual({
263
+ type: ROOT_NODE,
264
+ children: [
265
+ {
266
+ type: ELEMENT_NODE,
267
+ name: "div",
268
+ props: [],
269
+ children: [{ type: TEXT_NODE, value: " Hello World " }]
270
+ }
271
+ ]
272
+ });
273
+ });
274
+ it("preserves whitespace in text nodes with elements", () => {
275
+ const ast = jsx`<div>
276
+ Hello World
277
+ <span>!</span>
278
+ </div>`;
279
+ expect(ast).toEqual({
280
+ type: ROOT_NODE,
281
+ children: [
282
+ {
283
+ type: ELEMENT_NODE,
284
+ name: "div",
285
+ props: [],
286
+ children: [
287
+ {
288
+ type: TEXT_NODE,
289
+ value: `
290
+ Hello World
291
+ `
292
+ },
293
+ {
294
+ type: ELEMENT_NODE,
295
+ name: "span",
296
+ props: [],
297
+ children: [{ type: TEXT_NODE, value: "!" }]
298
+ }
299
+ ]
300
+ }
301
+ ]
302
+ });
303
+ });
304
+
305
+ it("preserves whitespace in mixed text nodes", () => {
306
+ const name = "User";
307
+ const ast = jsx`<div> Hello ${name} ! </div>`;
308
+ expect(ast).toEqual({
309
+ type: ROOT_NODE,
310
+ children: [
311
+ {
312
+ type: ELEMENT_NODE,
313
+ name: "div",
314
+ props: [],
315
+ children: [
316
+ { type: TEXT_NODE, value: " Hello " },
317
+ { type: EXPRESSION_NODE, value: 0 },
318
+ { type: TEXT_NODE, value: " ! " }
319
+ ]
320
+ }
321
+ ]
322
+ });
323
+ });
324
+
325
+ it("trims whitespace-only text nodes around expressions", () => {
326
+ const name = "User";
327
+ const ast = jsx`<div>
328
+ ${name}
329
+ </div>`;
330
+ expect(ast).toEqual({
331
+ type: ROOT_NODE,
332
+ children: [
333
+ {
334
+ type: ELEMENT_NODE,
335
+ name: "div",
336
+ props: [],
337
+ children: [{ type: EXPRESSION_NODE, value: 0 }]
338
+ }
339
+ ]
340
+ });
341
+ });
342
+
343
+ it("trims whitespace-only text nodes around expressions", () => {
344
+ const name = "User";
345
+ const ast = jsx` ${name} `;
346
+ expect(ast).toEqual({
347
+ type: ROOT_NODE,
348
+ children: [
349
+ {
350
+ type: TEXT_NODE,
351
+ value: " "
352
+ },
353
+ {
354
+ type: EXPRESSION_NODE,
355
+ value: 0
356
+ },
357
+ {
358
+ type: TEXT_NODE,
359
+ value: " "
360
+ }
361
+ ]
362
+ });
363
+ });
364
+
365
+ it("filters only beginning and trailing whitespace in mixed text nodes", () => {
366
+ const name = "User";
367
+ const ast = jsx`<div> ${"Hello"} ${name} ! </div>`;
368
+ expect(ast).toEqual({
369
+ type: ROOT_NODE,
370
+ children: [
371
+ {
372
+ type: ELEMENT_NODE,
373
+ name: "div",
374
+ props: [],
375
+ children: [
376
+ { type: EXPRESSION_NODE, value: 0 },
377
+ { type: TEXT_NODE, value: " " },
378
+ { type: EXPRESSION_NODE, value: 1 },
379
+ { type: TEXT_NODE, value: " ! " }
380
+ ]
381
+ }
382
+ ]
383
+ });
384
+ });
385
+ });
386
+
387
+ describe("Complex Examples", () => {
388
+ it("JSX with multiple expressions", () => {
389
+ const title = "App";
390
+ const content = "Hello";
391
+ const count = 42;
392
+ const ast = jsx`
393
+ <div id="root">
394
+ <h1>${title}</h1>
395
+ <p>${content} - ${count}</p>
396
+ </div>
397
+ `;
398
+ expect(ast).toEqual({
399
+ type: ROOT_NODE,
400
+ children: [
401
+ {
402
+ type: ELEMENT_NODE,
403
+ name: "div",
404
+ props: [{ name: "id", type: STATIC_PROP, value: "root", quote: '"' }],
405
+ children: [
406
+ {
407
+ type: ELEMENT_NODE,
408
+ name: "h1",
409
+ props: [],
410
+ children: [{ type: EXPRESSION_NODE, value: 0 }]
411
+ },
412
+ {
413
+ type: ELEMENT_NODE,
414
+ name: "p",
415
+ props: [],
416
+ children: [
417
+ { type: EXPRESSION_NODE, value: 1 },
418
+ { type: TEXT_NODE, value: " - " },
419
+ { type: EXPRESSION_NODE, value: 2 }
420
+ ]
421
+ }
422
+ ]
423
+ }
424
+ ]
425
+ });
426
+ });
427
+
428
+ it("list-like structure", () => {
429
+ const items = ["a", "b", "c"];
430
+ const ast = jsx`
431
+ <ul>
432
+ <li>${items[0]}</li>
433
+ <li>${items[1]}</li>
434
+ <li>${items[2]}</li>
435
+ </ul>
436
+ `;
437
+ expect(ast).toEqual({
438
+ type: ROOT_NODE,
439
+ children: [
440
+ {
441
+ type: ELEMENT_NODE,
442
+ name: "ul",
443
+ props: [],
444
+ children: [
445
+ {
446
+ type: ELEMENT_NODE,
447
+ name: "li",
448
+ props: [],
449
+ children: [{ type: EXPRESSION_NODE, value: 0 }]
450
+ },
451
+ {
452
+ type: ELEMENT_NODE,
453
+ name: "li",
454
+ props: [],
455
+ children: [{ type: EXPRESSION_NODE, value: 1 }]
456
+ },
457
+ {
458
+ type: ELEMENT_NODE,
459
+ name: "li",
460
+ props: [],
461
+ children: [{ type: EXPRESSION_NODE, value: 2 }]
462
+ }
463
+ ]
464
+ }
465
+ ]
466
+ });
467
+ });
468
+ });
469
+
470
+ describe("Specialized Element AST", () => {
471
+ it("void elements: children", () => {
472
+ // Note: br is void, img is void. They should be siblings, not nested.
473
+ const ast = jsx`<div><img src="test.png" >Children should get <span>wiped</span></img></div>`;
474
+
475
+ expect(ast).toEqual({
476
+ type: ROOT_NODE,
477
+ children: [
478
+ {
479
+ type: ELEMENT_NODE,
480
+ name: "div",
481
+ props: [],
482
+ children: [
483
+ {
484
+ type: ELEMENT_NODE,
485
+ name: "img",
486
+ props: [
487
+ {
488
+ name: "src",
489
+ type: STATIC_PROP,
490
+ value: "test.png",
491
+ quote: '"'
492
+ }
493
+ ],
494
+ children: []
495
+ }
496
+ ]
497
+ }
498
+ ]
499
+ });
500
+ });
501
+
502
+ it("raw text elements: textarea ignoring content", () => {
503
+ // The content inside <textarea> is treated as a single TEXT_NODE
504
+ const ast = jsx`<textarea><div class="fake">${0}</div></textarea>`;
505
+
506
+ expect(ast).toEqual({
507
+ type: ROOT_NODE,
508
+ children: [
509
+ {
510
+ type: ELEMENT_NODE,
511
+ name: "textarea",
512
+ props: [],
513
+ children: [
514
+ {
515
+ type: TEXT_NODE,
516
+ value: '<div class="fake">'
517
+ },
518
+ { type: EXPRESSION_NODE, value: 0 },
519
+ {
520
+ type: TEXT_NODE,
521
+ value: "</div>"
522
+ }
523
+ ]
524
+ }
525
+ ]
526
+ });
527
+ });
528
+ });
529
+
530
+ describe("Dynamic Component Tags", () => {
531
+ it("parses dynamic opening tag with expression as tag name", () => {
532
+ const Comp = "Comp";
533
+ const ast = jsx`<${Comp}></${Comp}>`;
534
+ expect(ast).toEqual({
535
+ type: ROOT_NODE,
536
+ children: [
537
+ {
538
+ type: COMPONENT_NODE,
539
+ name: 0,
540
+ props: [],
541
+ children: []
542
+ }
543
+ ]
544
+ });
545
+ });
546
+
547
+ it("parses dynamic component with children", () => {
548
+ const Comp = "Comp";
549
+ const ast = jsx`<${Comp}><span>content</span></${Comp}>`;
550
+ expect(ast).toEqual({
551
+ type: ROOT_NODE,
552
+ children: [
553
+ {
554
+ type: COMPONENT_NODE,
555
+ name: 0,
556
+ props: [],
557
+ children: [
558
+ {
559
+ type: ELEMENT_NODE,
560
+ name: "span",
561
+ props: [],
562
+ children: [{ type: TEXT_NODE, value: "content" }]
563
+ }
564
+ ]
565
+ }
566
+ ]
567
+ });
568
+ });
569
+
570
+ it("parses self-closing dynamic component", () => {
571
+ const Comp = "Comp";
572
+ const ast = jsx`<${Comp} />`;
573
+ expect(ast).toEqual({
574
+ type: ROOT_NODE,
575
+ children: [
576
+ {
577
+ type: COMPONENT_NODE,
578
+ name: 0,
579
+ props: [],
580
+ children: []
581
+ }
582
+ ]
583
+ });
584
+ });
585
+
586
+ it("parses dynamic component with attributes", () => {
587
+ const Comp = "Comp";
588
+ const value = "test";
589
+ const ast = jsx`<${Comp} id=${value}></${Comp}>`;
590
+ expect(ast).toEqual({
591
+ type: ROOT_NODE,
592
+ children: [
593
+ {
594
+ type: COMPONENT_NODE,
595
+ name: 0,
596
+ props: [{ name: "id", type: EXPRESSION_PROP, value: 1 }],
597
+ children: []
598
+ }
599
+ ]
600
+ });
601
+ });
602
+ });
603
+
604
+ describe("Edge Cases", () => {
605
+ it("empty template", () => {
606
+ const ast = jsx``;
607
+ expect(ast).toEqual({ type: ROOT_NODE, children: [] });
608
+ });
609
+ it("only expressions", () => {
610
+ const a = 1;
611
+ const b = 2;
612
+ const ast = jsx`${a}${b}`;
613
+ expect(ast).toEqual({
614
+ type: ROOT_NODE,
615
+ children: [
616
+ { type: EXPRESSION_NODE, value: 0 },
617
+ { type: EXPRESSION_NODE, value: 1 }
618
+ ]
619
+ });
620
+ });
621
+ });
622
+
623
+ describe("Errors", () => {
624
+ it("error on open tag", () => {
625
+ expect(() => jsx`<div`).toThrow();
626
+ });
627
+
628
+ it("error on mismatched tag", () => {
629
+ expect(() => jsx`<div></span>`).toThrow();
630
+ });
631
+
632
+ it("error on extra <", () => {
633
+ expect(() => jsx`<div><</span>`).toThrow();
634
+ });
635
+
636
+ it("error on bad tag name", () => {
637
+ expect(() => jsx`<1div><</1div>`).toThrow();
638
+ });
639
+
640
+ it("error on unclosed tags", () => {
641
+ expect(() => jsx`<div>`).toThrow();
642
+ });
643
+
644
+ it("error on spread without expression", () => {
645
+ expect(() => jsx`<div ... bool></div>`).toThrow();
646
+ });
647
+
648
+ it("error on unmatched close", () => {
649
+ expect(() => jsx`</div>`).toThrow();
650
+ });
651
+ });
652
+
653
+ describe("Advanced Parser Edge Cases", () => {
654
+ describe("Spread Attributes", () => {
655
+ it("handles multiple spread attributes", () => {
656
+ const props1 = { class: "first" };
657
+ const props2 = { id: "second" };
658
+ const ast = jsx`<div ...${props1} ...${props2}></div>`;
659
+
660
+ expect(ast).toEqual({
661
+ type: ROOT_NODE,
662
+ children: [
663
+ {
664
+ type: ELEMENT_NODE,
665
+ name: "div",
666
+ props: [
667
+ { type: SPREAD_PROP, value: 0 },
668
+ { type: SPREAD_PROP, value: 1 }
669
+ ],
670
+ children: []
671
+ }
672
+ ]
673
+ });
674
+ });
675
+
676
+ it("handles spread with mixed other attributes", () => {
677
+ const props = { class: "dynamic", "data-test": "value" };
678
+ const ast = jsx`<div id="static" ...${props} required></div>`;
679
+
680
+ expect(ast).toEqual({
681
+ type: ROOT_NODE,
682
+ children: [
683
+ {
684
+ type: ELEMENT_NODE,
685
+ name: "div",
686
+ props: [
687
+ { name: "id", type: STATIC_PROP, value: "static", quote: '"' },
688
+ { type: SPREAD_PROP, value: 0 },
689
+ { name: "required", type: BOOLEAN_PROP, value: true }
690
+ ],
691
+ children: []
692
+ }
693
+ ]
694
+ });
695
+ });
696
+ });
697
+
698
+ describe("Special Tag Names and Namespaces", () => {
699
+ it("handles custom elements with hyphens", () => {
700
+ const ast = jsx`<my-custom-element attr="value"></my-custom-element>`;
701
+
702
+ expect(ast).toEqual({
703
+ type: ROOT_NODE,
704
+ children: [
705
+ {
706
+ type: ELEMENT_NODE,
707
+ name: "my-custom-element",
708
+ props: [{ name: "attr", type: STATIC_PROP, value: "value", quote: '"' }],
709
+ children: []
710
+ }
711
+ ]
712
+ });
713
+ });
714
+
715
+ it("handles namespaced tags", () => {
716
+ const ast = jsx`<svg:rect x="10" y="20"></svg:rect>`;
717
+
718
+ expect(ast).toEqual({
719
+ type: ROOT_NODE,
720
+ children: [
721
+ {
722
+ type: ELEMENT_NODE,
723
+ name: "svg:rect",
724
+ props: [
725
+ { name: "x", type: STATIC_PROP, value: "10", quote: '"' },
726
+ { name: "y", type: STATIC_PROP, value: "20", quote: '"' }
727
+ ],
728
+ children: []
729
+ }
730
+ ]
731
+ });
732
+ });
733
+
734
+ it("handles foreignObject in SVG", () => {
735
+ const ast = jsx`<svg><foreignObject><div>HTML content</div></foreignObject></svg>`;
736
+
737
+ expect(ast).toEqual({
738
+ type: ROOT_NODE,
739
+ children: [
740
+ {
741
+ type: ELEMENT_NODE,
742
+ name: "svg",
743
+ props: [],
744
+ children: [
745
+ {
746
+ type: ELEMENT_NODE,
747
+ name: "foreignObject",
748
+ props: [],
749
+ children: [
750
+ {
751
+ type: ELEMENT_NODE,
752
+ name: "div",
753
+ props: [],
754
+ children: [{ type: TEXT_NODE, value: "HTML content" }]
755
+ }
756
+ ]
757
+ }
758
+ ]
759
+ }
760
+ ]
761
+ });
762
+ });
763
+ });
764
+
765
+ describe("Advanced Text and Expression Handling", () => {
766
+ it("handles complex text with multiple adjacent expressions", () => {
767
+ const items = ["a", "b", "c"];
768
+ const separator = ", ";
769
+ const ast = jsx`${items[0]}${separator}${items[1]}${separator}${items[2]}`;
770
+
771
+ expect(ast).toEqual({
772
+ type: ROOT_NODE,
773
+ children: [
774
+ { type: EXPRESSION_NODE, value: 0 },
775
+ { type: EXPRESSION_NODE, value: 1 },
776
+ { type: EXPRESSION_NODE, value: 2 },
777
+ { type: EXPRESSION_NODE, value: 3 },
778
+ { type: EXPRESSION_NODE, value: 4 }
779
+ ]
780
+ });
781
+ });
782
+
783
+ it("handles text with HTML entities", () => {
784
+ const ast = jsx`<div>Use &lt; and &gt; for brackets</div>`;
785
+
786
+ expect(ast).toEqual({
787
+ type: ROOT_NODE,
788
+ children: [
789
+ {
790
+ type: ELEMENT_NODE,
791
+ name: "div",
792
+ props: [],
793
+ children: [{ type: TEXT_NODE, value: "Use &lt; and &gt; for brackets" }]
794
+ }
795
+ ]
796
+ });
797
+ });
798
+
799
+ it("handles text with numeric character references", () => {
800
+ const ast = jsx`<div>Copyright &#169; 2023</div>`;
801
+
802
+ expect(ast).toEqual({
803
+ type: ROOT_NODE,
804
+ children: [
805
+ {
806
+ type: ELEMENT_NODE,
807
+ name: "div",
808
+ props: [],
809
+ children: [{ type: TEXT_NODE, value: "Copyright &#169; 2023" }]
810
+ }
811
+ ]
812
+ });
813
+ });
814
+ });
815
+
816
+ describe("Complex Nesting and Structure", () => {
817
+ it("handles deeply nested structures", () => {
818
+ const ast = jsx`
819
+ <div>
820
+ <section>
821
+ <article>
822
+ <header>
823
+ <h1>Title</h1>
824
+ </header>
825
+ <main>
826
+ <p>Content</p>
827
+ </main>
828
+ <footer>
829
+ <small>Footer</small>
830
+ </footer>
831
+ </article>
832
+ </section>
833
+ </div>
834
+ `;
835
+
836
+ const divChild = ast.children[0] as any;
837
+ const sectionChild = divChild.children[0] as any;
838
+ const articleChild = sectionChild.children[0] as any;
839
+
840
+ expect(articleChild.name).toBe("article");
841
+ expect(articleChild.children.length).toBe(3);
842
+ });
843
+
844
+ it("handles sibling elements at root", () => {
845
+ const ast = jsx`<div>First</div><span>Second</span><p>Third</p>`;
846
+
847
+ expect(ast.children).toHaveLength(3);
848
+ expect((ast.children[0] as any).name).toBe("div");
849
+ expect((ast.children[1] as any).name).toBe("span");
850
+ expect((ast.children[2] as any).name).toBe("p");
851
+ });
852
+
853
+ it("handles mixed text and elements at root", () => {
854
+ const ast = jsx`Before<div>Element</div>After`;
855
+
856
+ expect(ast.children).toHaveLength(3);
857
+ expect((ast.children[0] as any).type).toBe(TEXT_NODE);
858
+ expect((ast.children[1] as any).type).toBe(ELEMENT_NODE);
859
+ expect((ast.children[2] as any).type).toBe(TEXT_NODE);
860
+ });
861
+ });
862
+
863
+ describe("Void and Raw Text Elements", () => {
864
+ it("handles void elements with attributes and self-closing syntax", () => {
865
+ const ast = jsx`<img src="test.jpg" alt="Test Image" />`;
866
+
867
+ expect(ast).toEqual({
868
+ type: ROOT_NODE,
869
+ children: [
870
+ {
871
+ type: ELEMENT_NODE,
872
+ name: "img",
873
+ props: [
874
+ { name: "src", type: STATIC_PROP, value: "test.jpg", quote: '"' },
875
+ { name: "alt", type: STATIC_PROP, value: "Test Image", quote: '"' }
876
+ ],
877
+ children: []
878
+ }
879
+ ]
880
+ });
881
+ });
882
+
883
+ it("handles void elements without explicit slash", () => {
884
+ const ast = jsx`<br></br>`;
885
+
886
+ expect(ast).toEqual({
887
+ type: ROOT_NODE,
888
+ children: [
889
+ {
890
+ type: ELEMENT_NODE,
891
+ name: "br",
892
+ props: [],
893
+ children: []
894
+ }
895
+ ]
896
+ });
897
+ });
898
+
899
+ it("handles script element with complex content", () => {
900
+ const ast = jsx`<script>
901
+ function test() {
902
+ return x < y && z > w;
903
+ }
904
+ </script>`;
905
+
906
+ expect(((ast.children[0] as any).children[0] as any).value).toContain(
907
+ "return x < y && z > w;"
908
+ );
909
+ });
910
+
911
+ it("handles style element with CSS content", () => {
912
+ const ast = jsx`<style>
913
+ .class > .child { color: red; }
914
+ @media (max-width: 768px) {
915
+ .responsive { display: block; }
916
+ }
917
+ </style>`;
918
+
919
+ const styleContent = ((ast.children[0] as any).children[0] as any).value;
920
+ expect(styleContent).toContain(".class > .child");
921
+ expect(styleContent).toContain("@media");
922
+ });
923
+ });
924
+
925
+ describe("Attribute Namespaces and Special Props", () => {
926
+ it("handles prop: and attr: namespaces", () => {
927
+ const value = "test";
928
+ const ast = jsx`<input prop:value=${value} attr:title="Title"></input>`;
929
+
930
+ expect(ast).toEqual({
931
+ type: ROOT_NODE,
932
+ children: [
933
+ {
934
+ type: ELEMENT_NODE,
935
+ name: "input",
936
+ props: [
937
+ { name: "prop:value", type: EXPRESSION_PROP, value: 0 },
938
+ { name: "attr:title", type: STATIC_PROP, value: "Title", quote: '"' }
939
+ ],
940
+ children: []
941
+ }
942
+ ]
943
+ });
944
+ });
945
+
946
+ it("handles ref attribute", () => {
947
+ const ref = (el: HTMLElement) => {};
948
+ const ast = jsx`<div ref=${ref}></div>`;
949
+
950
+ expect(ast).toEqual({
951
+ type: ROOT_NODE,
952
+ children: [
953
+ {
954
+ type: ELEMENT_NODE,
955
+ name: "div",
956
+ props: [{ name: "ref", type: EXPRESSION_PROP, value: 0 }],
957
+ children: []
958
+ }
959
+ ]
960
+ });
961
+ });
962
+ });
963
+
964
+ describe("Error Recovery and Edge Cases", () => {
965
+ it("handles attributes with special characters", () => {
966
+ const ast = jsx`<div data-attr_with.special:chars="value"></div>`;
967
+
968
+ expect(((ast.children[0] as any).props[0] as any).name).toBe("data-attr_with.special:chars");
969
+ });
970
+ });
971
+ });