@wordpress/core-data 7.40.2-next.v.202602241322.0 → 7.41.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.
- package/CHANGELOG.md +2 -0
- package/build/actions.cjs +1 -1
- package/build/actions.cjs.map +2 -2
- package/build/awareness/types.cjs.map +1 -1
- package/build/entities.cjs +17 -10
- package/build/entities.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +38 -0
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/private-actions.cjs +7 -2
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +4 -1
- package/build/private-apis.cjs.map +2 -2
- package/build/private-selectors.cjs +7 -2
- package/build/private-selectors.cjs.map +2 -2
- package/build/reducer.cjs +11 -1
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +15 -12
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +5 -5
- package/build/sync.cjs.map +1 -1
- package/build/types.cjs.map +1 -1
- package/build/utils/crdt-blocks.cjs +50 -31
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-selection.cjs +46 -18
- package/build/utils/crdt-selection.cjs.map +2 -2
- package/build/utils/crdt.cjs +12 -1
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +1 -1
- package/build-module/actions.mjs.map +2 -2
- package/build-module/entities.mjs +19 -11
- package/build-module/entities.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +37 -0
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/private-actions.mjs +5 -1
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +6 -2
- package/build-module/private-apis.mjs.map +2 -2
- package/build-module/private-selectors.mjs +5 -1
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/reducer.mjs +10 -1
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +15 -12
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +3 -3
- package/build-module/sync.mjs.map +1 -1
- package/build-module/utils/crdt-blocks.mjs +50 -31
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-selection.mjs +45 -18
- package/build-module/utils/crdt-selection.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +16 -6
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/awareness/types.d.ts +5 -0
- package/build-types/awareness/types.d.ts.map +1 -1
- package/build-types/entities.d.ts +1 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts +10 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-actions.d.ts +1 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +7 -0
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/reducer.d.ts +15 -0
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +1 -0
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/sync.d.ts +2 -2
- package/build-types/sync.d.ts.map +1 -1
- package/build-types/types.d.ts +1 -0
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +1 -1
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-selection.d.ts +10 -0
- package/build-types/utils/crdt-selection.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +1 -0
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/package.json +18 -18
- package/src/actions.js +2 -2
- package/src/awareness/types.ts +6 -0
- package/src/entities.js +23 -11
- package/src/hooks/use-post-editor-awareness-state.ts +70 -0
- package/src/private-actions.js +13 -0
- package/src/private-apis.js +4 -0
- package/src/private-selectors.ts +10 -0
- package/src/reducer.js +21 -0
- package/src/resolvers.js +21 -15
- package/src/selectors.ts +1 -0
- package/src/sync.ts +2 -2
- package/src/test/entities.js +47 -14
- package/src/test/resolvers.js +46 -80
- package/src/types.ts +1 -0
- package/src/utils/crdt-blocks.ts +113 -47
- package/src/utils/crdt-selection.ts +84 -24
- package/src/utils/crdt.ts +23 -7
- package/src/utils/test/crdt-blocks.ts +938 -0
- package/src/utils/test/crdt.ts +136 -10
|
@@ -39,6 +39,7 @@ jest.mock( '@wordpress/blocks', () => ( {
|
|
|
39
39
|
*/
|
|
40
40
|
import {
|
|
41
41
|
mergeCrdtBlocks,
|
|
42
|
+
mergeRichTextUpdate,
|
|
42
43
|
type Block,
|
|
43
44
|
type YBlock,
|
|
44
45
|
type YBlocks,
|
|
@@ -477,5 +478,942 @@ describe( 'crdt-blocks', () => {
|
|
|
477
478
|
// The level attribute should still exist
|
|
478
479
|
expect( attributes.get( 'level' ) ).toBe( 1 );
|
|
479
480
|
} );
|
|
481
|
+
|
|
482
|
+
it( 'handles block type changes from non-rich-text to rich-text', () => {
|
|
483
|
+
// Start with freeform block (content is non-rich-text)
|
|
484
|
+
const freeformBlocks: Block[] = [
|
|
485
|
+
{
|
|
486
|
+
name: 'core/freeform',
|
|
487
|
+
attributes: { content: 'Freeform content' },
|
|
488
|
+
innerBlocks: [],
|
|
489
|
+
clientId: 'block-1',
|
|
490
|
+
},
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
mergeCrdtBlocks( yblocks, freeformBlocks, null );
|
|
494
|
+
|
|
495
|
+
const block1 = yblocks.get( 0 );
|
|
496
|
+
const content1 = (
|
|
497
|
+
block1.get( 'attributes' ) as YBlockAttributes
|
|
498
|
+
).get( 'content' );
|
|
499
|
+
expect( block1.get( 'name' ) ).toBe( 'core/freeform' );
|
|
500
|
+
expect( typeof content1 ).toBe( 'string' );
|
|
501
|
+
expect( content1 ).toBe( 'Freeform content' );
|
|
502
|
+
|
|
503
|
+
// Change to paragraph block (content becomes rich-text)
|
|
504
|
+
const paragraphBlocks: Block[] = [
|
|
505
|
+
{
|
|
506
|
+
name: 'core/paragraph',
|
|
507
|
+
attributes: { content: 'Freeform content' },
|
|
508
|
+
innerBlocks: [],
|
|
509
|
+
clientId: 'block-1',
|
|
510
|
+
},
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
mergeCrdtBlocks( yblocks, paragraphBlocks, null );
|
|
514
|
+
|
|
515
|
+
expect( yblocks.length ).toBe( 1 );
|
|
516
|
+
const block2 = yblocks.get( 0 );
|
|
517
|
+
const content2 = (
|
|
518
|
+
block2.get( 'attributes' ) as YBlockAttributes
|
|
519
|
+
).get( 'content' ) as Y.Text;
|
|
520
|
+
expect( block2.get( 'name' ) ).toBe( 'core/paragraph' );
|
|
521
|
+
expect( content2 ).toBeInstanceOf( Y.Text );
|
|
522
|
+
expect( content2.toString() ).toBe( 'Freeform content' );
|
|
523
|
+
} );
|
|
524
|
+
|
|
525
|
+
it( 'syncs nested blocks with blob attributes', () => {
|
|
526
|
+
const nestedGallery: Block[] = [
|
|
527
|
+
{
|
|
528
|
+
name: 'core/group',
|
|
529
|
+
attributes: {},
|
|
530
|
+
innerBlocks: [
|
|
531
|
+
{
|
|
532
|
+
name: 'core/gallery',
|
|
533
|
+
attributes: {},
|
|
534
|
+
innerBlocks: [
|
|
535
|
+
{
|
|
536
|
+
name: 'core/image',
|
|
537
|
+
attributes: {
|
|
538
|
+
url: 'http://example.com/image.jpg',
|
|
539
|
+
blob: 'blob:...',
|
|
540
|
+
},
|
|
541
|
+
innerBlocks: [],
|
|
542
|
+
},
|
|
543
|
+
],
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
},
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
mergeCrdtBlocks( yblocks, nestedGallery, null );
|
|
550
|
+
|
|
551
|
+
expect( yblocks.length ).toBe( 1 );
|
|
552
|
+
const groupBlock = yblocks.get( 0 );
|
|
553
|
+
expect( groupBlock.get( 'name' ) ).toBe( 'core/group' );
|
|
554
|
+
|
|
555
|
+
const innerBlocks = groupBlock.get( 'innerBlocks' ) as YBlocks;
|
|
556
|
+
expect( innerBlocks.length ).toBe( 1 );
|
|
557
|
+
expect( innerBlocks.get( 0 ).get( 'name' ) ).toBe( 'core/gallery' );
|
|
558
|
+
} );
|
|
559
|
+
|
|
560
|
+
it( 'handles complex block reordering', () => {
|
|
561
|
+
const initialBlocks: Block[] = [
|
|
562
|
+
{
|
|
563
|
+
name: 'core/paragraph',
|
|
564
|
+
attributes: { content: 'A' },
|
|
565
|
+
innerBlocks: [],
|
|
566
|
+
clientId: 'block-a',
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
name: 'core/paragraph',
|
|
570
|
+
attributes: { content: 'B' },
|
|
571
|
+
innerBlocks: [],
|
|
572
|
+
clientId: 'block-b',
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: 'core/paragraph',
|
|
576
|
+
attributes: { content: 'C' },
|
|
577
|
+
innerBlocks: [],
|
|
578
|
+
clientId: 'block-c',
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: 'core/paragraph',
|
|
582
|
+
attributes: { content: 'D' },
|
|
583
|
+
innerBlocks: [],
|
|
584
|
+
clientId: 'block-d',
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: 'core/paragraph',
|
|
588
|
+
attributes: { content: 'E' },
|
|
589
|
+
innerBlocks: [],
|
|
590
|
+
clientId: 'block-e',
|
|
591
|
+
},
|
|
592
|
+
];
|
|
593
|
+
|
|
594
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
595
|
+
expect( yblocks.length ).toBe( 5 );
|
|
596
|
+
|
|
597
|
+
// Reorder: [A, B, C, D, E] -> [C, A, E, B, D]
|
|
598
|
+
const reorderedBlocks: Block[] = [
|
|
599
|
+
{
|
|
600
|
+
name: 'core/paragraph',
|
|
601
|
+
attributes: { content: 'C' },
|
|
602
|
+
innerBlocks: [],
|
|
603
|
+
clientId: 'block-c',
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: 'core/paragraph',
|
|
607
|
+
attributes: { content: 'A' },
|
|
608
|
+
innerBlocks: [],
|
|
609
|
+
clientId: 'block-a',
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: 'core/paragraph',
|
|
613
|
+
attributes: { content: 'E' },
|
|
614
|
+
innerBlocks: [],
|
|
615
|
+
clientId: 'block-e',
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: 'core/paragraph',
|
|
619
|
+
attributes: { content: 'B' },
|
|
620
|
+
innerBlocks: [],
|
|
621
|
+
clientId: 'block-b',
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
name: 'core/paragraph',
|
|
625
|
+
attributes: { content: 'D' },
|
|
626
|
+
innerBlocks: [],
|
|
627
|
+
clientId: 'block-d',
|
|
628
|
+
},
|
|
629
|
+
];
|
|
630
|
+
|
|
631
|
+
mergeCrdtBlocks( yblocks, reorderedBlocks, null );
|
|
632
|
+
|
|
633
|
+
expect( yblocks.length ).toBe( 5 );
|
|
634
|
+
const contents = [ 'C', 'A', 'E', 'B', 'D' ];
|
|
635
|
+
contents.forEach( ( expectedContent, i ) => {
|
|
636
|
+
const block = yblocks.get( i );
|
|
637
|
+
const content = (
|
|
638
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
639
|
+
).get( 'content' ) as Y.Text;
|
|
640
|
+
expect( content.toString() ).toBe( expectedContent );
|
|
641
|
+
} );
|
|
642
|
+
} );
|
|
643
|
+
|
|
644
|
+
it( 'handles many deletions (10 blocks to 2 blocks)', () => {
|
|
645
|
+
const manyBlocks: Block[] = Array.from(
|
|
646
|
+
{ length: 10 },
|
|
647
|
+
( _, i ) => ( {
|
|
648
|
+
name: 'core/paragraph',
|
|
649
|
+
attributes: { content: `Block ${ i }` },
|
|
650
|
+
innerBlocks: [],
|
|
651
|
+
clientId: `block-${ i }`,
|
|
652
|
+
} )
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
mergeCrdtBlocks( yblocks, manyBlocks, null );
|
|
656
|
+
expect( yblocks.length ).toBe( 10 );
|
|
657
|
+
|
|
658
|
+
const fewBlocks: Block[] = [
|
|
659
|
+
{
|
|
660
|
+
name: 'core/paragraph',
|
|
661
|
+
attributes: { content: 'Block 0' },
|
|
662
|
+
innerBlocks: [],
|
|
663
|
+
clientId: 'block-0',
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: 'core/paragraph',
|
|
667
|
+
attributes: { content: 'Block 9' },
|
|
668
|
+
innerBlocks: [],
|
|
669
|
+
clientId: 'block-9',
|
|
670
|
+
},
|
|
671
|
+
];
|
|
672
|
+
|
|
673
|
+
mergeCrdtBlocks( yblocks, fewBlocks, null );
|
|
674
|
+
|
|
675
|
+
expect( yblocks.length ).toBe( 2 );
|
|
676
|
+
const content0 = (
|
|
677
|
+
yblocks.get( 0 ).get( 'attributes' ) as YBlockAttributes
|
|
678
|
+
).get( 'content' ) as Y.Text;
|
|
679
|
+
expect( content0.toString() ).toBe( 'Block 0' );
|
|
680
|
+
const content1 = (
|
|
681
|
+
yblocks.get( 1 ).get( 'attributes' ) as YBlockAttributes
|
|
682
|
+
).get( 'content' ) as Y.Text;
|
|
683
|
+
expect( content1.toString() ).toBe( 'Block 9' );
|
|
684
|
+
} );
|
|
685
|
+
|
|
686
|
+
it( 'handles many insertions (2 blocks to 10 blocks)', () => {
|
|
687
|
+
const fewBlocks: Block[] = [
|
|
688
|
+
{
|
|
689
|
+
name: 'core/paragraph',
|
|
690
|
+
attributes: { content: 'Block 0' },
|
|
691
|
+
innerBlocks: [],
|
|
692
|
+
clientId: 'block-0',
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: 'core/paragraph',
|
|
696
|
+
attributes: { content: 'Block 9' },
|
|
697
|
+
innerBlocks: [],
|
|
698
|
+
clientId: 'block-9',
|
|
699
|
+
},
|
|
700
|
+
];
|
|
701
|
+
|
|
702
|
+
mergeCrdtBlocks( yblocks, fewBlocks, null );
|
|
703
|
+
expect( yblocks.length ).toBe( 2 );
|
|
704
|
+
|
|
705
|
+
const manyBlocks: Block[] = Array.from(
|
|
706
|
+
{ length: 10 },
|
|
707
|
+
( _, i ) => ( {
|
|
708
|
+
name: 'core/paragraph',
|
|
709
|
+
attributes: { content: `Block ${ i }` },
|
|
710
|
+
innerBlocks: [],
|
|
711
|
+
clientId: `block-${ i }`,
|
|
712
|
+
} )
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
mergeCrdtBlocks( yblocks, manyBlocks, null );
|
|
716
|
+
|
|
717
|
+
expect( yblocks.length ).toBe( 10 );
|
|
718
|
+
manyBlocks.forEach( ( block, i ) => {
|
|
719
|
+
const yblock = yblocks.get( i );
|
|
720
|
+
const content = (
|
|
721
|
+
yblock.get( 'attributes' ) as YBlockAttributes
|
|
722
|
+
).get( 'content' ) as Y.Text;
|
|
723
|
+
expect( content.toString() ).toBe( `Block ${ i }` );
|
|
724
|
+
} );
|
|
725
|
+
} );
|
|
726
|
+
|
|
727
|
+
it( 'handles changes with all different block content', () => {
|
|
728
|
+
const blocksA: Block[] = [
|
|
729
|
+
{
|
|
730
|
+
name: 'core/paragraph',
|
|
731
|
+
attributes: { content: 'A1' },
|
|
732
|
+
innerBlocks: [],
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: 'core/paragraph',
|
|
736
|
+
attributes: { content: 'A2' },
|
|
737
|
+
innerBlocks: [],
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
name: 'core/paragraph',
|
|
741
|
+
attributes: { content: 'A3' },
|
|
742
|
+
innerBlocks: [],
|
|
743
|
+
},
|
|
744
|
+
];
|
|
745
|
+
|
|
746
|
+
mergeCrdtBlocks( yblocks, blocksA, null );
|
|
747
|
+
expect( yblocks.length ).toBe( 3 );
|
|
748
|
+
|
|
749
|
+
const blocksB: Block[] = [
|
|
750
|
+
{
|
|
751
|
+
name: 'core/paragraph',
|
|
752
|
+
attributes: { content: 'B1' },
|
|
753
|
+
innerBlocks: [],
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: 'core/paragraph',
|
|
757
|
+
attributes: { content: 'B2' },
|
|
758
|
+
innerBlocks: [],
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
name: 'core/paragraph',
|
|
762
|
+
attributes: { content: 'B3' },
|
|
763
|
+
innerBlocks: [],
|
|
764
|
+
},
|
|
765
|
+
];
|
|
766
|
+
|
|
767
|
+
mergeCrdtBlocks( yblocks, blocksB, null );
|
|
768
|
+
|
|
769
|
+
expect( yblocks.length ).toBe( 3 );
|
|
770
|
+
[ 'B1', 'B2', 'B3' ].forEach( ( expected, i ) => {
|
|
771
|
+
const content = (
|
|
772
|
+
yblocks.get( i ).get( 'attributes' ) as YBlockAttributes
|
|
773
|
+
).get( 'content' ) as Y.Text;
|
|
774
|
+
expect( content.toString() ).toBe( expected );
|
|
775
|
+
} );
|
|
776
|
+
} );
|
|
777
|
+
|
|
778
|
+
it( 'clears all blocks when syncing empty array', () => {
|
|
779
|
+
const blocks: Block[] = [
|
|
780
|
+
{
|
|
781
|
+
name: 'core/paragraph',
|
|
782
|
+
attributes: { content: 'Content' },
|
|
783
|
+
innerBlocks: [],
|
|
784
|
+
},
|
|
785
|
+
];
|
|
786
|
+
|
|
787
|
+
mergeCrdtBlocks( yblocks, blocks, null );
|
|
788
|
+
expect( yblocks.length ).toBe( 1 );
|
|
789
|
+
|
|
790
|
+
mergeCrdtBlocks( yblocks, [], null );
|
|
791
|
+
expect( yblocks.length ).toBe( 0 );
|
|
792
|
+
} );
|
|
793
|
+
|
|
794
|
+
it( 'handles deeply nested blocks', () => {
|
|
795
|
+
const deeplyNested: Block[] = [
|
|
796
|
+
{
|
|
797
|
+
name: 'core/group',
|
|
798
|
+
attributes: {},
|
|
799
|
+
innerBlocks: [
|
|
800
|
+
{
|
|
801
|
+
name: 'core/group',
|
|
802
|
+
attributes: {},
|
|
803
|
+
innerBlocks: [
|
|
804
|
+
{
|
|
805
|
+
name: 'core/group',
|
|
806
|
+
attributes: {},
|
|
807
|
+
innerBlocks: [
|
|
808
|
+
{
|
|
809
|
+
name: 'core/group',
|
|
810
|
+
attributes: {},
|
|
811
|
+
innerBlocks: [
|
|
812
|
+
{
|
|
813
|
+
name: 'core/paragraph',
|
|
814
|
+
attributes: {
|
|
815
|
+
content: 'Deep content',
|
|
816
|
+
},
|
|
817
|
+
innerBlocks: [],
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
},
|
|
821
|
+
],
|
|
822
|
+
},
|
|
823
|
+
],
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
},
|
|
827
|
+
];
|
|
828
|
+
|
|
829
|
+
mergeCrdtBlocks( yblocks, deeplyNested, null );
|
|
830
|
+
|
|
831
|
+
// Navigate to the deepest block
|
|
832
|
+
let current: YBlocks | YBlock = yblocks;
|
|
833
|
+
for ( let i = 0; i < 4; i++ ) {
|
|
834
|
+
expect( ( current as YBlocks ).length ).toBe( 1 );
|
|
835
|
+
current = ( current as YBlocks ).get( 0 );
|
|
836
|
+
current = ( current as YBlock ).get( 'innerBlocks' ) as YBlocks;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
expect( ( current as YBlocks ).length ).toBe( 1 );
|
|
840
|
+
const deepBlock = ( current as YBlocks ).get( 0 );
|
|
841
|
+
const content = (
|
|
842
|
+
deepBlock.get( 'attributes' ) as YBlockAttributes
|
|
843
|
+
).get( 'content' ) as Y.Text;
|
|
844
|
+
expect( content.toString() ).toBe( 'Deep content' );
|
|
845
|
+
|
|
846
|
+
// Update innermost block
|
|
847
|
+
const updatedDeep: Block[] = [
|
|
848
|
+
{
|
|
849
|
+
name: 'core/group',
|
|
850
|
+
attributes: {},
|
|
851
|
+
innerBlocks: [
|
|
852
|
+
{
|
|
853
|
+
name: 'core/group',
|
|
854
|
+
attributes: {},
|
|
855
|
+
innerBlocks: [
|
|
856
|
+
{
|
|
857
|
+
name: 'core/group',
|
|
858
|
+
attributes: {},
|
|
859
|
+
innerBlocks: [
|
|
860
|
+
{
|
|
861
|
+
name: 'core/group',
|
|
862
|
+
attributes: {},
|
|
863
|
+
innerBlocks: [
|
|
864
|
+
{
|
|
865
|
+
name: 'core/paragraph',
|
|
866
|
+
attributes: {
|
|
867
|
+
content: 'Updated deep',
|
|
868
|
+
},
|
|
869
|
+
innerBlocks: [],
|
|
870
|
+
},
|
|
871
|
+
],
|
|
872
|
+
},
|
|
873
|
+
],
|
|
874
|
+
},
|
|
875
|
+
],
|
|
876
|
+
},
|
|
877
|
+
],
|
|
878
|
+
},
|
|
879
|
+
];
|
|
880
|
+
|
|
881
|
+
mergeCrdtBlocks( yblocks, updatedDeep, null );
|
|
882
|
+
|
|
883
|
+
// Verify update propagated
|
|
884
|
+
current = yblocks;
|
|
885
|
+
for ( let i = 0; i < 4; i++ ) {
|
|
886
|
+
current = ( current as YBlocks ).get( 0 );
|
|
887
|
+
current = ( current as YBlock ).get( 'innerBlocks' ) as YBlocks;
|
|
888
|
+
}
|
|
889
|
+
const updatedBlock = ( current as YBlocks ).get( 0 );
|
|
890
|
+
const updatedContent = (
|
|
891
|
+
updatedBlock.get( 'attributes' ) as YBlockAttributes
|
|
892
|
+
).get( 'content' ) as Y.Text;
|
|
893
|
+
expect( updatedContent.toString() ).toBe( 'Updated deep' );
|
|
894
|
+
} );
|
|
895
|
+
|
|
896
|
+
it( 'handles null and undefined attribute values', () => {
|
|
897
|
+
const blocksWithNullAttrs: Block[] = [
|
|
898
|
+
{
|
|
899
|
+
name: 'core/paragraph',
|
|
900
|
+
attributes: {
|
|
901
|
+
content: 'Content',
|
|
902
|
+
customAttr: null,
|
|
903
|
+
otherAttr: undefined,
|
|
904
|
+
},
|
|
905
|
+
innerBlocks: [],
|
|
906
|
+
},
|
|
907
|
+
];
|
|
908
|
+
|
|
909
|
+
mergeCrdtBlocks( yblocks, blocksWithNullAttrs, null );
|
|
910
|
+
|
|
911
|
+
expect( yblocks.length ).toBe( 1 );
|
|
912
|
+
const block = yblocks.get( 0 );
|
|
913
|
+
const attributes = block.get( 'attributes' ) as YBlockAttributes;
|
|
914
|
+
expect( attributes.get( 'content' ) ).toBeInstanceOf( Y.Text );
|
|
915
|
+
expect( attributes.get( 'customAttr' ) ).toBe( null );
|
|
916
|
+
} );
|
|
917
|
+
|
|
918
|
+
it( 'handles rich-text updates with cursor at start', () => {
|
|
919
|
+
const blocks: Block[] = [
|
|
920
|
+
{
|
|
921
|
+
name: 'core/paragraph',
|
|
922
|
+
attributes: { content: 'Hello World' },
|
|
923
|
+
innerBlocks: [],
|
|
924
|
+
},
|
|
925
|
+
];
|
|
926
|
+
|
|
927
|
+
mergeCrdtBlocks( yblocks, blocks, null );
|
|
928
|
+
|
|
929
|
+
const updatedBlocks: Block[] = [
|
|
930
|
+
{
|
|
931
|
+
name: 'core/paragraph',
|
|
932
|
+
attributes: { content: 'XHello World' },
|
|
933
|
+
innerBlocks: [],
|
|
934
|
+
},
|
|
935
|
+
];
|
|
936
|
+
|
|
937
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 0 );
|
|
938
|
+
|
|
939
|
+
const block = yblocks.get( 0 );
|
|
940
|
+
const content = (
|
|
941
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
942
|
+
).get( 'content' ) as Y.Text;
|
|
943
|
+
expect( content.toString() ).toBe( 'XHello World' );
|
|
944
|
+
} );
|
|
945
|
+
|
|
946
|
+
it( 'handles rich-text updates with cursor at end', () => {
|
|
947
|
+
const blocks: Block[] = [
|
|
948
|
+
{
|
|
949
|
+
name: 'core/paragraph',
|
|
950
|
+
attributes: { content: 'Hello World' },
|
|
951
|
+
innerBlocks: [],
|
|
952
|
+
},
|
|
953
|
+
];
|
|
954
|
+
|
|
955
|
+
mergeCrdtBlocks( yblocks, blocks, null );
|
|
956
|
+
|
|
957
|
+
const updatedBlocks: Block[] = [
|
|
958
|
+
{
|
|
959
|
+
name: 'core/paragraph',
|
|
960
|
+
attributes: { content: 'Hello World!' },
|
|
961
|
+
innerBlocks: [],
|
|
962
|
+
},
|
|
963
|
+
];
|
|
964
|
+
|
|
965
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 11 );
|
|
966
|
+
|
|
967
|
+
const block = yblocks.get( 0 );
|
|
968
|
+
const content = (
|
|
969
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
970
|
+
).get( 'content' ) as Y.Text;
|
|
971
|
+
expect( content.toString() ).toBe( 'Hello World!' );
|
|
972
|
+
} );
|
|
973
|
+
|
|
974
|
+
it( 'handles rich-text updates with cursor beyond text length', () => {
|
|
975
|
+
const blocks: Block[] = [
|
|
976
|
+
{
|
|
977
|
+
name: 'core/paragraph',
|
|
978
|
+
attributes: { content: 'Hello' },
|
|
979
|
+
innerBlocks: [],
|
|
980
|
+
},
|
|
981
|
+
];
|
|
982
|
+
|
|
983
|
+
mergeCrdtBlocks( yblocks, blocks, null );
|
|
984
|
+
|
|
985
|
+
const updatedBlocks: Block[] = [
|
|
986
|
+
{
|
|
987
|
+
name: 'core/paragraph',
|
|
988
|
+
attributes: { content: 'Hello World' },
|
|
989
|
+
innerBlocks: [],
|
|
990
|
+
},
|
|
991
|
+
];
|
|
992
|
+
|
|
993
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 999 );
|
|
994
|
+
|
|
995
|
+
const block = yblocks.get( 0 );
|
|
996
|
+
const content = (
|
|
997
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
998
|
+
).get( 'content' ) as Y.Text;
|
|
999
|
+
expect( content.toString() ).toBe( 'Hello World' );
|
|
1000
|
+
} );
|
|
1001
|
+
|
|
1002
|
+
it( 'deletes extra block properties not in incoming blocks', () => {
|
|
1003
|
+
const initialBlocks: Block[] = [
|
|
1004
|
+
{
|
|
1005
|
+
name: 'core/paragraph',
|
|
1006
|
+
attributes: { content: 'Content' },
|
|
1007
|
+
innerBlocks: [],
|
|
1008
|
+
clientId: 'block-1',
|
|
1009
|
+
isValid: true,
|
|
1010
|
+
originalContent: 'Original',
|
|
1011
|
+
},
|
|
1012
|
+
];
|
|
1013
|
+
|
|
1014
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1015
|
+
|
|
1016
|
+
const block1 = yblocks.get( 0 );
|
|
1017
|
+
expect( block1.get( 'isValid' ) ).toBe( true );
|
|
1018
|
+
expect( block1.get( 'originalContent' ) ).toBe( 'Original' );
|
|
1019
|
+
|
|
1020
|
+
const updatedBlocks: Block[] = [
|
|
1021
|
+
{
|
|
1022
|
+
name: 'core/paragraph',
|
|
1023
|
+
attributes: { content: 'Content' },
|
|
1024
|
+
innerBlocks: [],
|
|
1025
|
+
clientId: 'block-1',
|
|
1026
|
+
},
|
|
1027
|
+
];
|
|
1028
|
+
|
|
1029
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
1030
|
+
|
|
1031
|
+
const block2 = yblocks.get( 0 );
|
|
1032
|
+
expect( block2.has( 'isValid' ) ).toBe( false );
|
|
1033
|
+
expect( block2.has( 'originalContent' ) ).toBe( false );
|
|
1034
|
+
} );
|
|
1035
|
+
|
|
1036
|
+
it( 'deletes rich-text attributes when removed from block', () => {
|
|
1037
|
+
const blocksWithRichText: Block[] = [
|
|
1038
|
+
{
|
|
1039
|
+
name: 'core/paragraph',
|
|
1040
|
+
attributes: {
|
|
1041
|
+
content: 'Rich text content',
|
|
1042
|
+
caption: 'Caption text',
|
|
1043
|
+
},
|
|
1044
|
+
innerBlocks: [],
|
|
1045
|
+
},
|
|
1046
|
+
];
|
|
1047
|
+
|
|
1048
|
+
mergeCrdtBlocks( yblocks, blocksWithRichText, null );
|
|
1049
|
+
|
|
1050
|
+
const block1 = yblocks.get( 0 );
|
|
1051
|
+
const attrs1 = block1.get( 'attributes' ) as YBlockAttributes;
|
|
1052
|
+
expect( attrs1.has( 'content' ) ).toBe( true );
|
|
1053
|
+
expect( attrs1.has( 'caption' ) ).toBe( true );
|
|
1054
|
+
|
|
1055
|
+
const blocksWithoutCaption: Block[] = [
|
|
1056
|
+
{
|
|
1057
|
+
name: 'core/paragraph',
|
|
1058
|
+
attributes: {
|
|
1059
|
+
content: 'Rich text content',
|
|
1060
|
+
},
|
|
1061
|
+
innerBlocks: [],
|
|
1062
|
+
},
|
|
1063
|
+
];
|
|
1064
|
+
|
|
1065
|
+
mergeCrdtBlocks( yblocks, blocksWithoutCaption, null );
|
|
1066
|
+
|
|
1067
|
+
const block2 = yblocks.get( 0 );
|
|
1068
|
+
const attrs2 = block2.get( 'attributes' ) as YBlockAttributes;
|
|
1069
|
+
expect( attrs2.has( 'content' ) ).toBe( true );
|
|
1070
|
+
expect( attrs2.has( 'caption' ) ).toBe( false );
|
|
1071
|
+
} );
|
|
1072
|
+
} );
|
|
1073
|
+
|
|
1074
|
+
describe( 'emoji handling', () => {
|
|
1075
|
+
// Emoji like 😀 (U+1F600) are surrogate pairs in UTF-16 (.length === 2).
|
|
1076
|
+
// The CRDT sync must preserve them without corruption (no U+FFFD / '�').
|
|
1077
|
+
|
|
1078
|
+
it( 'preserves emoji in initial block content', () => {
|
|
1079
|
+
const blocks: Block[] = [
|
|
1080
|
+
{
|
|
1081
|
+
name: 'core/paragraph',
|
|
1082
|
+
attributes: { content: 'Hello 😀 World' },
|
|
1083
|
+
innerBlocks: [],
|
|
1084
|
+
},
|
|
1085
|
+
];
|
|
1086
|
+
|
|
1087
|
+
mergeCrdtBlocks( yblocks, blocks, null );
|
|
1088
|
+
|
|
1089
|
+
const block = yblocks.get( 0 );
|
|
1090
|
+
const content = (
|
|
1091
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1092
|
+
).get( 'content' ) as Y.Text;
|
|
1093
|
+
expect( content.toString() ).toBe( 'Hello 😀 World' );
|
|
1094
|
+
} );
|
|
1095
|
+
|
|
1096
|
+
it( 'handles inserting emoji into existing rich-text', () => {
|
|
1097
|
+
const initialBlocks: Block[] = [
|
|
1098
|
+
{
|
|
1099
|
+
name: 'core/paragraph',
|
|
1100
|
+
attributes: { content: 'Hello World' },
|
|
1101
|
+
innerBlocks: [],
|
|
1102
|
+
clientId: 'block-1',
|
|
1103
|
+
},
|
|
1104
|
+
];
|
|
1105
|
+
|
|
1106
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1107
|
+
|
|
1108
|
+
const updatedBlocks: Block[] = [
|
|
1109
|
+
{
|
|
1110
|
+
name: 'core/paragraph',
|
|
1111
|
+
attributes: { content: 'Hello 😀 World' },
|
|
1112
|
+
innerBlocks: [],
|
|
1113
|
+
clientId: 'block-1',
|
|
1114
|
+
},
|
|
1115
|
+
];
|
|
1116
|
+
|
|
1117
|
+
// Cursor after 'Hello 😀' = 6 + 2 = 8
|
|
1118
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 8 );
|
|
1119
|
+
|
|
1120
|
+
const block = yblocks.get( 0 );
|
|
1121
|
+
const content = (
|
|
1122
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1123
|
+
).get( 'content' ) as Y.Text;
|
|
1124
|
+
expect( content.toString() ).toBe( 'Hello 😀 World' );
|
|
1125
|
+
} );
|
|
1126
|
+
|
|
1127
|
+
it( 'handles deleting emoji from rich-text', () => {
|
|
1128
|
+
const initialBlocks: Block[] = [
|
|
1129
|
+
{
|
|
1130
|
+
name: 'core/paragraph',
|
|
1131
|
+
attributes: { content: 'Hello 😀 World' },
|
|
1132
|
+
innerBlocks: [],
|
|
1133
|
+
clientId: 'block-1',
|
|
1134
|
+
},
|
|
1135
|
+
];
|
|
1136
|
+
|
|
1137
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1138
|
+
|
|
1139
|
+
const updatedBlocks: Block[] = [
|
|
1140
|
+
{
|
|
1141
|
+
name: 'core/paragraph',
|
|
1142
|
+
attributes: { content: 'Hello World' },
|
|
1143
|
+
innerBlocks: [],
|
|
1144
|
+
clientId: 'block-1',
|
|
1145
|
+
},
|
|
1146
|
+
];
|
|
1147
|
+
|
|
1148
|
+
// Cursor at position 6 (after 'Hello ', emoji was deleted)
|
|
1149
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 6 );
|
|
1150
|
+
|
|
1151
|
+
const block = yblocks.get( 0 );
|
|
1152
|
+
const content = (
|
|
1153
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1154
|
+
).get( 'content' ) as Y.Text;
|
|
1155
|
+
expect( content.toString() ).toBe( 'Hello World' );
|
|
1156
|
+
} );
|
|
1157
|
+
|
|
1158
|
+
it( 'handles typing after emoji in rich-text', () => {
|
|
1159
|
+
const initialBlocks: Block[] = [
|
|
1160
|
+
{
|
|
1161
|
+
name: 'core/paragraph',
|
|
1162
|
+
attributes: { content: 'a😀b' },
|
|
1163
|
+
innerBlocks: [],
|
|
1164
|
+
clientId: 'block-1',
|
|
1165
|
+
},
|
|
1166
|
+
];
|
|
1167
|
+
|
|
1168
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1169
|
+
|
|
1170
|
+
const updatedBlocks: Block[] = [
|
|
1171
|
+
{
|
|
1172
|
+
name: 'core/paragraph',
|
|
1173
|
+
attributes: { content: 'a😀xb' },
|
|
1174
|
+
innerBlocks: [],
|
|
1175
|
+
clientId: 'block-1',
|
|
1176
|
+
},
|
|
1177
|
+
];
|
|
1178
|
+
|
|
1179
|
+
// Cursor after 'a😀x' = 1 + 2 + 1 = 4
|
|
1180
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 4 );
|
|
1181
|
+
|
|
1182
|
+
const block = yblocks.get( 0 );
|
|
1183
|
+
const content = (
|
|
1184
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1185
|
+
).get( 'content' ) as Y.Text;
|
|
1186
|
+
expect( content.toString() ).toBe( 'a😀xb' );
|
|
1187
|
+
} );
|
|
1188
|
+
|
|
1189
|
+
it( 'handles multiple emoji in rich-text updates', () => {
|
|
1190
|
+
const initialBlocks: Block[] = [
|
|
1191
|
+
{
|
|
1192
|
+
name: 'core/paragraph',
|
|
1193
|
+
attributes: { content: '😀🎉🚀' },
|
|
1194
|
+
innerBlocks: [],
|
|
1195
|
+
clientId: 'block-1',
|
|
1196
|
+
},
|
|
1197
|
+
];
|
|
1198
|
+
|
|
1199
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1200
|
+
|
|
1201
|
+
// Insert ' hello ' between first and second emoji
|
|
1202
|
+
const updatedBlocks: Block[] = [
|
|
1203
|
+
{
|
|
1204
|
+
name: 'core/paragraph',
|
|
1205
|
+
attributes: { content: '😀 hello 🎉🚀' },
|
|
1206
|
+
innerBlocks: [],
|
|
1207
|
+
clientId: 'block-1',
|
|
1208
|
+
},
|
|
1209
|
+
];
|
|
1210
|
+
|
|
1211
|
+
// Cursor after '😀 hello ' = 2 + 7 = 9
|
|
1212
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 9 );
|
|
1213
|
+
|
|
1214
|
+
const block = yblocks.get( 0 );
|
|
1215
|
+
const content = (
|
|
1216
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1217
|
+
).get( 'content' ) as Y.Text;
|
|
1218
|
+
expect( content.toString() ).toBe( '😀 hello 🎉🚀' );
|
|
1219
|
+
} );
|
|
1220
|
+
} );
|
|
1221
|
+
|
|
1222
|
+
describe( 'mergeRichTextUpdate - emoji handling', () => {
|
|
1223
|
+
it( 'preserves emoji when appending text', () => {
|
|
1224
|
+
const yText = doc.getText( 'test' );
|
|
1225
|
+
yText.insert( 0, '😀' );
|
|
1226
|
+
|
|
1227
|
+
mergeRichTextUpdate( yText, '😀x' );
|
|
1228
|
+
|
|
1229
|
+
expect( yText.toString() ).toBe( '😀x' );
|
|
1230
|
+
} );
|
|
1231
|
+
|
|
1232
|
+
it( 'preserves emoji when inserting before emoji', () => {
|
|
1233
|
+
const yText = doc.getText( 'test' );
|
|
1234
|
+
yText.insert( 0, '😀' );
|
|
1235
|
+
|
|
1236
|
+
mergeRichTextUpdate( yText, 'x😀' );
|
|
1237
|
+
|
|
1238
|
+
expect( yText.toString() ).toBe( 'x😀' );
|
|
1239
|
+
} );
|
|
1240
|
+
|
|
1241
|
+
it( 'preserves emoji when replacing text around emoji', () => {
|
|
1242
|
+
const yText = doc.getText( 'test' );
|
|
1243
|
+
yText.insert( 0, 'a😀b' );
|
|
1244
|
+
|
|
1245
|
+
mergeRichTextUpdate( yText, 'a😀c', 4 );
|
|
1246
|
+
|
|
1247
|
+
expect( yText.toString() ).toBe( 'a😀c' );
|
|
1248
|
+
} );
|
|
1249
|
+
|
|
1250
|
+
it( 'handles inserting emoji into plain text', () => {
|
|
1251
|
+
const yText = doc.getText( 'test' );
|
|
1252
|
+
yText.insert( 0, 'ab' );
|
|
1253
|
+
|
|
1254
|
+
mergeRichTextUpdate( yText, 'a😀b', 3 );
|
|
1255
|
+
|
|
1256
|
+
expect( yText.toString() ).toBe( 'a😀b' );
|
|
1257
|
+
} );
|
|
1258
|
+
|
|
1259
|
+
it( 'handles deleting emoji', () => {
|
|
1260
|
+
const yText = doc.getText( 'test' );
|
|
1261
|
+
yText.insert( 0, 'a😀b' );
|
|
1262
|
+
|
|
1263
|
+
mergeRichTextUpdate( yText, 'ab', 1 );
|
|
1264
|
+
|
|
1265
|
+
expect( yText.toString() ).toBe( 'ab' );
|
|
1266
|
+
} );
|
|
1267
|
+
|
|
1268
|
+
it( 'handles text with multiple emoji', () => {
|
|
1269
|
+
const yText = doc.getText( 'test' );
|
|
1270
|
+
yText.insert( 0, 'Hello 😀 World 🎉' );
|
|
1271
|
+
|
|
1272
|
+
mergeRichTextUpdate( yText, 'Hello 😀 Beautiful World 🎉', 19 );
|
|
1273
|
+
|
|
1274
|
+
expect( yText.toString() ).toBe( 'Hello 😀 Beautiful World 🎉' );
|
|
1275
|
+
} );
|
|
1276
|
+
|
|
1277
|
+
it( 'handles compound emoji (flag emoji)', () => {
|
|
1278
|
+
// Flag emoji like 🏳️🌈 are compound and has .length === 6 in JavaScript
|
|
1279
|
+
const yText = doc.getText( 'test' );
|
|
1280
|
+
yText.insert( 0, 'a🏳️🌈b' );
|
|
1281
|
+
|
|
1282
|
+
mergeRichTextUpdate( yText, 'a🏳️🌈xb', 7 );
|
|
1283
|
+
|
|
1284
|
+
expect( yText.toString() ).toBe( 'a🏳️🌈xb' );
|
|
1285
|
+
} );
|
|
1286
|
+
|
|
1287
|
+
it( 'handles emoji with skin tone modifier', () => {
|
|
1288
|
+
// 👋🏽 is U+1F44B U+1F3FD (wave + medium skin tone), .length === 4
|
|
1289
|
+
const yText = doc.getText( 'test' );
|
|
1290
|
+
yText.insert( 0, 'Hi 👋🏽' );
|
|
1291
|
+
|
|
1292
|
+
mergeRichTextUpdate( yText, 'Hi 👋🏽!', 6 );
|
|
1293
|
+
|
|
1294
|
+
expect( yText.toString() ).toBe( 'Hi 👋🏽!' );
|
|
1295
|
+
} );
|
|
1296
|
+
} );
|
|
1297
|
+
|
|
1298
|
+
describe( 'supplementary plane characters (non-emoji)', () => {
|
|
1299
|
+
// Characters above U+FFFF are stored as surrogate pairs in UTF-16,
|
|
1300
|
+
// so .length === 2 per character. The diff library v8 counts them
|
|
1301
|
+
// as 1 grapheme cluster, causing the same mismatch as emoji.
|
|
1302
|
+
|
|
1303
|
+
describe( 'mergeCrdtBlocks', () => {
|
|
1304
|
+
it( 'handles CJK Extension B characters (rare kanji)', () => {
|
|
1305
|
+
// 𠮷 (U+20BB7) is a real character used in Japanese names.
|
|
1306
|
+
// Surrogate pair: .length === 2.
|
|
1307
|
+
const initialBlocks: Block[] = [
|
|
1308
|
+
{
|
|
1309
|
+
name: 'core/paragraph',
|
|
1310
|
+
attributes: { content: '𠮷野家' },
|
|
1311
|
+
innerBlocks: [],
|
|
1312
|
+
clientId: 'block-1',
|
|
1313
|
+
},
|
|
1314
|
+
];
|
|
1315
|
+
|
|
1316
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1317
|
+
|
|
1318
|
+
const updatedBlocks: Block[] = [
|
|
1319
|
+
{
|
|
1320
|
+
name: 'core/paragraph',
|
|
1321
|
+
attributes: { content: '𠮷野家は美味しい' },
|
|
1322
|
+
innerBlocks: [],
|
|
1323
|
+
clientId: 'block-1',
|
|
1324
|
+
},
|
|
1325
|
+
];
|
|
1326
|
+
|
|
1327
|
+
// Cursor after '𠮷野家は美味しい' = 2+1+1+1+1+1+1+1 = 9
|
|
1328
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 9 );
|
|
1329
|
+
|
|
1330
|
+
const block = yblocks.get( 0 );
|
|
1331
|
+
const content = (
|
|
1332
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1333
|
+
).get( 'content' ) as Y.Text;
|
|
1334
|
+
expect( content.toString() ).toBe( '𠮷野家は美味しい' );
|
|
1335
|
+
} );
|
|
1336
|
+
|
|
1337
|
+
it( 'handles mathematical symbols from supplementary plane', () => {
|
|
1338
|
+
// 𝐀 (U+1D400) — .length === 2
|
|
1339
|
+
const initialBlocks: Block[] = [
|
|
1340
|
+
{
|
|
1341
|
+
name: 'core/paragraph',
|
|
1342
|
+
attributes: { content: 'Let 𝐀 be' },
|
|
1343
|
+
innerBlocks: [],
|
|
1344
|
+
clientId: 'block-1',
|
|
1345
|
+
},
|
|
1346
|
+
];
|
|
1347
|
+
|
|
1348
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1349
|
+
|
|
1350
|
+
const updatedBlocks: Block[] = [
|
|
1351
|
+
{
|
|
1352
|
+
name: 'core/paragraph',
|
|
1353
|
+
attributes: { content: 'Let 𝐀 be a matrix' },
|
|
1354
|
+
innerBlocks: [],
|
|
1355
|
+
clientId: 'block-1',
|
|
1356
|
+
},
|
|
1357
|
+
];
|
|
1358
|
+
|
|
1359
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 18 );
|
|
1360
|
+
|
|
1361
|
+
const block = yblocks.get( 0 );
|
|
1362
|
+
const content = (
|
|
1363
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1364
|
+
).get( 'content' ) as Y.Text;
|
|
1365
|
+
expect( content.toString() ).toBe( 'Let 𝐀 be a matrix' );
|
|
1366
|
+
} );
|
|
1367
|
+
} );
|
|
1368
|
+
|
|
1369
|
+
describe( 'mergeRichTextUpdate', () => {
|
|
1370
|
+
it( 'preserves CJK Extension B characters when appending', () => {
|
|
1371
|
+
const yText = doc.getText( 'test' );
|
|
1372
|
+
yText.insert( 0, '𠮷' );
|
|
1373
|
+
|
|
1374
|
+
mergeRichTextUpdate( yText, '𠮷x' );
|
|
1375
|
+
|
|
1376
|
+
expect( yText.toString() ).toBe( '𠮷x' );
|
|
1377
|
+
} );
|
|
1378
|
+
|
|
1379
|
+
it( 'handles inserting after CJK Extension B character', () => {
|
|
1380
|
+
const yText = doc.getText( 'test' );
|
|
1381
|
+
yText.insert( 0, 'a𠮷b' );
|
|
1382
|
+
|
|
1383
|
+
mergeRichTextUpdate( yText, 'a𠮷xb', 4 );
|
|
1384
|
+
|
|
1385
|
+
expect( yText.toString() ).toBe( 'a𠮷xb' );
|
|
1386
|
+
} );
|
|
1387
|
+
|
|
1388
|
+
it( 'handles mathematical symbols from supplementary plane', () => {
|
|
1389
|
+
// 𝐀 (U+1D400) — .length === 2
|
|
1390
|
+
const yText = doc.getText( 'test' );
|
|
1391
|
+
yText.insert( 0, 'a𝐀b' );
|
|
1392
|
+
|
|
1393
|
+
mergeRichTextUpdate( yText, 'a𝐀xb', 4 );
|
|
1394
|
+
|
|
1395
|
+
expect( yText.toString() ).toBe( 'a𝐀xb' );
|
|
1396
|
+
} );
|
|
1397
|
+
|
|
1398
|
+
it( 'handles mixed surrogate pairs and BMP text', () => {
|
|
1399
|
+
// 𠮷 (CJK Ext B) + 😀 (emoji) — both surrogate pairs
|
|
1400
|
+
const yText = doc.getText( 'test' );
|
|
1401
|
+
yText.insert( 0, '𠮷😀' );
|
|
1402
|
+
|
|
1403
|
+
mergeRichTextUpdate( yText, '𠮷😀!' );
|
|
1404
|
+
|
|
1405
|
+
expect( yText.toString() ).toBe( '𠮷😀!' );
|
|
1406
|
+
} );
|
|
1407
|
+
|
|
1408
|
+
it( 'handles musical symbols (supplementary plane)', () => {
|
|
1409
|
+
// 𝄞 (U+1D11E, Musical Symbol G Clef) — .length === 2
|
|
1410
|
+
const yText = doc.getText( 'test' );
|
|
1411
|
+
yText.insert( 0, 'a𝄞b' );
|
|
1412
|
+
|
|
1413
|
+
mergeRichTextUpdate( yText, 'a𝄞xb', 4 );
|
|
1414
|
+
|
|
1415
|
+
expect( yText.toString() ).toBe( 'a𝄞xb' );
|
|
1416
|
+
} );
|
|
1417
|
+
} );
|
|
480
1418
|
} );
|
|
481
1419
|
} );
|