@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,1019 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ CLOSE_TAG_TOKEN,
4
+ EQUALS_TOKEN,
5
+ EXPRESSION_TOKEN,
6
+ IDENTIFIER_TOKEN,
7
+ OPEN_TAG_TOKEN,
8
+ SLASH_TOKEN,
9
+ STRING_TOKEN,
10
+ TEXT_TOKEN,
11
+ tokenize
12
+ } from "../src/tokenize";
13
+ import { RawTextElements } from "./core.js";
14
+
15
+ function tokenizeTemplate(strings: TemplateStringsArray, ...values: any[]) {
16
+ return tokenize(strings, RawTextElements);
17
+ }
18
+
19
+ describe("basic tags", () => {
20
+ it("should tokenize opening tag", () => {
21
+ const tokens = tokenizeTemplate`<div`;
22
+
23
+ expect(tokens).toEqual([
24
+ {
25
+ type: OPEN_TAG_TOKEN
26
+ },
27
+ {
28
+ type: IDENTIFIER_TOKEN,
29
+ value: "div"
30
+ }
31
+ ]);
32
+ });
33
+
34
+ it("should tokenize complete tag", () => {
35
+ const tokens = tokenizeTemplate`<div>`;
36
+
37
+ expect(tokens).toEqual([
38
+ {
39
+ type: OPEN_TAG_TOKEN
40
+ },
41
+ {
42
+ type: IDENTIFIER_TOKEN,
43
+ value: "div"
44
+ },
45
+ {
46
+ type: CLOSE_TAG_TOKEN
47
+ }
48
+ ]);
49
+ });
50
+
51
+ it("should tokenize self-closing tag", () => {
52
+ const tokens = tokenizeTemplate`<div />`;
53
+
54
+ expect(tokens).toEqual([
55
+ {
56
+ type: OPEN_TAG_TOKEN
57
+ },
58
+ {
59
+ type: IDENTIFIER_TOKEN,
60
+ value: "div"
61
+ },
62
+ {
63
+ type: SLASH_TOKEN
64
+ },
65
+ {
66
+ type: CLOSE_TAG_TOKEN
67
+ }
68
+ ]);
69
+ });
70
+
71
+ it("should tokenize opening and closing tag", () => {
72
+ const tokens = tokenizeTemplate`<div></div>`;
73
+
74
+ expect(tokens).toEqual([
75
+ {
76
+ type: OPEN_TAG_TOKEN
77
+ },
78
+ {
79
+ type: IDENTIFIER_TOKEN,
80
+ value: "div"
81
+ },
82
+ {
83
+ type: CLOSE_TAG_TOKEN
84
+ },
85
+ {
86
+ type: OPEN_TAG_TOKEN
87
+ },
88
+ {
89
+ type: SLASH_TOKEN
90
+ },
91
+ {
92
+ type: IDENTIFIER_TOKEN,
93
+ value: "div"
94
+ },
95
+
96
+ {
97
+ type: CLOSE_TAG_TOKEN
98
+ }
99
+ ]);
100
+ });
101
+ });
102
+
103
+ describe("attribute values", () => {
104
+ it("should tokenize quoted string", () => {
105
+ const tokens = tokenizeTemplate`<div id="hello">`;
106
+
107
+ expect(tokens).toEqual([
108
+ {
109
+ type: OPEN_TAG_TOKEN
110
+ },
111
+ {
112
+ type: IDENTIFIER_TOKEN,
113
+ value: "div"
114
+ },
115
+ {
116
+ type: IDENTIFIER_TOKEN,
117
+ value: "id"
118
+ },
119
+ {
120
+ type: EQUALS_TOKEN
121
+ },
122
+ {
123
+ type: STRING_TOKEN,
124
+ value: "hello",
125
+ quote: '"'
126
+ },
127
+ {
128
+ type: CLOSE_TAG_TOKEN
129
+ }
130
+ ]);
131
+ });
132
+
133
+ it("should tokenize single quoted string", () => {
134
+ const tokens = tokenizeTemplate`<div id='hello'>`;
135
+
136
+ expect(tokens).toEqual([
137
+ {
138
+ type: OPEN_TAG_TOKEN
139
+ },
140
+ {
141
+ type: IDENTIFIER_TOKEN,
142
+ value: "div"
143
+ },
144
+ {
145
+ type: IDENTIFIER_TOKEN,
146
+ value: "id"
147
+ },
148
+ {
149
+ type: EQUALS_TOKEN
150
+ },
151
+ {
152
+ type: STRING_TOKEN,
153
+ value: "hello",
154
+ quote: "'"
155
+ },
156
+ {
157
+ type: CLOSE_TAG_TOKEN
158
+ }
159
+ ]);
160
+ });
161
+
162
+ it("should handle empty quoted string", () => {
163
+ const tokens = tokenizeTemplate`<div class="">`;
164
+
165
+ expect(tokens).toEqual([
166
+ {
167
+ type: OPEN_TAG_TOKEN
168
+ },
169
+ {
170
+ type: IDENTIFIER_TOKEN,
171
+ value: "div"
172
+ },
173
+ {
174
+ type: IDENTIFIER_TOKEN,
175
+ value: "class"
176
+ },
177
+ {
178
+ type: EQUALS_TOKEN
179
+ },
180
+ {
181
+ type: STRING_TOKEN,
182
+ value: "",
183
+ quote: '"'
184
+ },
185
+ {
186
+ type: CLOSE_TAG_TOKEN
187
+ }
188
+ ]);
189
+ });
190
+
191
+ it("should handle boolean like attribute", () => {
192
+ const tokens = tokenizeTemplate`<div enabled bool>`;
193
+
194
+ expect(tokens).toEqual([
195
+ {
196
+ type: OPEN_TAG_TOKEN
197
+ },
198
+ {
199
+ type: IDENTIFIER_TOKEN,
200
+ value: "div"
201
+ },
202
+ {
203
+ type: IDENTIFIER_TOKEN,
204
+ value: "enabled"
205
+ },
206
+ {
207
+ type: IDENTIFIER_TOKEN,
208
+ value: "bool"
209
+ },
210
+
211
+ {
212
+ type: CLOSE_TAG_TOKEN
213
+ }
214
+ ]);
215
+ });
216
+
217
+ it("should handle deeply nested quotes", () => {
218
+ const tokens = tokenizeTemplate`<div data="value with 'nested' quotes">`;
219
+
220
+ expect(tokens).toContainEqual(
221
+ expect.objectContaining({
222
+ type: STRING_TOKEN,
223
+ value: "value with 'nested' quotes",
224
+ quote: '"'
225
+ })
226
+ );
227
+ });
228
+
229
+ it("should handle attribute values with special characters", () => {
230
+ const tokens = tokenizeTemplate`<div data="!@#$%^&*()_+-=[]{}|;:,.<>?">`;
231
+
232
+ expect(tokens).toContainEqual(
233
+ expect.objectContaining({
234
+ type: STRING_TOKEN,
235
+ value: "!@#$%^&*()_+-=[]{}|;:,.<>?",
236
+ quote: '"'
237
+ })
238
+ );
239
+ });
240
+
241
+ it("should handle URL-like attribute values", () => {
242
+ const tokens = tokenizeTemplate`<a href="https://example.com/path?query=value&other=test#section">`;
243
+
244
+ expect(tokens).toContainEqual(
245
+ expect.objectContaining({
246
+ type: STRING_TOKEN,
247
+ value: "https://example.com/path?query=value&other=test#section",
248
+ quote: '"'
249
+ })
250
+ );
251
+ });
252
+
253
+ it("attribute name doesnt trigger raw text", () => {
254
+ const tokens = tokenizeTemplate`
255
+ <h1 title=""></h1>
256
+ `;
257
+
258
+ expect(tokens).toEqual([
259
+ { type: TEXT_TOKEN, value: "\n " },
260
+ { type: OPEN_TAG_TOKEN },
261
+ { type: IDENTIFIER_TOKEN, value: "h1" },
262
+ { type: IDENTIFIER_TOKEN, value: "title" },
263
+ { type: EQUALS_TOKEN },
264
+ { type: STRING_TOKEN, value: "", quote: '"' },
265
+
266
+ { type: CLOSE_TAG_TOKEN },
267
+ { type: OPEN_TAG_TOKEN },
268
+ { type: SLASH_TOKEN },
269
+ { type: IDENTIFIER_TOKEN, value: "h1" },
270
+ { type: CLOSE_TAG_TOKEN },
271
+ { type: TEXT_TOKEN, value: "\n " }
272
+ ]);
273
+ });
274
+ });
275
+
276
+ describe("expressions", () => {
277
+ it("should tokenize simple expression", () => {
278
+ const value = "test";
279
+ const tokens = tokenizeTemplate`${value}`;
280
+
281
+ expect(tokens).toEqual([
282
+ {
283
+ type: EXPRESSION_TOKEN,
284
+ value: 0
285
+ }
286
+ ]);
287
+ });
288
+
289
+ it("should tokenize multiple expressions", () => {
290
+ const a = "first";
291
+ const b = "second";
292
+ const tokens = tokenizeTemplate`${a}${b}`;
293
+
294
+ expect(tokens).toEqual([
295
+ {
296
+ type: EXPRESSION_TOKEN,
297
+ value: 0
298
+ },
299
+ {
300
+ type: EXPRESSION_TOKEN,
301
+ value: 1
302
+ }
303
+ ]);
304
+ });
305
+
306
+ it("should handle expression in unquoted attribute", () => {
307
+ const id = "my-id";
308
+ const tokens = tokenizeTemplate`<div id=${id}>`;
309
+
310
+ expect(tokens).toEqual([
311
+ {
312
+ type: OPEN_TAG_TOKEN
313
+ },
314
+ {
315
+ type: IDENTIFIER_TOKEN,
316
+ value: "div"
317
+ },
318
+ {
319
+ type: IDENTIFIER_TOKEN,
320
+ value: "id"
321
+ },
322
+ {
323
+ type: EQUALS_TOKEN
324
+ },
325
+ {
326
+ type: EXPRESSION_TOKEN,
327
+ value: 0
328
+ },
329
+ {
330
+ type: CLOSE_TAG_TOKEN
331
+ }
332
+ ]);
333
+ });
334
+
335
+ it("should handle mixed text and expressions", () => {
336
+ const name = "World";
337
+ const tokens = tokenizeTemplate`Hello ${name}!`;
338
+
339
+ expect(tokens).toEqual([
340
+ {
341
+ type: TEXT_TOKEN,
342
+ value: "Hello "
343
+ },
344
+ {
345
+ type: EXPRESSION_TOKEN,
346
+ value: 0
347
+ },
348
+ {
349
+ type: TEXT_TOKEN,
350
+ value: "!"
351
+ }
352
+ ]);
353
+ });
354
+
355
+ it("should handle data attributes with hyphens and underscores", () => {
356
+ const tokens = tokenizeTemplate`<div data-my_value="test" data_other-name="value">`;
357
+
358
+ const attrNames = tokens.filter(
359
+ t => t.type === IDENTIFIER_TOKEN && (t.value as string).includes("data")
360
+ );
361
+ expect(attrNames.length).toBeGreaterThanOrEqual(2);
362
+ });
363
+ });
364
+
365
+ describe("whitespace handling", () => {
366
+ it("should skip whitespace inside tags", () => {
367
+ const tokens = tokenizeTemplate`< \n div id = "app" >`;
368
+
369
+ // Should not have whitespace tokens in tag context
370
+ expect(tokens).toEqual([
371
+ {
372
+ type: OPEN_TAG_TOKEN
373
+ },
374
+ {
375
+ type: IDENTIFIER_TOKEN,
376
+ value: "div"
377
+ },
378
+ {
379
+ type: IDENTIFIER_TOKEN,
380
+ value: "id"
381
+ },
382
+ {
383
+ type: EQUALS_TOKEN
384
+ },
385
+
386
+ {
387
+ type: STRING_TOKEN,
388
+ value: "app",
389
+ quote: '"'
390
+ },
391
+
392
+ {
393
+ type: CLOSE_TAG_TOKEN
394
+ }
395
+ ]);
396
+ });
397
+
398
+ it("should preserve text content whitespace", () => {
399
+ const tokens = tokenizeTemplate` Hello World `;
400
+
401
+ expect(tokens).toEqual([
402
+ {
403
+ type: TEXT_TOKEN,
404
+ value: " Hello World "
405
+ }
406
+ ]);
407
+ });
408
+
409
+ it("should handle multiline content with preserved whitespace", () => {
410
+ const tokens = tokenizeTemplate`<div>
411
+ Hello
412
+ </div>`;
413
+
414
+ expect(tokens).toEqual([
415
+ {
416
+ type: OPEN_TAG_TOKEN
417
+ },
418
+ {
419
+ type: IDENTIFIER_TOKEN,
420
+ value: "div"
421
+ },
422
+ {
423
+ type: CLOSE_TAG_TOKEN
424
+ },
425
+ {
426
+ type: TEXT_TOKEN,
427
+ value: "\n Hello\n "
428
+ },
429
+ {
430
+ type: OPEN_TAG_TOKEN
431
+ },
432
+ {
433
+ type: SLASH_TOKEN
434
+ },
435
+ {
436
+ type: IDENTIFIER_TOKEN,
437
+ value: "div"
438
+ },
439
+ {
440
+ type: CLOSE_TAG_TOKEN
441
+ }
442
+ ]);
443
+ });
444
+
445
+ it("should handle tabs and mixed whitespace", () => {
446
+ const tokens = tokenizeTemplate`\tHello\nWorld `;
447
+
448
+ expect(tokens).toEqual([
449
+ {
450
+ type: TEXT_TOKEN,
451
+ value: "\tHello\nWorld "
452
+ }
453
+ ]);
454
+ });
455
+
456
+ it("should handle whitespace around expressions", () => {
457
+ const name = "test";
458
+ const tokens = tokenizeTemplate` ${name} `;
459
+
460
+ expect(tokens).toEqual([
461
+ {
462
+ type: TEXT_TOKEN,
463
+ value: " "
464
+ },
465
+ {
466
+ type: EXPRESSION_TOKEN,
467
+ value: 0
468
+ },
469
+ {
470
+ type: TEXT_TOKEN,
471
+ value: " "
472
+ }
473
+ ]);
474
+ });
475
+ });
476
+
477
+ describe("edge cases", () => {
478
+ it("should handle empty template", () => {
479
+ const tokens = tokenizeTemplate``;
480
+
481
+ expect(tokens).toEqual([]);
482
+ });
483
+
484
+ it("should handle only whitespace", () => {
485
+ const tokens = tokenizeTemplate` `;
486
+
487
+ expect(tokens).toEqual([
488
+ {
489
+ type: TEXT_TOKEN,
490
+ value: " "
491
+ }
492
+ ]);
493
+ });
494
+
495
+ it("should handle special characters in text", () => {
496
+ const tokens = tokenizeTemplate`Hello & goodbye`;
497
+
498
+ expect(tokens).toEqual([
499
+ {
500
+ type: TEXT_TOKEN,
501
+ value: "Hello & goodbye"
502
+ }
503
+ ]);
504
+ });
505
+
506
+ it("should handle consecutive expressions", () => {
507
+ const a = "first";
508
+ const b = "second";
509
+ const tokens = tokenizeTemplate`${a}${b}`;
510
+
511
+ expect(tokens).toEqual([
512
+ {
513
+ type: EXPRESSION_TOKEN,
514
+ value: 0
515
+ },
516
+ {
517
+ type: EXPRESSION_TOKEN,
518
+ value: 1
519
+ }
520
+ ]);
521
+ });
522
+
523
+ it("should handle 1 letter tags", () => {
524
+ const tokens = tokenizeTemplate`<tr class=${0}>
525
+ <td class="col-md-1" textContent=${1} />
526
+ <td class="col-md-4">
527
+ <a onClick=${2} textContent=${3} />
528
+ </td>
529
+ <td class="col-md-1">
530
+ <a onClick=${4}>
531
+ <span class="glyphicon glyphicon-remove" aria-hidden="true" />
532
+ </a>
533
+ </td>
534
+ <td class="col-md-6" />
535
+ </tr>`;
536
+ expect(tokens.filter(t => t.type === IDENTIFIER_TOKEN && t.value === "a").length).toBe(3);
537
+ });
538
+ });
539
+
540
+ describe("special characters in names", () => {
541
+ it("should tokenize tag with hyphens", () => {
542
+ const tokens = tokenizeTemplate`<my-component />`;
543
+
544
+ expect(tokens).toEqual([
545
+ {
546
+ type: OPEN_TAG_TOKEN
547
+ },
548
+ {
549
+ type: IDENTIFIER_TOKEN,
550
+ value: "my-component"
551
+ },
552
+ {
553
+ type: SLASH_TOKEN
554
+ },
555
+ {
556
+ type: CLOSE_TAG_TOKEN
557
+ }
558
+ ]);
559
+ });
560
+
561
+ it("should tokenize tag with periods", () => {
562
+ const tokens = tokenizeTemplate`<my.component />`;
563
+
564
+ expect(tokens).toEqual([
565
+ {
566
+ type: OPEN_TAG_TOKEN
567
+ },
568
+ {
569
+ type: IDENTIFIER_TOKEN,
570
+ value: "my.component"
571
+ },
572
+ {
573
+ type: SLASH_TOKEN
574
+ },
575
+ {
576
+ type: CLOSE_TAG_TOKEN
577
+ }
578
+ ]);
579
+ });
580
+
581
+ it("should tokenize tag with colons", () => {
582
+ const tokens = tokenizeTemplate`<svg:rect />`;
583
+
584
+ expect(tokens).toEqual([
585
+ {
586
+ type: OPEN_TAG_TOKEN
587
+ },
588
+ {
589
+ type: IDENTIFIER_TOKEN,
590
+ value: "svg:rect"
591
+ },
592
+ {
593
+ type: SLASH_TOKEN
594
+ },
595
+ {
596
+ type: CLOSE_TAG_TOKEN
597
+ }
598
+ ]);
599
+ });
600
+
601
+ it("should tokenize tag with underscores", () => {
602
+ const tokens = tokenizeTemplate`<my_component />`;
603
+
604
+ expect(tokens).toEqual([
605
+ {
606
+ type: OPEN_TAG_TOKEN
607
+ },
608
+ {
609
+ type: IDENTIFIER_TOKEN,
610
+ value: "my_component"
611
+ },
612
+ {
613
+ type: SLASH_TOKEN
614
+ },
615
+ {
616
+ type: CLOSE_TAG_TOKEN
617
+ }
618
+ ]);
619
+ });
620
+
621
+ it("should tokenize attribute with -_.:$", () => {
622
+ const tokens = tokenizeTemplate`<div data-id data_id data.id data:id dataid$>`;
623
+
624
+ expect(tokens).toEqual([
625
+ {
626
+ type: OPEN_TAG_TOKEN
627
+ },
628
+ {
629
+ type: IDENTIFIER_TOKEN,
630
+ value: "div"
631
+ },
632
+ {
633
+ type: IDENTIFIER_TOKEN,
634
+ value: "data-id"
635
+ },
636
+ {
637
+ type: IDENTIFIER_TOKEN,
638
+ value: "data_id"
639
+ },
640
+ {
641
+ type: IDENTIFIER_TOKEN,
642
+ value: "data.id"
643
+ },
644
+ {
645
+ type: IDENTIFIER_TOKEN,
646
+ value: "data:id"
647
+ },
648
+ {
649
+ type: IDENTIFIER_TOKEN,
650
+ value: "dataid$"
651
+ },
652
+ {
653
+ type: CLOSE_TAG_TOKEN
654
+ }
655
+ ]);
656
+ });
657
+ });
658
+
659
+ describe("invalid syntax", () => {
660
+ it("should throw with extra <", () => {
661
+ expect(() => tokenizeTemplate`<<div / >`).toThrow();
662
+ });
663
+
664
+ it("should throw with extra <", () => {
665
+ expect(() => tokenizeTemplate`<div / <>`).toThrow();
666
+ });
667
+
668
+ it("should throw on invalid identofier", () => {
669
+ expect(() => tokenizeTemplate`<.div />`).toThrow();
670
+ });
671
+
672
+ it("should throw on invalid identofier", () => {
673
+ expect(() => tokenizeTemplate`<div @fa />`).toThrow();
674
+ });
675
+
676
+ it("should throw on invalid identofier", () => {
677
+ expect(() => tokenizeTemplate`<div 0fa />`).toThrow();
678
+ });
679
+ });
680
+
681
+ describe("bad but valid syntaxes", () => {
682
+ it("should handle multiple attributes in tight syntax", () => {
683
+ const tokens = tokenizeTemplate`<div a="1"b="2"c="3">`;
684
+
685
+ const attrNames = tokens.filter(
686
+ t => t.type === IDENTIFIER_TOKEN && t.value && /^[abc]$/.test(t.value as string)
687
+ );
688
+ expect(attrNames).toHaveLength(3);
689
+ });
690
+
691
+ it("should handle attribute without value but with slash", () => {
692
+ const tokens = tokenizeTemplate`<div required/>`;
693
+
694
+ expect(tokens).toContainEqual(
695
+ expect.objectContaining({
696
+ type: IDENTIFIER_TOKEN,
697
+ value: "required"
698
+ })
699
+ );
700
+ expect(tokens).toContainEqual(
701
+ expect.objectContaining({
702
+ type: SLASH_TOKEN
703
+ })
704
+ );
705
+ });
706
+
707
+ it("should handle whitespace variations", () => {
708
+ const tokens = tokenizeTemplate`<div id = "value" />`;
709
+
710
+ expect(tokens).toContainEqual(
711
+ expect.objectContaining({
712
+ type: IDENTIFIER_TOKEN,
713
+ value: "id"
714
+ })
715
+ );
716
+ expect(tokens).toContainEqual(
717
+ expect.objectContaining({
718
+ type: STRING_TOKEN,
719
+ value: "value"
720
+ })
721
+ );
722
+ });
723
+ });
724
+
725
+ describe("handling of raw text elements", () => {
726
+ it("should tokenize content inside <script> as text", () => {
727
+ const tokens = tokenizeTemplate`<script>const a = 5<10;</script>`;
728
+
729
+ expect(tokens).toEqual([
730
+ { type: OPEN_TAG_TOKEN },
731
+ { type: IDENTIFIER_TOKEN, value: "script" },
732
+ { type: CLOSE_TAG_TOKEN },
733
+ { type: TEXT_TOKEN, value: "const a = 5<10;" },
734
+ { type: OPEN_TAG_TOKEN },
735
+ { type: SLASH_TOKEN },
736
+ { type: IDENTIFIER_TOKEN, value: "script" },
737
+ { type: CLOSE_TAG_TOKEN }
738
+ ]);
739
+ });
740
+
741
+ it("should tokenize content inside <style> as text", () => {
742
+ const tokens = tokenizeTemplate`<style>.class > span { color: red; }</style>`;
743
+
744
+ expect(tokens).toEqual([
745
+ { type: OPEN_TAG_TOKEN },
746
+ { type: IDENTIFIER_TOKEN, value: "style" },
747
+ { type: CLOSE_TAG_TOKEN },
748
+ { type: TEXT_TOKEN, value: ".class > span { color: red; }" },
749
+ { type: OPEN_TAG_TOKEN },
750
+ { type: SLASH_TOKEN },
751
+ { type: IDENTIFIER_TOKEN, value: "style" },
752
+ { type: CLOSE_TAG_TOKEN }
753
+ ]);
754
+ });
755
+
756
+ it("should handle nested raw text elements ", () => {
757
+ const tokens = tokenizeTemplate`<div><style>.class > span { color: red; }</style></div>`;
758
+
759
+ expect(tokens).toEqual([
760
+ { type: OPEN_TAG_TOKEN },
761
+ { type: IDENTIFIER_TOKEN, value: "div" },
762
+ { type: CLOSE_TAG_TOKEN },
763
+ { type: OPEN_TAG_TOKEN },
764
+ { type: IDENTIFIER_TOKEN, value: "style" },
765
+ { type: CLOSE_TAG_TOKEN },
766
+ { type: TEXT_TOKEN, value: ".class > span { color: red; }" },
767
+ { type: OPEN_TAG_TOKEN },
768
+ { type: SLASH_TOKEN },
769
+ { type: IDENTIFIER_TOKEN, value: "style" },
770
+ { type: CLOSE_TAG_TOKEN },
771
+ { type: OPEN_TAG_TOKEN },
772
+ { type: SLASH_TOKEN },
773
+ { type: IDENTIFIER_TOKEN, value: "div" },
774
+ { type: CLOSE_TAG_TOKEN }
775
+ ]);
776
+ });
777
+
778
+ it("should tokenize content inside <textarea> as text", () => {
779
+ const tokens = tokenizeTemplate`<textarea>This is <span>not parsed</span>.</textarea>`;
780
+
781
+ expect(tokens).toEqual([
782
+ { type: OPEN_TAG_TOKEN },
783
+ { type: IDENTIFIER_TOKEN, value: "textarea" },
784
+ { type: CLOSE_TAG_TOKEN },
785
+ { type: TEXT_TOKEN, value: "This is <span>not parsed</span>." },
786
+ { type: OPEN_TAG_TOKEN },
787
+ { type: SLASH_TOKEN },
788
+ { type: IDENTIFIER_TOKEN, value: "textarea" },
789
+ { type: CLOSE_TAG_TOKEN }
790
+ ]);
791
+ });
792
+
793
+ it("should handle raw text elements with attributes and expressions", () => {
794
+ const tokens = tokenizeTemplate`<textarea type=${0}><span>${1}</span></textarea>`;
795
+ expect(tokens).toEqual([
796
+ { type: OPEN_TAG_TOKEN },
797
+ { type: IDENTIFIER_TOKEN, value: "textarea" },
798
+ { type: IDENTIFIER_TOKEN, value: "type" },
799
+ { type: EQUALS_TOKEN },
800
+ { type: EXPRESSION_TOKEN, value: 0 },
801
+ { type: CLOSE_TAG_TOKEN },
802
+ { type: TEXT_TOKEN, value: "<span>" },
803
+ { type: EXPRESSION_TOKEN, value: 1 },
804
+ { type: TEXT_TOKEN, value: "</span>" },
805
+ { type: OPEN_TAG_TOKEN },
806
+ { type: SLASH_TOKEN },
807
+ { type: IDENTIFIER_TOKEN, value: "textarea" },
808
+ { type: CLOSE_TAG_TOKEN }
809
+ ]);
810
+ });
811
+
812
+ it("should handle raw text elements and white space in tags", () => {
813
+ const tokens = tokenizeTemplate`< textarea > ${0}< / textarea >`;
814
+ expect(tokens).toEqual([
815
+ { type: OPEN_TAG_TOKEN },
816
+ { type: IDENTIFIER_TOKEN, value: "textarea" },
817
+ { type: CLOSE_TAG_TOKEN },
818
+ { type: TEXT_TOKEN, value: ` ` },
819
+ { type: EXPRESSION_TOKEN, value: 0 },
820
+ { type: OPEN_TAG_TOKEN },
821
+ { type: SLASH_TOKEN },
822
+ { type: IDENTIFIER_TOKEN, value: "textarea" },
823
+ { type: CLOSE_TAG_TOKEN }
824
+ ]);
825
+ });
826
+
827
+ it("should handle nested raw text elements", () => {
828
+ const tokens = tokenizeTemplate`<textarea><textarea>const a = 5;</textarea></textarea>`;
829
+ expect(tokens).toEqual([
830
+ { type: OPEN_TAG_TOKEN },
831
+ { type: IDENTIFIER_TOKEN, value: "textarea" },
832
+ { type: CLOSE_TAG_TOKEN },
833
+ { type: TEXT_TOKEN, value: "<textarea>const a = 5;" },
834
+ { type: OPEN_TAG_TOKEN },
835
+ { type: SLASH_TOKEN },
836
+ { type: IDENTIFIER_TOKEN, value: "textarea" },
837
+ { type: CLOSE_TAG_TOKEN },
838
+ { type: OPEN_TAG_TOKEN },
839
+ { type: SLASH_TOKEN },
840
+ { type: IDENTIFIER_TOKEN, value: "textarea" },
841
+ { type: CLOSE_TAG_TOKEN }
842
+ ]);
843
+ });
844
+
845
+ it("should handle self-closing raw text elements", () => {
846
+ const tokens = tokenizeTemplate`<textarea ${0} />Text`;
847
+ expect(tokens).toEqual([
848
+ { type: OPEN_TAG_TOKEN },
849
+ { type: IDENTIFIER_TOKEN, value: "textarea" },
850
+ { type: EXPRESSION_TOKEN, value: 0 },
851
+ { type: SLASH_TOKEN },
852
+ { type: CLOSE_TAG_TOKEN },
853
+ { type: TEXT_TOKEN, value: "Text" }
854
+ ]);
855
+ });
856
+ });
857
+
858
+ describe("dynamic component tags", () => {
859
+ it("should tokenize dynamic opening tag with expression", () => {
860
+ const Comp = "Comp";
861
+ const tokens = tokenizeTemplate`<${Comp}>`;
862
+
863
+ expect(tokens).toEqual([
864
+ { type: OPEN_TAG_TOKEN },
865
+ { type: EXPRESSION_TOKEN, value: 0 },
866
+ { type: CLOSE_TAG_TOKEN }
867
+ ]);
868
+ });
869
+
870
+ it("should tokenize dynamic self-closing tag", () => {
871
+ const Comp = "Comp";
872
+ const tokens = tokenizeTemplate`<${Comp} />`;
873
+
874
+ expect(tokens).toEqual([
875
+ { type: OPEN_TAG_TOKEN },
876
+ { type: EXPRESSION_TOKEN, value: 0 },
877
+ { type: SLASH_TOKEN },
878
+ { type: CLOSE_TAG_TOKEN }
879
+ ]);
880
+ });
881
+
882
+ it("should tokenize dynamic closing tag", () => {
883
+ const Comp = "Comp";
884
+ const tokens = tokenizeTemplate`<${Comp}></${Comp}>`;
885
+
886
+ expect(tokens).toEqual([
887
+ { type: OPEN_TAG_TOKEN },
888
+ { type: EXPRESSION_TOKEN, value: 0 },
889
+ { type: CLOSE_TAG_TOKEN },
890
+ { type: OPEN_TAG_TOKEN },
891
+ { type: SLASH_TOKEN },
892
+ { type: EXPRESSION_TOKEN, value: 1 },
893
+ { type: CLOSE_TAG_TOKEN }
894
+ ]);
895
+ });
896
+
897
+ it("should tokenize shorthand close tag", () => {
898
+ const tokens = tokenizeTemplate`<Comp>content<//>`;
899
+
900
+ expect(tokens).toEqual([
901
+ { type: OPEN_TAG_TOKEN },
902
+ { type: IDENTIFIER_TOKEN, value: "Comp" },
903
+ { type: CLOSE_TAG_TOKEN },
904
+ { type: TEXT_TOKEN, value: "content" },
905
+ { type: OPEN_TAG_TOKEN },
906
+ { type: SLASH_TOKEN },
907
+ { type: SLASH_TOKEN },
908
+ { type: CLOSE_TAG_TOKEN }
909
+ ]);
910
+ });
911
+
912
+ it("should tokenize dynamic tag with attributes", () => {
913
+ const Comp = "Comp";
914
+ const tokens = tokenizeTemplate`<${Comp} id="test" />`;
915
+
916
+ expect(tokens).toEqual([
917
+ { type: OPEN_TAG_TOKEN },
918
+ { type: EXPRESSION_TOKEN, value: 0 },
919
+ { type: IDENTIFIER_TOKEN, value: "id" },
920
+ { type: EQUALS_TOKEN },
921
+ { type: STRING_TOKEN, value: "test", quote: '"' },
922
+ { type: SLASH_TOKEN },
923
+ { type: CLOSE_TAG_TOKEN }
924
+ ]);
925
+ });
926
+ });
927
+
928
+ describe("comments handling", () => {
929
+ it("should not tokenize comments", () => {
930
+ const tokens = tokenizeTemplate`<div><!-- This is a comment --></div>`;
931
+ expect(tokens).toEqual([
932
+ { type: OPEN_TAG_TOKEN },
933
+ { type: IDENTIFIER_TOKEN, value: "div" },
934
+ { type: CLOSE_TAG_TOKEN },
935
+ { type: OPEN_TAG_TOKEN },
936
+ { type: SLASH_TOKEN },
937
+ { type: IDENTIFIER_TOKEN, value: "div" },
938
+ { type: CLOSE_TAG_TOKEN }
939
+ ]);
940
+ });
941
+
942
+ it("should handle comments with special characters", () => {
943
+ const tokens = tokenizeTemplate`<!-- Special chars: <>&'" -->`;
944
+ expect(tokens).toEqual([]);
945
+ });
946
+
947
+ it("should handle comments with expressions inside", () => {
948
+ const value = "test";
949
+ const tokens = tokenizeTemplate`<!-- Comment with ${value} inside -->`;
950
+ expect(tokens).toEqual([]);
951
+ });
952
+
953
+ it("should skip line comments in tags", () => {
954
+ const tokens = tokenizeTemplate`<button
955
+ disabled//comment
956
+ //handle with expression=${1}
957
+ class="btn"
958
+ />`;
959
+
960
+ expect(tokens).toEqual([
961
+ { type: OPEN_TAG_TOKEN },
962
+ { type: IDENTIFIER_TOKEN, value: "button" },
963
+ { type: IDENTIFIER_TOKEN, value: "disabled" },
964
+ { type: IDENTIFIER_TOKEN, value: "class" },
965
+ { type: EQUALS_TOKEN },
966
+ { type: STRING_TOKEN, value: "btn", quote: '"' },
967
+ { type: SLASH_TOKEN },
968
+ { type: CLOSE_TAG_TOKEN }
969
+ ]);
970
+ });
971
+
972
+ it("should skip line comments before the tag name", () => {
973
+ const tokens = tokenizeTemplate`<
974
+ //comment
975
+ button
976
+ />`;
977
+
978
+ expect(tokens).toEqual([
979
+ { type: OPEN_TAG_TOKEN },
980
+ { type: IDENTIFIER_TOKEN, value: "button" },
981
+ { type: SLASH_TOKEN },
982
+ { type: CLOSE_TAG_TOKEN }
983
+ ]);
984
+ });
985
+
986
+ it("should keep shorthand closing tags", () => {
987
+ const tokens = tokenizeTemplate`<${0}>Text<//>`;
988
+
989
+ expect(tokens).toEqual([
990
+ { type: OPEN_TAG_TOKEN },
991
+ { type: EXPRESSION_TOKEN, value: 0 },
992
+ { type: CLOSE_TAG_TOKEN },
993
+ { type: TEXT_TOKEN, value: "Text" },
994
+ { type: OPEN_TAG_TOKEN },
995
+ { type: SLASH_TOKEN },
996
+ { type: SLASH_TOKEN },
997
+ { type: CLOSE_TAG_TOKEN }
998
+ ]);
999
+ });
1000
+
1001
+ it("should skip block comments in tags", () => {
1002
+ const tokens = tokenizeTemplate`<button
1003
+ disabled /*comment
1004
+ //handle with expression=${1}
1005
+ */class="btn"
1006
+ />`;
1007
+
1008
+ expect(tokens).toEqual([
1009
+ { type: OPEN_TAG_TOKEN },
1010
+ { type: IDENTIFIER_TOKEN, value: "button" },
1011
+ { type: IDENTIFIER_TOKEN, value: "disabled" },
1012
+ { type: IDENTIFIER_TOKEN, value: "class" },
1013
+ { type: EQUALS_TOKEN },
1014
+ { type: STRING_TOKEN, value: "btn", quote: '"' },
1015
+ { type: SLASH_TOKEN },
1016
+ { type: CLOSE_TAG_TOKEN }
1017
+ ]);
1018
+ });
1019
+ });