@platecms/delta-cast 0.6.0 → 0.7.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.
@@ -0,0 +1,2123 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { normalizeCast } from "./normalize-cast";
3
+ import { Root } from "./schemas/schema";
4
+
5
+ describe("normalizeCast", () => {
6
+ describe("adjacent node merging", () => {
7
+ describe("text node merging", () => {
8
+ it("should merge adjacent text nodes in a paragraph", () => {
9
+ const input: Root = {
10
+ type: "root",
11
+ children: [
12
+ {
13
+ type: "paragraph",
14
+ children: [
15
+ { type: "text", value: "Hello" },
16
+ { type: "text", value: " " },
17
+ { type: "text", value: "world" },
18
+ { type: "text", value: "!" },
19
+ ],
20
+ },
21
+ ],
22
+ };
23
+
24
+ const expected: Root = {
25
+ type: "root",
26
+ children: [
27
+ {
28
+ type: "paragraph",
29
+ children: [{ type: "text", value: "Hello world!" }],
30
+ },
31
+ ],
32
+ };
33
+
34
+ const result = normalizeCast(input);
35
+ expect(result).toEqual(expected);
36
+ });
37
+
38
+ it("should merge text nodes across different containers", () => {
39
+ const input: Root = {
40
+ type: "root",
41
+ children: [
42
+ {
43
+ type: "paragraph",
44
+ children: [
45
+ { type: "text", value: "Start" },
46
+ { type: "text", value: " " },
47
+ { type: "bold", children: [{ type: "text", value: "bold" }] },
48
+ { type: "text", value: " " },
49
+ { type: "text", value: "end" },
50
+ ],
51
+ },
52
+ ],
53
+ };
54
+
55
+ const expected: Root = {
56
+ type: "root",
57
+ children: [
58
+ {
59
+ type: "paragraph",
60
+ children: [
61
+ { type: "text", value: "Start " },
62
+ { type: "bold", children: [{ type: "text", value: "bold" }] },
63
+ { type: "text", value: " end" },
64
+ ],
65
+ },
66
+ ],
67
+ };
68
+
69
+ const result = normalizeCast(input);
70
+ expect(result).toEqual(expected);
71
+ });
72
+
73
+ it("should handle empty text nodes", () => {
74
+ const input: Root = {
75
+ type: "root",
76
+ children: [
77
+ {
78
+ type: "paragraph",
79
+ children: [
80
+ { type: "text", value: "Hello" },
81
+ { type: "text", value: "" },
82
+ { type: "text", value: "world" },
83
+ ],
84
+ },
85
+ ],
86
+ };
87
+
88
+ const expected: Root = {
89
+ type: "root",
90
+ children: [
91
+ {
92
+ type: "paragraph",
93
+ children: [{ type: "text", value: "Helloworld" }],
94
+ },
95
+ ],
96
+ };
97
+
98
+ const result = normalizeCast(input);
99
+ expect(result).toEqual(expected);
100
+ });
101
+ });
102
+
103
+ describe("formatting node merging", () => {
104
+ it("should merge adjacent bold nodes", () => {
105
+ const input: Root = {
106
+ type: "root",
107
+ children: [
108
+ {
109
+ type: "paragraph",
110
+ children: [
111
+ { type: "bold", children: [{ type: "text", value: "Hello" }] },
112
+ { type: "bold", children: [{ type: "text", value: " world" }] },
113
+ ],
114
+ },
115
+ ],
116
+ };
117
+
118
+ const expected: Root = {
119
+ type: "root",
120
+ children: [
121
+ {
122
+ type: "paragraph",
123
+ children: [{ type: "bold", children: [{ type: "text", value: "Hello world" }] }],
124
+ },
125
+ ],
126
+ };
127
+
128
+ const result = normalizeCast(input);
129
+ expect(result).toEqual(expected);
130
+ });
131
+
132
+ it("should merge adjacent italic nodes", () => {
133
+ const input: Root = {
134
+ type: "root",
135
+ children: [
136
+ {
137
+ type: "paragraph",
138
+ children: [
139
+ { type: "italic", children: [{ type: "text", value: "Hello" }] },
140
+ { type: "italic", children: [{ type: "text", value: " world" }] },
141
+ ],
142
+ },
143
+ ],
144
+ };
145
+
146
+ const expected: Root = {
147
+ type: "root",
148
+ children: [
149
+ {
150
+ type: "paragraph",
151
+ children: [{ type: "italic", children: [{ type: "text", value: "Hello world" }] }],
152
+ },
153
+ ],
154
+ };
155
+
156
+ const result = normalizeCast(input);
157
+ expect(result).toEqual(expected);
158
+ });
159
+
160
+ it("should merge adjacent underline nodes", () => {
161
+ const input: Root = {
162
+ type: "root",
163
+ children: [
164
+ {
165
+ type: "paragraph",
166
+ children: [
167
+ { type: "underline", children: [{ type: "text", value: "Hello" }] },
168
+ { type: "underline", children: [{ type: "text", value: " world" }] },
169
+ ],
170
+ },
171
+ ],
172
+ };
173
+
174
+ const expected: Root = {
175
+ type: "root",
176
+ children: [
177
+ {
178
+ type: "paragraph",
179
+ children: [{ type: "underline", children: [{ type: "text", value: "Hello world" }] }],
180
+ },
181
+ ],
182
+ };
183
+
184
+ const result = normalizeCast(input);
185
+ expect(result).toEqual(expected);
186
+ });
187
+
188
+ it("should merge adjacent strikethrough nodes", () => {
189
+ const input: Root = {
190
+ type: "root",
191
+ children: [
192
+ {
193
+ type: "paragraph",
194
+ children: [
195
+ { type: "strikethrough", children: [{ type: "text", value: "Hello" }] },
196
+ { type: "strikethrough", children: [{ type: "text", value: " world" }] },
197
+ ],
198
+ },
199
+ ],
200
+ };
201
+
202
+ const expected: Root = {
203
+ type: "root",
204
+ children: [
205
+ {
206
+ type: "paragraph",
207
+ children: [{ type: "strikethrough", children: [{ type: "text", value: "Hello world" }] }],
208
+ },
209
+ ],
210
+ };
211
+
212
+ const result = normalizeCast(input);
213
+ expect(result).toEqual(expected);
214
+ });
215
+
216
+ it("should merge adjacent highlight nodes", () => {
217
+ const input: Root = {
218
+ type: "root",
219
+ children: [
220
+ {
221
+ type: "paragraph",
222
+ children: [
223
+ { type: "highlight", children: [{ type: "text", value: "Hello" }] },
224
+ { type: "highlight", children: [{ type: "text", value: " world" }] },
225
+ ],
226
+ },
227
+ ],
228
+ };
229
+
230
+ const expected: Root = {
231
+ type: "root",
232
+ children: [
233
+ {
234
+ type: "paragraph",
235
+ children: [{ type: "highlight", children: [{ type: "text", value: "Hello world" }] }],
236
+ },
237
+ ],
238
+ };
239
+
240
+ const result = normalizeCast(input);
241
+ expect(result).toEqual(expected);
242
+ });
243
+
244
+ it("should merge multiple adjacent formatting nodes of the same type", () => {
245
+ const input: Root = {
246
+ type: "root",
247
+ children: [
248
+ {
249
+ type: "paragraph",
250
+ children: [
251
+ { type: "bold", children: [{ type: "text", value: "A" }] },
252
+ { type: "bold", children: [{ type: "text", value: "B" }] },
253
+ { type: "bold", children: [{ type: "text", value: "C" }] },
254
+ ],
255
+ },
256
+ ],
257
+ };
258
+
259
+ const expected: Root = {
260
+ type: "root",
261
+ children: [
262
+ {
263
+ type: "paragraph",
264
+ children: [{ type: "bold", children: [{ type: "text", value: "ABC" }] }],
265
+ },
266
+ ],
267
+ };
268
+
269
+ const result = normalizeCast(input);
270
+ expect(result).toEqual(expected);
271
+ });
272
+
273
+ it("should merge formatting nodes with nested content", () => {
274
+ const input: Root = {
275
+ type: "root",
276
+ children: [
277
+ {
278
+ type: "paragraph",
279
+ children: [
280
+ {
281
+ type: "bold",
282
+ children: [
283
+ { type: "text", value: "Hello" },
284
+ { type: "italic", children: [{ type: "text", value: "italic" }] },
285
+ ],
286
+ },
287
+ { type: "bold", children: [{ type: "text", value: " world" }] },
288
+ ],
289
+ },
290
+ ],
291
+ };
292
+
293
+ const expected: Root = {
294
+ type: "root",
295
+ children: [
296
+ {
297
+ type: "paragraph",
298
+ children: [
299
+ {
300
+ type: "bold",
301
+ children: [
302
+ { type: "text", value: "Hello" },
303
+ { type: "italic", children: [{ type: "text", value: "italic" }] },
304
+ { type: "text", value: " world" },
305
+ ],
306
+ },
307
+ ],
308
+ },
309
+ ],
310
+ };
311
+
312
+ const result = normalizeCast(input);
313
+ expect(result).toEqual(expected);
314
+ });
315
+ });
316
+
317
+ describe("non-mergeable nodes", () => {
318
+ it("should not merge adjacent contentValue nodes", () => {
319
+ const input: Root = {
320
+ type: "root",
321
+ children: [
322
+ {
323
+ type: "paragraph",
324
+ children: [
325
+ { type: "contentValue", prn: "prn:content-value:123" },
326
+ { type: "contentValue", prn: "prn:content-value:456" },
327
+ ],
328
+ },
329
+ ],
330
+ };
331
+
332
+ const result = normalizeCast(input);
333
+ expect(result).toEqual(input); // Should remain unchanged
334
+ });
335
+
336
+ it("should not merge adjacent externalLink nodes", () => {
337
+ const input: Root = {
338
+ type: "root",
339
+ children: [
340
+ {
341
+ type: "paragraph",
342
+ children: [
343
+ { type: "externalLink", url: "https://example.com", children: [{ type: "text", value: "Link 1" }] },
344
+ { type: "externalLink", url: "https://other.com", children: [{ type: "text", value: "Link 2" }] },
345
+ ],
346
+ },
347
+ ],
348
+ };
349
+
350
+ const result = normalizeCast(input);
351
+ expect(result).toEqual(input); // Should remain unchanged
352
+ });
353
+
354
+ it("should not merge adjacent internalLink nodes", () => {
355
+ const input: Root = {
356
+ type: "root",
357
+ children: [
358
+ {
359
+ type: "paragraph",
360
+ children: [
361
+ { type: "internalLink", prn: "prn:path-part:123", children: [{ type: "text", value: "Link 1" }] },
362
+ { type: "internalLink", prn: "prn:path-part:456", children: [{ type: "text", value: "Link 2" }] },
363
+ ],
364
+ },
365
+ ],
366
+ };
367
+
368
+ const result = normalizeCast(input);
369
+ expect(result).toEqual(input); // Should remain unchanged
370
+ });
371
+
372
+ it("should not merge adjacent inlineCode nodes", () => {
373
+ const input: Root = {
374
+ type: "root",
375
+ children: [
376
+ {
377
+ type: "paragraph",
378
+ children: [
379
+ { type: "inlineCode", value: "code1" },
380
+ { type: "inlineCode", value: "code2" },
381
+ ],
382
+ },
383
+ ],
384
+ };
385
+
386
+ const result = normalizeCast(input);
387
+ expect(result).toEqual(input); // Should remain unchanged
388
+ });
389
+
390
+ it("should not merge different types of formatting nodes", () => {
391
+ const input: Root = {
392
+ type: "root",
393
+ children: [
394
+ {
395
+ type: "paragraph",
396
+ children: [
397
+ { type: "bold", children: [{ type: "text", value: "bold" }] },
398
+ { type: "italic", children: [{ type: "text", value: "italic" }] },
399
+ ],
400
+ },
401
+ ],
402
+ };
403
+
404
+ const result = normalizeCast(input);
405
+ expect(result).toEqual(input); // Should remain unchanged
406
+ });
407
+ });
408
+
409
+ describe("mixed scenarios", () => {
410
+ it("should handle complex mixed merging scenarios", () => {
411
+ const input: Root = {
412
+ type: "root",
413
+ children: [
414
+ {
415
+ type: "paragraph",
416
+ children: [
417
+ { type: "text", value: "Start " },
418
+ { type: "bold", children: [{ type: "text", value: "bold1" }] },
419
+ { type: "bold", children: [{ type: "text", value: "bold2" }] },
420
+ { type: "text", value: " " },
421
+ { type: "text", value: "middle " },
422
+ { type: "italic", children: [{ type: "text", value: "italic1" }] },
423
+ { type: "italic", children: [{ type: "text", value: "italic2" }] },
424
+ { type: "text", value: " end" },
425
+ ],
426
+ },
427
+ ],
428
+ };
429
+
430
+ const expected: Root = {
431
+ type: "root",
432
+ children: [
433
+ {
434
+ type: "paragraph",
435
+ children: [
436
+ { type: "text", value: "Start " },
437
+ { type: "bold", children: [{ type: "text", value: "bold1bold2" }] },
438
+ { type: "text", value: " middle " },
439
+ { type: "italic", children: [{ type: "text", value: "italic1italic2" }] },
440
+ { type: "text", value: " end" },
441
+ ],
442
+ },
443
+ ],
444
+ };
445
+
446
+ const result = normalizeCast(input);
447
+ expect(result).toEqual(expected);
448
+ });
449
+
450
+ it("should merge across different block containers", () => {
451
+ const input: Root = {
452
+ type: "root",
453
+ children: [
454
+ {
455
+ type: "paragraph",
456
+ children: [
457
+ { type: "text", value: "Para1 " },
458
+ { type: "text", value: "text" },
459
+ ],
460
+ },
461
+ {
462
+ type: "paragraph",
463
+ children: [
464
+ { type: "text", value: "Para2 " },
465
+ { type: "text", value: "text" },
466
+ ],
467
+ },
468
+ ],
469
+ };
470
+
471
+ const expected: Root = {
472
+ type: "root",
473
+ children: [
474
+ {
475
+ type: "paragraph",
476
+ children: [{ type: "text", value: "Para1 text" }],
477
+ },
478
+ {
479
+ type: "paragraph",
480
+ children: [{ type: "text", value: "Para2 text" }],
481
+ },
482
+ ],
483
+ };
484
+
485
+ const result = normalizeCast(input);
486
+ expect(result).toEqual(expected);
487
+ });
488
+ });
489
+ });
490
+
491
+ describe("formatting node nesting order", () => {
492
+ it("should reorder formatting nodes to follow the standard hierarchy", () => {
493
+ const input: Root = {
494
+ type: "root",
495
+ children: [
496
+ {
497
+ type: "paragraph",
498
+ children: [
499
+ {
500
+ type: "highlight",
501
+ children: [
502
+ {
503
+ type: "bold",
504
+ children: [
505
+ {
506
+ type: "italic",
507
+ children: [{ type: "text", value: "text" }],
508
+ },
509
+ ],
510
+ },
511
+ ],
512
+ },
513
+ ],
514
+ },
515
+ ],
516
+ };
517
+
518
+ const expected: Root = {
519
+ type: "root",
520
+ children: [
521
+ {
522
+ type: "paragraph",
523
+ children: [
524
+ {
525
+ type: "bold",
526
+ children: [
527
+ {
528
+ type: "italic",
529
+ children: [
530
+ {
531
+ type: "highlight",
532
+ children: [{ type: "text", value: "text" }],
533
+ },
534
+ ],
535
+ },
536
+ ],
537
+ },
538
+ ],
539
+ },
540
+ ],
541
+ };
542
+
543
+ const result = normalizeCast(input);
544
+ expect(result).toEqual(expected);
545
+ });
546
+
547
+ it("should reorder multiple levels of mixed formatting", () => {
548
+ const input: Root = {
549
+ type: "root",
550
+ children: [
551
+ {
552
+ type: "paragraph",
553
+ children: [
554
+ {
555
+ type: "strikethrough",
556
+ children: [
557
+ {
558
+ type: "underline",
559
+ children: [
560
+ {
561
+ type: "bold",
562
+ children: [
563
+ {
564
+ type: "highlight",
565
+ children: [
566
+ {
567
+ type: "italic",
568
+ children: [{ type: "text", value: "complex text" }],
569
+ },
570
+ ],
571
+ },
572
+ ],
573
+ },
574
+ ],
575
+ },
576
+ ],
577
+ },
578
+ ],
579
+ },
580
+ ],
581
+ };
582
+
583
+ const expected: Root = {
584
+ type: "root",
585
+ children: [
586
+ {
587
+ type: "paragraph",
588
+ children: [
589
+ {
590
+ type: "bold",
591
+ children: [
592
+ {
593
+ type: "italic",
594
+ children: [
595
+ {
596
+ type: "underline",
597
+ children: [
598
+ {
599
+ type: "strikethrough",
600
+ children: [
601
+ {
602
+ type: "highlight",
603
+ children: [{ type: "text", value: "complex text" }],
604
+ },
605
+ ],
606
+ },
607
+ ],
608
+ },
609
+ ],
610
+ },
611
+ ],
612
+ },
613
+ ],
614
+ },
615
+ ],
616
+ };
617
+
618
+ const result = normalizeCast(input);
619
+ expect(result).toEqual(expected);
620
+ });
621
+
622
+ it("should handle already correctly ordered formatting", () => {
623
+ const input: Root = {
624
+ type: "root",
625
+ children: [
626
+ {
627
+ type: "paragraph",
628
+ children: [
629
+ {
630
+ type: "bold",
631
+ children: [
632
+ {
633
+ type: "italic",
634
+ children: [
635
+ {
636
+ type: "underline",
637
+ children: [
638
+ {
639
+ type: "strikethrough",
640
+ children: [
641
+ {
642
+ type: "highlight",
643
+ children: [{ type: "text", value: "correctly ordered" }],
644
+ },
645
+ ],
646
+ },
647
+ ],
648
+ },
649
+ ],
650
+ },
651
+ ],
652
+ },
653
+ ],
654
+ },
655
+ ],
656
+ };
657
+
658
+ const result = normalizeCast(input);
659
+ expect(result).toEqual(input); // Should remain unchanged
660
+ });
661
+
662
+ it("should reorder formatting nodes with mixed content", () => {
663
+ const input: Root = {
664
+ type: "root",
665
+ children: [
666
+ {
667
+ type: "paragraph",
668
+ children: [
669
+ {
670
+ type: "highlight",
671
+ children: [
672
+ { type: "text", value: "start " },
673
+ {
674
+ type: "bold",
675
+ children: [
676
+ {
677
+ type: "italic",
678
+ children: [{ type: "text", value: "bold italic" }],
679
+ },
680
+ ],
681
+ },
682
+ { type: "text", value: " end" },
683
+ ],
684
+ },
685
+ ],
686
+ },
687
+ ],
688
+ };
689
+
690
+ const expected: Root = {
691
+ type: "root",
692
+ children: [
693
+ {
694
+ type: "paragraph",
695
+ children: [
696
+ {
697
+ type: "highlight",
698
+ children: [{ type: "text", value: "start " }],
699
+ },
700
+ {
701
+ type: "bold",
702
+ children: [
703
+ {
704
+ type: "italic",
705
+ children: [
706
+ {
707
+ type: "highlight",
708
+ children: [{ type: "text", value: "bold italic" }],
709
+ },
710
+ ],
711
+ },
712
+ ],
713
+ },
714
+ {
715
+ type: "highlight",
716
+ children: [{ type: "text", value: " end" }],
717
+ },
718
+ ],
719
+ },
720
+ ],
721
+ };
722
+
723
+ const result = normalizeCast(input);
724
+ expect(result).toEqual(expected);
725
+ });
726
+
727
+ it("should handle single-level formatting nodes", () => {
728
+ const input: Root = {
729
+ type: "root",
730
+ children: [
731
+ {
732
+ type: "paragraph",
733
+ children: [
734
+ {
735
+ type: "highlight",
736
+ children: [{ type: "text", value: "highlighted" }],
737
+ },
738
+ {
739
+ type: "bold",
740
+ children: [{ type: "text", value: "bold" }],
741
+ },
742
+ ],
743
+ },
744
+ ],
745
+ };
746
+
747
+ const result = normalizeCast(input);
748
+ expect(result).toEqual(input); // Should remain unchanged (no nesting)
749
+ });
750
+
751
+ it("should preserve non-formatting nodes during reordering", () => {
752
+ const input: Root = {
753
+ type: "root",
754
+ children: [
755
+ {
756
+ type: "paragraph",
757
+ children: [
758
+ {
759
+ type: "highlight",
760
+ children: [
761
+ {
762
+ type: "bold",
763
+ children: [
764
+ { type: "text", value: "formatted " },
765
+ { type: "inlineCode", value: "code" },
766
+ { type: "text", value: " text" },
767
+ ],
768
+ },
769
+ ],
770
+ },
771
+ ],
772
+ },
773
+ ],
774
+ };
775
+
776
+ const expected: Root = {
777
+ type: "root",
778
+ children: [
779
+ {
780
+ type: "paragraph",
781
+ children: [
782
+ {
783
+ type: "bold",
784
+ children: [
785
+ {
786
+ type: "highlight",
787
+ children: [
788
+ { type: "text", value: "formatted " },
789
+ { type: "inlineCode", value: "code" },
790
+ { type: "text", value: " text" },
791
+ ],
792
+ },
793
+ ],
794
+ },
795
+ ],
796
+ },
797
+ ],
798
+ };
799
+
800
+ const result = normalizeCast(input);
801
+ expect(result).toEqual(expected);
802
+ });
803
+
804
+ it("should handle complex overlapping formatting with multiple subtrees with worst case scenario", () => {
805
+ // Input: _Hello 'world *how*' *are* you?_
806
+ const input: Root = {
807
+ type: "root",
808
+ children: [
809
+ {
810
+ type: "paragraph",
811
+ children: [
812
+ {
813
+ type: "underline",
814
+ children: [
815
+ { type: "text", value: "Hello " },
816
+ {
817
+ type: "italic",
818
+ children: [
819
+ { type: "text", value: "world " },
820
+ {
821
+ type: "bold",
822
+ children: [{ type: "text", value: "how" }],
823
+ },
824
+ ],
825
+ },
826
+ { type: "text", value: " " },
827
+ {
828
+ type: "bold",
829
+ children: [{ type: "text", value: "are" }],
830
+ },
831
+ { type: "text", value: " you?" },
832
+ ],
833
+ },
834
+ ],
835
+ },
836
+ ],
837
+ };
838
+
839
+ const expected: Root = {
840
+ type: "root",
841
+ children: [
842
+ {
843
+ type: "paragraph",
844
+ children: [
845
+ {
846
+ type: "underline",
847
+ children: [{ type: "text", value: "Hello " }],
848
+ },
849
+ {
850
+ type: "italic",
851
+ children: [{ type: "underline", children: [{ type: "text", value: "world " }] }],
852
+ },
853
+ {
854
+ type: "bold",
855
+ children: [
856
+ {
857
+ type: "italic",
858
+ children: [
859
+ {
860
+ type: "underline",
861
+ children: [{ type: "text", value: "how" }],
862
+ },
863
+ ],
864
+ },
865
+ ],
866
+ },
867
+ {
868
+ type: "underline",
869
+ children: [{ type: "text", value: " " }],
870
+ },
871
+ {
872
+ type: "bold",
873
+ children: [
874
+ {
875
+ type: "underline",
876
+ children: [{ type: "text", value: "are" }],
877
+ },
878
+ ],
879
+ },
880
+ {
881
+ type: "underline",
882
+ children: [{ type: "text", value: " you?" }],
883
+ },
884
+ ],
885
+ },
886
+ ],
887
+ };
888
+
889
+ const result = normalizeCast(input);
890
+ expect(result).toEqual(expected);
891
+ });
892
+
893
+ it("should handle complex overlapping formatting with multiple subtrees with best case scenario", () => {
894
+ // Input: *Hello 'world _how_' are you?*
895
+ const input: Root = {
896
+ type: "root",
897
+ children: [
898
+ {
899
+ type: "paragraph",
900
+ children: [
901
+ {
902
+ type: "bold",
903
+ children: [{ type: "text", value: "Hello " }],
904
+ },
905
+ {
906
+ type: "italic",
907
+ children: [
908
+ {
909
+ type: "bold",
910
+ children: [{ type: "text", value: "world " }],
911
+ },
912
+ ],
913
+ },
914
+ {
915
+ type: "bold",
916
+ children: [
917
+ {
918
+ type: "italic",
919
+ children: [
920
+ {
921
+ type: "underline",
922
+ children: [{ type: "text", value: "how" }],
923
+ },
924
+ ],
925
+ },
926
+ ],
927
+ },
928
+ {
929
+ type: "bold",
930
+ children: [{ type: "text", value: " are you?" }],
931
+ },
932
+ ],
933
+ },
934
+ ],
935
+ };
936
+ // Input: *Hello 'world _how_' are you?*
937
+
938
+ const expected: Root = {
939
+ type: "root",
940
+ children: [
941
+ {
942
+ type: "paragraph",
943
+ children: [
944
+ {
945
+ type: "bold",
946
+ children: [
947
+ { type: "text", value: "Hello " },
948
+ {
949
+ type: "italic",
950
+ children: [
951
+ { type: "text", value: "world " },
952
+ { type: "underline", children: [{ type: "text", value: "how" }] },
953
+ ],
954
+ },
955
+ { type: "text", value: " are you?" },
956
+ ],
957
+ },
958
+ ],
959
+ },
960
+ ],
961
+ };
962
+
963
+ const result = normalizeCast(input);
964
+ expect(result).toEqual(expected);
965
+ });
966
+ });
967
+
968
+ describe("duplicate formatting removal", () => {
969
+ it("should remove simple duplicate formatting", () => {
970
+ const input: Root = {
971
+ type: "root",
972
+ children: [
973
+ {
974
+ type: "paragraph",
975
+ children: [
976
+ {
977
+ type: "bold",
978
+ children: [
979
+ {
980
+ type: "bold",
981
+ children: [{ type: "text", value: "double bold" }],
982
+ },
983
+ ],
984
+ },
985
+ ],
986
+ },
987
+ ],
988
+ };
989
+
990
+ const expected: Root = {
991
+ type: "root",
992
+ children: [
993
+ {
994
+ type: "paragraph",
995
+ children: [
996
+ {
997
+ type: "bold",
998
+ children: [{ type: "text", value: "double bold" }],
999
+ },
1000
+ ],
1001
+ },
1002
+ ],
1003
+ };
1004
+
1005
+ const result = normalizeCast(input);
1006
+ expect(result).toEqual(expected);
1007
+ });
1008
+
1009
+ it("should remove multiple levels of duplicate formatting", () => {
1010
+ const input: Root = {
1011
+ type: "root",
1012
+ children: [
1013
+ {
1014
+ type: "paragraph",
1015
+ children: [
1016
+ {
1017
+ type: "bold",
1018
+ children: [
1019
+ {
1020
+ type: "bold",
1021
+ children: [
1022
+ {
1023
+ type: "bold",
1024
+ children: [{ type: "text", value: "triple bold" }],
1025
+ },
1026
+ ],
1027
+ },
1028
+ ],
1029
+ },
1030
+ ],
1031
+ },
1032
+ ],
1033
+ };
1034
+
1035
+ const expected: Root = {
1036
+ type: "root",
1037
+ children: [
1038
+ {
1039
+ type: "paragraph",
1040
+ children: [
1041
+ {
1042
+ type: "bold",
1043
+ children: [{ type: "text", value: "triple bold" }],
1044
+ },
1045
+ ],
1046
+ },
1047
+ ],
1048
+ };
1049
+
1050
+ const result = normalizeCast(input);
1051
+ expect(result).toEqual(expected);
1052
+ });
1053
+
1054
+ it("should remove duplicate formatting with mixed content", () => {
1055
+ const input: Root = {
1056
+ type: "root",
1057
+ children: [
1058
+ {
1059
+ type: "paragraph",
1060
+ children: [
1061
+ {
1062
+ type: "bold",
1063
+ children: [
1064
+ { type: "text", value: "start " },
1065
+ {
1066
+ type: "bold",
1067
+ children: [{ type: "text", value: "double bold" }],
1068
+ },
1069
+ { type: "text", value: " end" },
1070
+ ],
1071
+ },
1072
+ ],
1073
+ },
1074
+ ],
1075
+ };
1076
+
1077
+ const expected: Root = {
1078
+ type: "root",
1079
+ children: [
1080
+ {
1081
+ type: "paragraph",
1082
+ children: [
1083
+ {
1084
+ type: "bold",
1085
+ children: [{ type: "text", value: "start double bold end" }],
1086
+ },
1087
+ ],
1088
+ },
1089
+ ],
1090
+ };
1091
+
1092
+ const result = normalizeCast(input);
1093
+ expect(result).toEqual(expected);
1094
+ });
1095
+
1096
+ it("should remove duplicate formatting with nested different types", () => {
1097
+ const input: Root = {
1098
+ type: "root",
1099
+ children: [
1100
+ {
1101
+ type: "paragraph",
1102
+ children: [
1103
+ {
1104
+ type: "bold",
1105
+ children: [
1106
+ {
1107
+ type: "italic",
1108
+ children: [
1109
+ {
1110
+ type: "bold",
1111
+ children: [{ type: "text", value: "bold italic bold" }],
1112
+ },
1113
+ ],
1114
+ },
1115
+ ],
1116
+ },
1117
+ ],
1118
+ },
1119
+ ],
1120
+ };
1121
+
1122
+ const expected: Root = {
1123
+ type: "root",
1124
+ children: [
1125
+ {
1126
+ type: "paragraph",
1127
+ children: [
1128
+ {
1129
+ type: "bold",
1130
+ children: [
1131
+ {
1132
+ type: "italic",
1133
+ children: [{ type: "text", value: "bold italic bold" }],
1134
+ },
1135
+ ],
1136
+ },
1137
+ ],
1138
+ },
1139
+ ],
1140
+ };
1141
+
1142
+ const result = normalizeCast(input);
1143
+ expect(result).toEqual(expected);
1144
+ });
1145
+
1146
+ it("should remove multiple duplicate formatting nodes of the same type", () => {
1147
+ const input: Root = {
1148
+ type: "root",
1149
+ children: [
1150
+ {
1151
+ type: "paragraph",
1152
+ children: [
1153
+ {
1154
+ type: "bold",
1155
+ children: [
1156
+ {
1157
+ type: "bold",
1158
+ children: [{ type: "text", value: "text1" }],
1159
+ },
1160
+ {
1161
+ type: "bold",
1162
+ children: [{ type: "text", value: "text2" }],
1163
+ },
1164
+ {
1165
+ type: "bold",
1166
+ children: [{ type: "text", value: "text3" }],
1167
+ },
1168
+ ],
1169
+ },
1170
+ ],
1171
+ },
1172
+ ],
1173
+ };
1174
+
1175
+ const expected: Root = {
1176
+ type: "root",
1177
+ children: [
1178
+ {
1179
+ type: "paragraph",
1180
+ children: [
1181
+ {
1182
+ type: "bold",
1183
+ children: [{ type: "text", value: "text1text2text3" }],
1184
+ },
1185
+ ],
1186
+ },
1187
+ ],
1188
+ };
1189
+
1190
+ const result = normalizeCast(input);
1191
+ expect(result).toEqual(expected);
1192
+ });
1193
+
1194
+ it("should handle complex nested duplicate formatting", () => {
1195
+ const input: Root = {
1196
+ type: "root",
1197
+ children: [
1198
+ {
1199
+ type: "paragraph",
1200
+ children: [
1201
+ {
1202
+ type: "bold",
1203
+ children: [
1204
+ {
1205
+ type: "italic",
1206
+ children: [
1207
+ {
1208
+ type: "underline",
1209
+ children: [
1210
+ {
1211
+ type: "bold",
1212
+ children: [
1213
+ {
1214
+ type: "italic",
1215
+ children: [{ type: "text", value: "complex" }],
1216
+ },
1217
+ ],
1218
+ },
1219
+ ],
1220
+ },
1221
+ ],
1222
+ },
1223
+ ],
1224
+ },
1225
+ ],
1226
+ },
1227
+ ],
1228
+ };
1229
+
1230
+ const expected: Root = {
1231
+ type: "root",
1232
+ children: [
1233
+ {
1234
+ type: "paragraph",
1235
+ children: [
1236
+ {
1237
+ type: "bold",
1238
+ children: [
1239
+ {
1240
+ type: "italic",
1241
+ children: [
1242
+ {
1243
+ type: "underline",
1244
+ children: [{ type: "text", value: "complex" }],
1245
+ },
1246
+ ],
1247
+ },
1248
+ ],
1249
+ },
1250
+ ],
1251
+ },
1252
+ ],
1253
+ };
1254
+
1255
+ const result = normalizeCast(input);
1256
+ expect(result).toEqual(expected);
1257
+ });
1258
+
1259
+ it("should preserve non-formatting nodes during duplicate removal", () => {
1260
+ const input: Root = {
1261
+ type: "root",
1262
+ children: [
1263
+ {
1264
+ type: "paragraph",
1265
+ children: [
1266
+ {
1267
+ type: "bold",
1268
+ children: [
1269
+ { type: "text", value: "start " },
1270
+ {
1271
+ type: "bold",
1272
+ children: [{ type: "inlineCode", value: "code" }],
1273
+ },
1274
+ { type: "text", value: " end" },
1275
+ ],
1276
+ },
1277
+ ],
1278
+ },
1279
+ ],
1280
+ };
1281
+
1282
+ const expected: Root = {
1283
+ type: "root",
1284
+ children: [
1285
+ {
1286
+ type: "paragraph",
1287
+ children: [
1288
+ {
1289
+ type: "bold",
1290
+ children: [
1291
+ { type: "text", value: "start " },
1292
+ { type: "inlineCode", value: "code" },
1293
+ { type: "text", value: " end" },
1294
+ ],
1295
+ },
1296
+ ],
1297
+ },
1298
+ ],
1299
+ };
1300
+
1301
+ const result = normalizeCast(input);
1302
+ expect(result).toEqual(expected);
1303
+ });
1304
+
1305
+ it("should handle already correctly formatted content", () => {
1306
+ const input: Root = {
1307
+ type: "root",
1308
+ children: [
1309
+ {
1310
+ type: "paragraph",
1311
+ children: [
1312
+ {
1313
+ type: "bold",
1314
+ children: [
1315
+ {
1316
+ type: "italic",
1317
+ children: [
1318
+ {
1319
+ type: "underline",
1320
+ children: [{ type: "text", value: "correctly formatted" }],
1321
+ },
1322
+ ],
1323
+ },
1324
+ ],
1325
+ },
1326
+ ],
1327
+ },
1328
+ ],
1329
+ };
1330
+
1331
+ const result = normalizeCast(input);
1332
+ expect(result).toEqual(input); // Should remain unchanged
1333
+ });
1334
+
1335
+ it("should remove duplicates across different formatting types", () => {
1336
+ const input: Root = {
1337
+ type: "root",
1338
+ children: [
1339
+ {
1340
+ type: "paragraph",
1341
+ children: [
1342
+ {
1343
+ type: "bold",
1344
+ children: [
1345
+ {
1346
+ type: "italic",
1347
+ children: [
1348
+ {
1349
+ type: "bold",
1350
+ children: [
1351
+ {
1352
+ type: "underline",
1353
+ children: [
1354
+ {
1355
+ type: "italic",
1356
+ children: [{ type: "text", value: "mixed duplicates" }],
1357
+ },
1358
+ ],
1359
+ },
1360
+ ],
1361
+ },
1362
+ ],
1363
+ },
1364
+ ],
1365
+ },
1366
+ ],
1367
+ },
1368
+ ],
1369
+ };
1370
+
1371
+ const expected: Root = {
1372
+ type: "root",
1373
+ children: [
1374
+ {
1375
+ type: "paragraph",
1376
+ children: [
1377
+ {
1378
+ type: "bold",
1379
+ children: [
1380
+ {
1381
+ type: "italic",
1382
+ children: [
1383
+ {
1384
+ type: "underline",
1385
+ children: [{ type: "text", value: "mixed duplicates" }],
1386
+ },
1387
+ ],
1388
+ },
1389
+ ],
1390
+ },
1391
+ ],
1392
+ },
1393
+ ],
1394
+ };
1395
+
1396
+ const result = normalizeCast(input);
1397
+ expect(result).toEqual(expected);
1398
+ });
1399
+
1400
+ it("should handle empty formatting nodes after duplicate removal", () => {
1401
+ const input: Root = {
1402
+ type: "root",
1403
+ children: [
1404
+ {
1405
+ type: "paragraph",
1406
+ children: [
1407
+ {
1408
+ type: "bold",
1409
+ children: [
1410
+ {
1411
+ type: "bold",
1412
+ children: [],
1413
+ },
1414
+ ],
1415
+ },
1416
+ ],
1417
+ },
1418
+ ],
1419
+ };
1420
+
1421
+ const expected: Root = {
1422
+ type: "root",
1423
+ children: [
1424
+ {
1425
+ type: "paragraph",
1426
+ children: [],
1427
+ },
1428
+ ],
1429
+ };
1430
+
1431
+ const result = normalizeCast(input);
1432
+ expect(result).toEqual(expected);
1433
+ });
1434
+ });
1435
+
1436
+ describe("empty node cleanup", () => {
1437
+ it("should remove empty formatting nodes", () => {
1438
+ const input: Root = {
1439
+ type: "root",
1440
+ children: [
1441
+ {
1442
+ type: "paragraph",
1443
+ children: [
1444
+ {
1445
+ type: "bold",
1446
+ children: [],
1447
+ },
1448
+ { type: "text", value: "some text" },
1449
+ ],
1450
+ },
1451
+ ],
1452
+ };
1453
+
1454
+ const expected: Root = {
1455
+ type: "root",
1456
+ children: [
1457
+ {
1458
+ type: "paragraph",
1459
+ children: [{ type: "text", value: "some text" }],
1460
+ },
1461
+ ],
1462
+ };
1463
+
1464
+ const result = normalizeCast(input);
1465
+ expect(result).toEqual(expected);
1466
+ });
1467
+
1468
+ it("should preserve empty container nodes", () => {
1469
+ const input: Root = {
1470
+ type: "root",
1471
+ children: [
1472
+ {
1473
+ type: "paragraph",
1474
+ children: [],
1475
+ },
1476
+ {
1477
+ type: "paragraph",
1478
+ children: [{ type: "text", value: "non-empty paragraph" }],
1479
+ },
1480
+ ],
1481
+ };
1482
+
1483
+ const result = normalizeCast(input);
1484
+ expect(result).toEqual(input); // Should remain unchanged - block nodes are preserved even when empty
1485
+ });
1486
+
1487
+ it("should remove nested empty formatting nodes", () => {
1488
+ const input: Root = {
1489
+ type: "root",
1490
+ children: [
1491
+ {
1492
+ type: "paragraph",
1493
+ children: [
1494
+ {
1495
+ type: "bold",
1496
+ children: [
1497
+ {
1498
+ type: "italic",
1499
+ children: [],
1500
+ },
1501
+ ],
1502
+ },
1503
+ { type: "text", value: "text after empty" },
1504
+ ],
1505
+ },
1506
+ ],
1507
+ };
1508
+
1509
+ const expected: Root = {
1510
+ type: "root",
1511
+ children: [
1512
+ {
1513
+ type: "paragraph",
1514
+ children: [{ type: "text", value: "text after empty" }],
1515
+ },
1516
+ ],
1517
+ };
1518
+
1519
+ const result = normalizeCast(input);
1520
+ expect(result).toEqual(expected);
1521
+ });
1522
+
1523
+ it("should preserve empty list items", () => {
1524
+ const input: Root = {
1525
+ type: "root",
1526
+ children: [
1527
+ {
1528
+ type: "list",
1529
+ ordered: false,
1530
+ children: [
1531
+ {
1532
+ type: "listItem",
1533
+ children: [],
1534
+ },
1535
+ {
1536
+ type: "listItem",
1537
+ children: [{ type: "text", value: "non-empty item" }],
1538
+ },
1539
+ ],
1540
+ },
1541
+ ],
1542
+ };
1543
+
1544
+ const result = normalizeCast(input);
1545
+ expect(result).toEqual(input); // Should remain unchanged - block nodes are preserved even when empty
1546
+ });
1547
+
1548
+ it("should preserve empty lists when all items are empty", () => {
1549
+ const input: Root = {
1550
+ type: "root",
1551
+ children: [
1552
+ {
1553
+ type: "list",
1554
+ ordered: false,
1555
+ children: [
1556
+ {
1557
+ type: "listItem",
1558
+ children: [],
1559
+ },
1560
+ {
1561
+ type: "listItem",
1562
+ children: [],
1563
+ },
1564
+ ],
1565
+ },
1566
+ {
1567
+ type: "paragraph",
1568
+ children: [{ type: "text", value: "paragraph after empty list" }],
1569
+ },
1570
+ ],
1571
+ };
1572
+
1573
+ const result = normalizeCast(input);
1574
+ expect(result).toEqual(input); // Should remain unchanged - block nodes are preserved even when empty
1575
+ });
1576
+
1577
+ it("should preserve non-empty formatting nodes", () => {
1578
+ const input: Root = {
1579
+ type: "root",
1580
+ children: [
1581
+ {
1582
+ type: "paragraph",
1583
+ children: [
1584
+ {
1585
+ type: "bold",
1586
+ children: [{ type: "text", value: "bold text" }],
1587
+ },
1588
+ ],
1589
+ },
1590
+ ],
1591
+ };
1592
+
1593
+ const result = normalizeCast(input);
1594
+ expect(result).toEqual(input); // Should remain unchanged
1595
+ });
1596
+
1597
+ it("should handle complex nested empty nodes", () => {
1598
+ const input: Root = {
1599
+ type: "root",
1600
+ children: [
1601
+ {
1602
+ type: "paragraph",
1603
+ children: [
1604
+ {
1605
+ type: "bold",
1606
+ children: [
1607
+ {
1608
+ type: "italic",
1609
+ children: [
1610
+ {
1611
+ type: "underline",
1612
+ children: [],
1613
+ },
1614
+ ],
1615
+ },
1616
+ ],
1617
+ },
1618
+ { type: "text", value: "text" },
1619
+ ],
1620
+ },
1621
+ ],
1622
+ };
1623
+
1624
+ const expected: Root = {
1625
+ type: "root",
1626
+ children: [
1627
+ {
1628
+ type: "paragraph",
1629
+ children: [{ type: "text", value: "text" }],
1630
+ },
1631
+ ],
1632
+ };
1633
+
1634
+ const result = normalizeCast(input);
1635
+ expect(result).toEqual(expected);
1636
+ });
1637
+
1638
+ it("should preserve empty blockquote", () => {
1639
+ const input: Root = {
1640
+ type: "root",
1641
+ children: [
1642
+ {
1643
+ type: "blockquote",
1644
+ children: [],
1645
+ },
1646
+ {
1647
+ type: "paragraph",
1648
+ children: [{ type: "text", value: "paragraph after empty blockquote" }],
1649
+ },
1650
+ ],
1651
+ };
1652
+
1653
+ const result = normalizeCast(input);
1654
+ expect(result).toEqual(input); // Should remain unchanged - block nodes are preserved even when empty
1655
+ });
1656
+
1657
+ it("should preserve empty heading", () => {
1658
+ const input: Root = {
1659
+ type: "root",
1660
+ children: [
1661
+ {
1662
+ type: "heading",
1663
+ level: 1,
1664
+ children: [],
1665
+ },
1666
+ {
1667
+ type: "paragraph",
1668
+ children: [{ type: "text", value: "paragraph after empty heading" }],
1669
+ },
1670
+ ],
1671
+ };
1672
+
1673
+ const result = normalizeCast(input);
1674
+ expect(result).toEqual(input); // Should remain unchanged - block nodes are preserved even when empty
1675
+ });
1676
+
1677
+ it("should preserve empty code block", () => {
1678
+ const input: Root = {
1679
+ type: "root",
1680
+ children: [
1681
+ {
1682
+ type: "code",
1683
+ children: [],
1684
+ },
1685
+ {
1686
+ type: "paragraph",
1687
+ children: [{ type: "text", value: "paragraph after empty code" }],
1688
+ },
1689
+ ],
1690
+ };
1691
+
1692
+ const result = normalizeCast(input);
1693
+ expect(result).toEqual(input); // Should remain unchanged - block nodes are preserved even when empty
1694
+ });
1695
+
1696
+ it("should preserve root with only empty block children", () => {
1697
+ const input: Root = {
1698
+ type: "root",
1699
+ children: [
1700
+ {
1701
+ type: "paragraph",
1702
+ children: [],
1703
+ },
1704
+ {
1705
+ type: "heading",
1706
+ level: 1,
1707
+ children: [],
1708
+ },
1709
+ ],
1710
+ };
1711
+
1712
+ const result = normalizeCast(input);
1713
+ expect(result).toEqual(input); // Should remain unchanged - block nodes are preserved even when empty
1714
+ });
1715
+
1716
+ it("should preserve text nodes even if they're empty strings", () => {
1717
+ const input: Root = {
1718
+ type: "root",
1719
+ children: [
1720
+ {
1721
+ type: "paragraph",
1722
+ children: [{ type: "text", value: "" }],
1723
+ },
1724
+ ],
1725
+ };
1726
+
1727
+ const result = normalizeCast(input);
1728
+ expect(result).toEqual(input); // Should remain unchanged - empty text nodes are preserved
1729
+ });
1730
+
1731
+ it("should preserve inlineCode nodes even if they're empty", () => {
1732
+ const input: Root = {
1733
+ type: "root",
1734
+ children: [
1735
+ {
1736
+ type: "paragraph",
1737
+ children: [
1738
+ { type: "inlineCode", value: "" },
1739
+ { type: "text", value: "text" },
1740
+ ],
1741
+ },
1742
+ ],
1743
+ };
1744
+
1745
+ const result = normalizeCast(input);
1746
+ expect(result).toEqual(input); // Should remain unchanged - empty inlineCode nodes are preserved
1747
+ });
1748
+ });
1749
+
1750
+ describe("property preservation", () => {
1751
+ it("should preserve list properties (ordered, start)", () => {
1752
+ const input: Root = {
1753
+ type: "root",
1754
+ children: [
1755
+ {
1756
+ type: "list",
1757
+ ordered: true,
1758
+ start: 5,
1759
+ children: [
1760
+ {
1761
+ type: "listItem",
1762
+ children: [{ type: "text", value: "item 1" }],
1763
+ },
1764
+ {
1765
+ type: "listItem",
1766
+ children: [{ type: "text", value: "item 2" }],
1767
+ },
1768
+ ],
1769
+ },
1770
+ ],
1771
+ };
1772
+
1773
+ const result = normalizeCast(input);
1774
+ expect(result).toEqual(input); // Should remain unchanged
1775
+ });
1776
+
1777
+ it("should preserve code block language property", () => {
1778
+ const input: Root = {
1779
+ type: "root",
1780
+ children: [
1781
+ {
1782
+ type: "code",
1783
+ lang: "typescript",
1784
+ children: [{ type: "text", value: "const x = 1;" }],
1785
+ },
1786
+ ],
1787
+ };
1788
+
1789
+ const result = normalizeCast(input);
1790
+ expect(result).toEqual(input); // Should remain unchanged
1791
+ });
1792
+
1793
+ it("should preserve external link properties (url, target)", () => {
1794
+ const input: Root = {
1795
+ type: "root",
1796
+ children: [
1797
+ {
1798
+ type: "paragraph",
1799
+ children: [
1800
+ {
1801
+ type: "externalLink",
1802
+ url: "https://example.com",
1803
+ target: "blank",
1804
+ children: [{ type: "text", value: "external link" }],
1805
+ },
1806
+ ],
1807
+ },
1808
+ ],
1809
+ };
1810
+
1811
+ const result = normalizeCast(input);
1812
+ expect(result).toEqual(input); // Should remain unchanged
1813
+ });
1814
+
1815
+ it("should preserve internal link properties (prn, url, target)", () => {
1816
+ const input: Root = {
1817
+ type: "root",
1818
+ children: [
1819
+ {
1820
+ type: "paragraph",
1821
+ children: [
1822
+ {
1823
+ type: "internalLink",
1824
+ prn: "content-value-123",
1825
+ url: "/some/path",
1826
+ target: "self",
1827
+ children: [{ type: "text", value: "internal link" }],
1828
+ },
1829
+ ],
1830
+ },
1831
+ ],
1832
+ };
1833
+
1834
+ const result = normalizeCast(input);
1835
+ expect(result).toEqual(input); // Should remain unchanged
1836
+ });
1837
+
1838
+ it("should preserve heading levels", () => {
1839
+ const input: Root = {
1840
+ type: "root",
1841
+ children: [
1842
+ {
1843
+ type: "heading",
1844
+ level: 1,
1845
+ children: [{ type: "text", value: "Heading 1" }],
1846
+ },
1847
+ {
1848
+ type: "heading",
1849
+ level: 3,
1850
+ children: [{ type: "text", value: "Heading 3" }],
1851
+ },
1852
+ {
1853
+ type: "heading",
1854
+ level: 6,
1855
+ children: [{ type: "text", value: "Heading 6" }],
1856
+ },
1857
+ ],
1858
+ };
1859
+
1860
+ const result = normalizeCast(input);
1861
+ expect(result).toEqual(input); // Should remain unchanged
1862
+ });
1863
+
1864
+ it("should preserve properties while normalizing content", () => {
1865
+ const input: Root = {
1866
+ type: "root",
1867
+ children: [
1868
+ {
1869
+ type: "list",
1870
+ ordered: false,
1871
+ children: [
1872
+ {
1873
+ type: "listItem",
1874
+ children: [
1875
+ { type: "text", value: "Hello" },
1876
+ { type: "text", value: " " },
1877
+ {
1878
+ type: "bold",
1879
+ children: [
1880
+ {
1881
+ type: "italic",
1882
+ children: [{ type: "text", value: "world" }],
1883
+ },
1884
+ ],
1885
+ },
1886
+ ],
1887
+ },
1888
+ ],
1889
+ },
1890
+ ],
1891
+ };
1892
+
1893
+ const expected: Root = {
1894
+ type: "root",
1895
+ children: [
1896
+ {
1897
+ type: "list",
1898
+ ordered: false, // Property preserved
1899
+ children: [
1900
+ {
1901
+ type: "listItem",
1902
+ children: [
1903
+ { type: "text", value: "Hello " },
1904
+ {
1905
+ type: "bold", // Reordered
1906
+ children: [
1907
+ {
1908
+ type: "italic", // Reordered
1909
+ children: [{ type: "text", value: "world" }],
1910
+ },
1911
+ ],
1912
+ },
1913
+ ],
1914
+ },
1915
+ ],
1916
+ },
1917
+ ],
1918
+ };
1919
+
1920
+ const result = normalizeCast(input);
1921
+ expect(result).toEqual(expected);
1922
+ });
1923
+ });
1924
+
1925
+ describe("contentValue node handling", () => {
1926
+ it("should leave contentValue nodes completely untouched", () => {
1927
+ const input: Root = {
1928
+ type: "root",
1929
+ children: [
1930
+ {
1931
+ type: "paragraph",
1932
+ children: [
1933
+ {
1934
+ type: "contentValue",
1935
+ prn: "content-123",
1936
+ value: {
1937
+ type: "root",
1938
+ children: [
1939
+ {
1940
+ type: "paragraph",
1941
+ children: [
1942
+ { type: "text", value: "Hello" },
1943
+ { type: "text", value: " " },
1944
+ {
1945
+ type: "bold",
1946
+ children: [
1947
+ {
1948
+ type: "bold", // Duplicate formatting
1949
+ children: [{ type: "text", value: "world" }],
1950
+ },
1951
+ ],
1952
+ },
1953
+ ],
1954
+ },
1955
+ ],
1956
+ },
1957
+ },
1958
+ ],
1959
+ },
1960
+ ],
1961
+ };
1962
+
1963
+ const result = normalizeCast(input);
1964
+ expect(result).toEqual(input); // Should remain completely unchanged
1965
+ });
1966
+
1967
+ it("should leave contentValue nodes without nested CAST untouched", () => {
1968
+ const input: Root = {
1969
+ type: "root",
1970
+ children: [
1971
+ {
1972
+ type: "paragraph",
1973
+ children: [
1974
+ {
1975
+ type: "contentValue",
1976
+ prn: "content-456",
1977
+ // No value property
1978
+ },
1979
+ ],
1980
+ },
1981
+ ],
1982
+ };
1983
+
1984
+ const result = normalizeCast(input);
1985
+ expect(result).toEqual(input); // Should remain unchanged
1986
+ });
1987
+
1988
+ it("should normalize content around contentValue nodes but leave contentValue untouched", () => {
1989
+ const input: Root = {
1990
+ type: "root",
1991
+ children: [
1992
+ {
1993
+ type: "paragraph",
1994
+ children: [
1995
+ { type: "text", value: "Before" },
1996
+ { type: "text", value: " " },
1997
+ {
1998
+ type: "contentValue",
1999
+ prn: "content-789",
2000
+ value: {
2001
+ type: "root",
2002
+ children: [
2003
+ {
2004
+ type: "paragraph",
2005
+ children: [
2006
+ {
2007
+ type: "bold",
2008
+ children: [
2009
+ {
2010
+ type: "bold", // This should NOT be normalized
2011
+ children: [{ type: "text", value: "nested content" }],
2012
+ },
2013
+ ],
2014
+ },
2015
+ ],
2016
+ },
2017
+ ],
2018
+ },
2019
+ },
2020
+ { type: "text", value: " " },
2021
+ { type: "text", value: "after" },
2022
+ ],
2023
+ },
2024
+ ],
2025
+ };
2026
+
2027
+ const expected: Root = {
2028
+ type: "root",
2029
+ children: [
2030
+ {
2031
+ type: "paragraph",
2032
+ children: [
2033
+ { type: "text", value: "Before " }, // Merged
2034
+ {
2035
+ type: "contentValue",
2036
+ prn: "content-789",
2037
+ value: {
2038
+ type: "root",
2039
+ children: [
2040
+ {
2041
+ type: "paragraph",
2042
+ children: [
2043
+ {
2044
+ type: "bold",
2045
+ children: [
2046
+ {
2047
+ type: "bold", // Still has duplicate formatting - untouched!
2048
+ children: [{ type: "text", value: "nested content" }],
2049
+ },
2050
+ ],
2051
+ },
2052
+ ],
2053
+ },
2054
+ ],
2055
+ },
2056
+ },
2057
+ { type: "text", value: " after" }, // Merged
2058
+ ],
2059
+ },
2060
+ ],
2061
+ };
2062
+
2063
+ const result = normalizeCast(input);
2064
+ expect(result).toEqual(expected);
2065
+ });
2066
+
2067
+ it("should handle multiple contentValue nodes", () => {
2068
+ const input: Root = {
2069
+ type: "root",
2070
+ children: [
2071
+ {
2072
+ type: "paragraph",
2073
+ children: [
2074
+ {
2075
+ type: "contentValue",
2076
+ prn: "content-1",
2077
+ value: {
2078
+ type: "root",
2079
+ children: [
2080
+ {
2081
+ type: "paragraph",
2082
+ children: [
2083
+ { type: "text", value: "First" },
2084
+ { type: "text", value: " " },
2085
+ { type: "text", value: "content" },
2086
+ ],
2087
+ },
2088
+ ],
2089
+ },
2090
+ },
2091
+ {
2092
+ type: "contentValue",
2093
+ prn: "content-2",
2094
+ value: {
2095
+ type: "root",
2096
+ children: [
2097
+ {
2098
+ type: "paragraph",
2099
+ children: [
2100
+ {
2101
+ type: "bold",
2102
+ children: [
2103
+ {
2104
+ type: "bold", // Duplicate - should remain
2105
+ children: [{ type: "text", value: "Second content" }],
2106
+ },
2107
+ ],
2108
+ },
2109
+ ],
2110
+ },
2111
+ ],
2112
+ },
2113
+ },
2114
+ ],
2115
+ },
2116
+ ],
2117
+ };
2118
+
2119
+ const result = normalizeCast(input);
2120
+ expect(result).toEqual(input); // Both contentValue nodes should remain completely unchanged
2121
+ });
2122
+ });
2123
+ });