@wordpress/core-data 7.40.2-next.v.202602271551.0 → 7.40.2
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/README.md +0 -27
- package/build/actions.cjs +9 -26
- package/build/actions.cjs.map +2 -2
- package/build/awareness/post-editor-awareness.cjs +45 -6
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/awareness/types.cjs.map +1 -1
- package/build/entities.cjs +36 -16
- package/build/entities.cjs.map +2 -2
- package/build/entity-provider.cjs +15 -6
- package/build/entity-provider.cjs.map +2 -2
- package/build/hooks/use-entity-prop.cjs +33 -2
- package/build/hooks/use-entity-prop.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +120 -1
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/index.cjs +0 -3
- package/build/index.cjs.map +2 -2
- package/build/private-actions.cjs +27 -3
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +14 -5
- package/build/private-apis.cjs.map +3 -3
- package/build/private-selectors.cjs +22 -2
- package/build/private-selectors.cjs.map +2 -2
- package/build/queried-data/actions.cjs +1 -1
- package/build/queried-data/actions.cjs.map +2 -2
- package/build/queried-data/reducer.cjs +19 -13
- package/build/queried-data/reducer.cjs.map +2 -2
- package/build/reducer.cjs +19 -2
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +24 -17
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +0 -15
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +12 -3
- package/build/sync.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/block-selection-history.cjs +1 -1
- package/build/utils/block-selection-history.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +85 -28
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-selection.cjs +4 -1
- package/build/utils/crdt-selection.cjs.map +2 -2
- package/build/utils/crdt-text.cjs +52 -0
- package/build/utils/crdt-text.cjs.map +7 -0
- package/build/utils/crdt-user-selections.cjs +16 -6
- package/build/utils/crdt-user-selections.cjs.map +3 -3
- package/build/utils/crdt-utils.cjs +54 -2
- package/build/utils/crdt-utils.cjs.map +2 -2
- package/build/utils/crdt.cjs +26 -28
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +14 -26
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/post-editor-awareness.mjs +47 -7
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/entities.mjs +37 -16
- package/build-module/entities.mjs.map +2 -2
- package/build-module/entity-provider.mjs +15 -6
- package/build-module/entity-provider.mjs.map +2 -2
- package/build-module/hooks/use-entity-prop.mjs +34 -3
- package/build-module/hooks/use-entity-prop.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +117 -1
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/index.mjs +0 -2
- package/build-module/index.mjs.map +2 -2
- package/build-module/private-actions.mjs +24 -2
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +21 -6
- package/build-module/private-apis.mjs.map +3 -3
- package/build-module/private-selectors.mjs +19 -1
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/queried-data/actions.mjs +1 -1
- package/build-module/queried-data/actions.mjs.map +2 -2
- package/build-module/queried-data/reducer.mjs +19 -13
- package/build-module/queried-data/reducer.mjs.map +2 -2
- package/build-module/reducer.mjs +18 -2
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +24 -17
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +0 -14
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +8 -2
- package/build-module/sync.mjs.map +2 -2
- package/build-module/utils/block-selection-history.mjs +5 -2
- package/build-module/utils/block-selection-history.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +84 -28
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-selection.mjs +8 -2
- package/build-module/utils/crdt-selection.mjs.map +2 -2
- package/build-module/utils/crdt-text.mjs +26 -0
- package/build-module/utils/crdt-text.mjs.map +7 -0
- package/build-module/utils/crdt-user-selections.mjs +16 -7
- package/build-module/utils/crdt-user-selections.mjs.map +3 -3
- package/build-module/utils/crdt-utils.mjs +51 -1
- package/build-module/utils/crdt-utils.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +26 -28
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts +0 -11
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/post-editor-awareness.d.ts +2 -2
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/awareness/types.d.ts +6 -1
- package/build-types/awareness/types.d.ts.map +1 -1
- package/build-types/entities.d.ts +20 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/entity-provider.d.ts +11 -6
- package/build-types/entity-provider.d.ts.map +1 -1
- package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts +40 -7
- package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
- package/build-types/index.d.ts +0 -3
- package/build-types/index.d.ts.map +1 -1
- package/build-types/parsed-blocks-cache.d.ts +10 -0
- package/build-types/parsed-blocks-cache.d.ts.map +1 -0
- package/build-types/private-actions.d.ts +12 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-apis.d.ts +21 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +17 -0
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/queried-data/reducer.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 -9
- 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 +10 -2
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/block-selection-history.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +11 -0
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-selection.d.ts.map +1 -1
- package/build-types/utils/crdt-text.d.ts +16 -0
- package/build-types/utils/crdt-text.d.ts.map +1 -0
- package/build-types/utils/crdt-user-selections.d.ts +17 -4
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
- package/build-types/utils/crdt-utils.d.ts +20 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +13 -7
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
- package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
- package/build-types/utils/save-crdt-doc.d.ts +8 -0
- package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
- package/build-types/utils/test/crdt-utils.d.ts +2 -0
- package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
- package/package.json +18 -18
- package/src/actions.js +26 -41
- package/src/awareness/post-editor-awareness.ts +106 -7
- package/src/awareness/test/post-editor-awareness.ts +50 -10
- package/src/awareness/types.ts +7 -1
- package/src/entities.js +43 -17
- package/src/entity-provider.js +24 -11
- package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
- package/src/hooks/use-entity-prop.js +43 -3
- package/src/hooks/use-post-editor-awareness-state.ts +229 -7
- package/src/index.js +0 -6
- package/src/private-actions.js +43 -1
- package/src/{private-apis.js → private-apis.ts} +21 -3
- package/src/private-selectors.ts +43 -0
- package/src/queried-data/actions.js +1 -1
- package/src/queried-data/reducer.js +26 -14
- package/src/reducer.js +36 -1
- package/src/resolvers.js +33 -23
- package/src/selectors.ts +1 -32
- package/src/sync.ts +6 -0
- package/src/test/entities.js +204 -16
- package/src/test/private-actions.js +1 -1
- package/src/test/resolvers.js +144 -57
- package/src/test/store.js +116 -0
- package/src/types.ts +20 -2
- package/src/utils/block-selection-history.ts +5 -2
- package/src/utils/crdt-blocks.ts +159 -56
- package/src/utils/crdt-selection.ts +8 -2
- package/src/utils/crdt-text.ts +43 -0
- package/src/utils/crdt-user-selections.ts +29 -8
- package/src/utils/crdt-utils.ts +99 -0
- package/src/utils/crdt.ts +50 -40
- package/src/utils/test/crdt-blocks.ts +588 -24
- package/src/utils/test/crdt-user-selections.ts +5 -0
- package/src/utils/test/crdt-utils.ts +387 -0
- package/src/utils/test/crdt.ts +291 -54
|
@@ -31,19 +31,43 @@ jest.mock( '@wordpress/blocks', () => ( {
|
|
|
31
31
|
name: 'core/paragraph',
|
|
32
32
|
attributes: { content: { type: 'rich-text' } },
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
name: 'core/image',
|
|
36
|
+
attributes: {
|
|
37
|
+
blob: { type: 'string', role: 'local' },
|
|
38
|
+
url: { type: 'string' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'core/table',
|
|
43
|
+
attributes: {
|
|
44
|
+
hasFixedLayout: { type: 'boolean' },
|
|
45
|
+
caption: { type: 'rich-text' },
|
|
46
|
+
head: { type: 'array' },
|
|
47
|
+
body: { type: 'array' },
|
|
48
|
+
foot: { type: 'array' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
34
51
|
],
|
|
35
52
|
} ) );
|
|
36
53
|
|
|
54
|
+
/**
|
|
55
|
+
* WordPress dependencies
|
|
56
|
+
*/
|
|
57
|
+
import { RichTextData } from '@wordpress/rich-text';
|
|
58
|
+
|
|
37
59
|
/**
|
|
38
60
|
* Internal dependencies
|
|
39
61
|
*/
|
|
40
62
|
import {
|
|
41
63
|
mergeCrdtBlocks,
|
|
64
|
+
mergeRichTextUpdate,
|
|
42
65
|
type Block,
|
|
43
66
|
type YBlock,
|
|
44
67
|
type YBlocks,
|
|
45
68
|
type YBlockAttributes,
|
|
46
69
|
} from '../crdt-blocks';
|
|
70
|
+
import { getCachedRichTextData, createRichTextDataCache } from '../crdt-text';
|
|
47
71
|
|
|
48
72
|
describe( 'crdt-blocks', () => {
|
|
49
73
|
let doc: Y.Doc;
|
|
@@ -174,32 +198,30 @@ describe( 'crdt-blocks', () => {
|
|
|
174
198
|
expect( innerBlock.get( 'name' ) ).toBe( 'core/paragraph' );
|
|
175
199
|
} );
|
|
176
200
|
|
|
177
|
-
it( '
|
|
178
|
-
const
|
|
201
|
+
it( 'strips local attributes when syncing blocks', () => {
|
|
202
|
+
const imageWithBlob: Block[] = [
|
|
179
203
|
{
|
|
180
|
-
name: 'core/
|
|
181
|
-
attributes: {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
url: 'http://example.com/image.jpg',
|
|
187
|
-
blob: 'blob:...',
|
|
188
|
-
},
|
|
189
|
-
innerBlocks: [],
|
|
190
|
-
},
|
|
191
|
-
],
|
|
204
|
+
name: 'core/image',
|
|
205
|
+
attributes: {
|
|
206
|
+
url: 'http://example.com/image.jpg',
|
|
207
|
+
blob: 'blob:...',
|
|
208
|
+
},
|
|
209
|
+
innerBlocks: [],
|
|
192
210
|
},
|
|
193
211
|
];
|
|
194
212
|
|
|
195
|
-
mergeCrdtBlocks( yblocks,
|
|
213
|
+
mergeCrdtBlocks( yblocks, imageWithBlob, null );
|
|
196
214
|
|
|
197
|
-
|
|
198
|
-
|
|
215
|
+
expect( yblocks.length ).toBe( 1 );
|
|
216
|
+
const block = yblocks.get( 0 );
|
|
217
|
+
expect( block.get( 'name' ) ).toBe( 'core/image' );
|
|
218
|
+
const attrs = block.get( 'attributes' ) as YBlockAttributes;
|
|
219
|
+
expect( attrs.get( 'url' ) ).toBe( 'http://example.com/image.jpg' );
|
|
220
|
+
expect( attrs.has( 'blob' ) ).toBe( false );
|
|
199
221
|
} );
|
|
200
222
|
|
|
201
|
-
it( '
|
|
202
|
-
const
|
|
223
|
+
it( 'strips local attributes from inner blocks', () => {
|
|
224
|
+
const galleryWithBlobs: Block[] = [
|
|
203
225
|
{
|
|
204
226
|
name: 'core/gallery',
|
|
205
227
|
attributes: {},
|
|
@@ -208,6 +230,7 @@ describe( 'crdt-blocks', () => {
|
|
|
208
230
|
name: 'core/image',
|
|
209
231
|
attributes: {
|
|
210
232
|
url: 'http://example.com/image.jpg',
|
|
233
|
+
blob: 'blob:...',
|
|
211
234
|
},
|
|
212
235
|
innerBlocks: [],
|
|
213
236
|
},
|
|
@@ -215,11 +238,17 @@ describe( 'crdt-blocks', () => {
|
|
|
215
238
|
},
|
|
216
239
|
];
|
|
217
240
|
|
|
218
|
-
mergeCrdtBlocks( yblocks,
|
|
241
|
+
mergeCrdtBlocks( yblocks, galleryWithBlobs, null );
|
|
219
242
|
|
|
220
243
|
expect( yblocks.length ).toBe( 1 );
|
|
221
|
-
const
|
|
222
|
-
expect(
|
|
244
|
+
const gallery = yblocks.get( 0 );
|
|
245
|
+
expect( gallery.get( 'name' ) ).toBe( 'core/gallery' );
|
|
246
|
+
const innerBlocks = gallery.get( 'innerBlocks' ) as YBlocks;
|
|
247
|
+
expect( innerBlocks.length ).toBe( 1 );
|
|
248
|
+
const image = innerBlocks.get( 0 );
|
|
249
|
+
const attrs = image.get( 'attributes' ) as YBlockAttributes;
|
|
250
|
+
expect( attrs.get( 'url' ) ).toBe( 'http://example.com/image.jpg' );
|
|
251
|
+
expect( attrs.has( 'blob' ) ).toBe( false );
|
|
223
252
|
} );
|
|
224
253
|
|
|
225
254
|
it( 'handles block reordering', () => {
|
|
@@ -521,7 +550,7 @@ describe( 'crdt-blocks', () => {
|
|
|
521
550
|
expect( content2.toString() ).toBe( 'Freeform content' );
|
|
522
551
|
} );
|
|
523
552
|
|
|
524
|
-
it( '
|
|
553
|
+
it( 'strips local attributes from deeply nested blocks', () => {
|
|
525
554
|
const nestedGallery: Block[] = [
|
|
526
555
|
{
|
|
527
556
|
name: 'core/group',
|
|
@@ -553,7 +582,13 @@ describe( 'crdt-blocks', () => {
|
|
|
553
582
|
|
|
554
583
|
const innerBlocks = groupBlock.get( 'innerBlocks' ) as YBlocks;
|
|
555
584
|
expect( innerBlocks.length ).toBe( 1 );
|
|
556
|
-
|
|
585
|
+
const gallery = innerBlocks.get( 0 );
|
|
586
|
+
const galleryInner = gallery.get( 'innerBlocks' ) as YBlocks;
|
|
587
|
+
expect( galleryInner.length ).toBe( 1 );
|
|
588
|
+
const image = galleryInner.get( 0 );
|
|
589
|
+
const attrs = image.get( 'attributes' ) as YBlockAttributes;
|
|
590
|
+
expect( attrs.get( 'url' ) ).toBe( 'http://example.com/image.jpg' );
|
|
591
|
+
expect( attrs.has( 'blob' ) ).toBe( false );
|
|
557
592
|
} );
|
|
558
593
|
|
|
559
594
|
it( 'handles complex block reordering', () => {
|
|
@@ -1069,4 +1104,533 @@ describe( 'crdt-blocks', () => {
|
|
|
1069
1104
|
expect( attrs2.has( 'caption' ) ).toBe( false );
|
|
1070
1105
|
} );
|
|
1071
1106
|
} );
|
|
1107
|
+
|
|
1108
|
+
describe( 'table block', () => {
|
|
1109
|
+
it( 'preserves table cell content through CRDT round-trip', () => {
|
|
1110
|
+
const tableBlocks: Block[] = [
|
|
1111
|
+
{
|
|
1112
|
+
name: 'core/table',
|
|
1113
|
+
attributes: {
|
|
1114
|
+
hasFixedLayout: true,
|
|
1115
|
+
body: [
|
|
1116
|
+
{
|
|
1117
|
+
cells: [
|
|
1118
|
+
{
|
|
1119
|
+
content:
|
|
1120
|
+
RichTextData.fromPlainText( '1' ),
|
|
1121
|
+
tag: 'td',
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
content:
|
|
1125
|
+
RichTextData.fromPlainText( '2' ),
|
|
1126
|
+
tag: 'td',
|
|
1127
|
+
},
|
|
1128
|
+
],
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
cells: [
|
|
1132
|
+
{
|
|
1133
|
+
content:
|
|
1134
|
+
RichTextData.fromPlainText( '3' ),
|
|
1135
|
+
tag: 'td',
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
content:
|
|
1139
|
+
RichTextData.fromPlainText( '4' ),
|
|
1140
|
+
tag: 'td',
|
|
1141
|
+
},
|
|
1142
|
+
],
|
|
1143
|
+
},
|
|
1144
|
+
],
|
|
1145
|
+
},
|
|
1146
|
+
innerBlocks: [],
|
|
1147
|
+
},
|
|
1148
|
+
];
|
|
1149
|
+
|
|
1150
|
+
mergeCrdtBlocks( yblocks, tableBlocks, null );
|
|
1151
|
+
|
|
1152
|
+
// Simulate a CRDT encode/decode cycle (persistence or sync).
|
|
1153
|
+
const encoded = Y.encodeStateAsUpdate( doc );
|
|
1154
|
+
const doc2 = new Y.Doc();
|
|
1155
|
+
Y.applyUpdate( doc2, encoded );
|
|
1156
|
+
|
|
1157
|
+
const yblocks2 = doc2.getArray< YBlock >();
|
|
1158
|
+
expect( yblocks2.length ).toBe( 1 );
|
|
1159
|
+
|
|
1160
|
+
const block = yblocks2.get( 0 );
|
|
1161
|
+
const attrs = block.get( 'attributes' ) as YBlockAttributes;
|
|
1162
|
+
const body = attrs.get( 'body' ) as {
|
|
1163
|
+
cells: { content: string; tag: string }[];
|
|
1164
|
+
}[];
|
|
1165
|
+
|
|
1166
|
+
expect( body ).toHaveLength( 2 );
|
|
1167
|
+
expect( body[ 0 ].cells[ 0 ].content ).toBe( '1' );
|
|
1168
|
+
expect( body[ 0 ].cells[ 1 ].content ).toBe( '2' );
|
|
1169
|
+
expect( body[ 1 ].cells[ 0 ].content ).toBe( '3' );
|
|
1170
|
+
expect( body[ 1 ].cells[ 1 ].content ).toBe( '4' );
|
|
1171
|
+
|
|
1172
|
+
doc2.destroy();
|
|
1173
|
+
} );
|
|
1174
|
+
|
|
1175
|
+
it( 'preserves table cell content with HTML formatting', () => {
|
|
1176
|
+
const tableBlocks: Block[] = [
|
|
1177
|
+
{
|
|
1178
|
+
name: 'core/table',
|
|
1179
|
+
attributes: {
|
|
1180
|
+
hasFixedLayout: true,
|
|
1181
|
+
head: [
|
|
1182
|
+
{
|
|
1183
|
+
cells: [
|
|
1184
|
+
{
|
|
1185
|
+
content: RichTextData.fromHTMLString(
|
|
1186
|
+
'<strong>Header</strong>'
|
|
1187
|
+
),
|
|
1188
|
+
tag: 'th',
|
|
1189
|
+
},
|
|
1190
|
+
],
|
|
1191
|
+
},
|
|
1192
|
+
],
|
|
1193
|
+
body: [
|
|
1194
|
+
{
|
|
1195
|
+
cells: [
|
|
1196
|
+
{
|
|
1197
|
+
content: RichTextData.fromHTMLString(
|
|
1198
|
+
'<a href="https://example.com">Link</a>'
|
|
1199
|
+
),
|
|
1200
|
+
tag: 'td',
|
|
1201
|
+
},
|
|
1202
|
+
],
|
|
1203
|
+
},
|
|
1204
|
+
],
|
|
1205
|
+
},
|
|
1206
|
+
innerBlocks: [],
|
|
1207
|
+
},
|
|
1208
|
+
];
|
|
1209
|
+
|
|
1210
|
+
mergeCrdtBlocks( yblocks, tableBlocks, null );
|
|
1211
|
+
|
|
1212
|
+
// Round-trip through encode/decode.
|
|
1213
|
+
const encoded = Y.encodeStateAsUpdate( doc );
|
|
1214
|
+
const doc2 = new Y.Doc();
|
|
1215
|
+
Y.applyUpdate( doc2, encoded );
|
|
1216
|
+
|
|
1217
|
+
const yblocks2 = doc2.getArray< YBlock >();
|
|
1218
|
+
const block = yblocks2.get( 0 );
|
|
1219
|
+
const attrs = block.get( 'attributes' ) as YBlockAttributes;
|
|
1220
|
+
|
|
1221
|
+
const head = attrs.get( 'head' ) as {
|
|
1222
|
+
cells: { content: string }[];
|
|
1223
|
+
}[];
|
|
1224
|
+
expect( head[ 0 ].cells[ 0 ].content ).toBe(
|
|
1225
|
+
'<strong>Header</strong>'
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
const body = attrs.get( 'body' ) as {
|
|
1229
|
+
cells: { content: string }[];
|
|
1230
|
+
}[];
|
|
1231
|
+
expect( body[ 0 ].cells[ 0 ].content ).toBe(
|
|
1232
|
+
'<a href="https://example.com">Link</a>'
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
doc2.destroy();
|
|
1236
|
+
} );
|
|
1237
|
+
} );
|
|
1238
|
+
|
|
1239
|
+
describe( 'emoji handling', () => {
|
|
1240
|
+
// Emoji like 😀 (U+1F600) are surrogate pairs in UTF-16 (.length === 2).
|
|
1241
|
+
// The CRDT sync must preserve them without corruption (no U+FFFD / '�').
|
|
1242
|
+
|
|
1243
|
+
it( 'preserves emoji in initial block content', () => {
|
|
1244
|
+
const blocks: Block[] = [
|
|
1245
|
+
{
|
|
1246
|
+
name: 'core/paragraph',
|
|
1247
|
+
attributes: { content: 'Hello 😀 World' },
|
|
1248
|
+
innerBlocks: [],
|
|
1249
|
+
},
|
|
1250
|
+
];
|
|
1251
|
+
|
|
1252
|
+
mergeCrdtBlocks( yblocks, blocks, null );
|
|
1253
|
+
|
|
1254
|
+
const block = yblocks.get( 0 );
|
|
1255
|
+
const content = (
|
|
1256
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1257
|
+
).get( 'content' ) as Y.Text;
|
|
1258
|
+
expect( content.toString() ).toBe( 'Hello 😀 World' );
|
|
1259
|
+
} );
|
|
1260
|
+
|
|
1261
|
+
it( 'handles inserting emoji into existing rich-text', () => {
|
|
1262
|
+
const initialBlocks: Block[] = [
|
|
1263
|
+
{
|
|
1264
|
+
name: 'core/paragraph',
|
|
1265
|
+
attributes: { content: 'Hello World' },
|
|
1266
|
+
innerBlocks: [],
|
|
1267
|
+
clientId: 'block-1',
|
|
1268
|
+
},
|
|
1269
|
+
];
|
|
1270
|
+
|
|
1271
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1272
|
+
|
|
1273
|
+
const updatedBlocks: Block[] = [
|
|
1274
|
+
{
|
|
1275
|
+
name: 'core/paragraph',
|
|
1276
|
+
attributes: { content: 'Hello 😀 World' },
|
|
1277
|
+
innerBlocks: [],
|
|
1278
|
+
clientId: 'block-1',
|
|
1279
|
+
},
|
|
1280
|
+
];
|
|
1281
|
+
|
|
1282
|
+
// Cursor after 'Hello 😀' = 6 + 2 = 8
|
|
1283
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 8 );
|
|
1284
|
+
|
|
1285
|
+
const block = yblocks.get( 0 );
|
|
1286
|
+
const content = (
|
|
1287
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1288
|
+
).get( 'content' ) as Y.Text;
|
|
1289
|
+
expect( content.toString() ).toBe( 'Hello 😀 World' );
|
|
1290
|
+
} );
|
|
1291
|
+
|
|
1292
|
+
it( 'handles deleting emoji from rich-text', () => {
|
|
1293
|
+
const initialBlocks: Block[] = [
|
|
1294
|
+
{
|
|
1295
|
+
name: 'core/paragraph',
|
|
1296
|
+
attributes: { content: 'Hello 😀 World' },
|
|
1297
|
+
innerBlocks: [],
|
|
1298
|
+
clientId: 'block-1',
|
|
1299
|
+
},
|
|
1300
|
+
];
|
|
1301
|
+
|
|
1302
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1303
|
+
|
|
1304
|
+
const updatedBlocks: Block[] = [
|
|
1305
|
+
{
|
|
1306
|
+
name: 'core/paragraph',
|
|
1307
|
+
attributes: { content: 'Hello World' },
|
|
1308
|
+
innerBlocks: [],
|
|
1309
|
+
clientId: 'block-1',
|
|
1310
|
+
},
|
|
1311
|
+
];
|
|
1312
|
+
|
|
1313
|
+
// Cursor at position 6 (after 'Hello ', emoji was deleted)
|
|
1314
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 6 );
|
|
1315
|
+
|
|
1316
|
+
const block = yblocks.get( 0 );
|
|
1317
|
+
const content = (
|
|
1318
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1319
|
+
).get( 'content' ) as Y.Text;
|
|
1320
|
+
expect( content.toString() ).toBe( 'Hello World' );
|
|
1321
|
+
} );
|
|
1322
|
+
|
|
1323
|
+
it( 'handles typing after emoji in rich-text', () => {
|
|
1324
|
+
const initialBlocks: Block[] = [
|
|
1325
|
+
{
|
|
1326
|
+
name: 'core/paragraph',
|
|
1327
|
+
attributes: { content: 'a😀b' },
|
|
1328
|
+
innerBlocks: [],
|
|
1329
|
+
clientId: 'block-1',
|
|
1330
|
+
},
|
|
1331
|
+
];
|
|
1332
|
+
|
|
1333
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1334
|
+
|
|
1335
|
+
const updatedBlocks: Block[] = [
|
|
1336
|
+
{
|
|
1337
|
+
name: 'core/paragraph',
|
|
1338
|
+
attributes: { content: 'a😀xb' },
|
|
1339
|
+
innerBlocks: [],
|
|
1340
|
+
clientId: 'block-1',
|
|
1341
|
+
},
|
|
1342
|
+
];
|
|
1343
|
+
|
|
1344
|
+
// Cursor after 'a😀x' = 1 + 2 + 1 = 4
|
|
1345
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 4 );
|
|
1346
|
+
|
|
1347
|
+
const block = yblocks.get( 0 );
|
|
1348
|
+
const content = (
|
|
1349
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1350
|
+
).get( 'content' ) as Y.Text;
|
|
1351
|
+
expect( content.toString() ).toBe( 'a😀xb' );
|
|
1352
|
+
} );
|
|
1353
|
+
|
|
1354
|
+
it( 'handles multiple emoji in rich-text updates', () => {
|
|
1355
|
+
const initialBlocks: Block[] = [
|
|
1356
|
+
{
|
|
1357
|
+
name: 'core/paragraph',
|
|
1358
|
+
attributes: { content: '😀🎉🚀' },
|
|
1359
|
+
innerBlocks: [],
|
|
1360
|
+
clientId: 'block-1',
|
|
1361
|
+
},
|
|
1362
|
+
];
|
|
1363
|
+
|
|
1364
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1365
|
+
|
|
1366
|
+
// Insert ' hello ' between first and second emoji
|
|
1367
|
+
const updatedBlocks: Block[] = [
|
|
1368
|
+
{
|
|
1369
|
+
name: 'core/paragraph',
|
|
1370
|
+
attributes: { content: '😀 hello 🎉🚀' },
|
|
1371
|
+
innerBlocks: [],
|
|
1372
|
+
clientId: 'block-1',
|
|
1373
|
+
},
|
|
1374
|
+
];
|
|
1375
|
+
|
|
1376
|
+
// Cursor after '😀 hello ' = 2 + 7 = 9
|
|
1377
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 9 );
|
|
1378
|
+
|
|
1379
|
+
const block = yblocks.get( 0 );
|
|
1380
|
+
const content = (
|
|
1381
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1382
|
+
).get( 'content' ) as Y.Text;
|
|
1383
|
+
expect( content.toString() ).toBe( '😀 hello 🎉🚀' );
|
|
1384
|
+
} );
|
|
1385
|
+
} );
|
|
1386
|
+
|
|
1387
|
+
describe( 'mergeRichTextUpdate - emoji handling', () => {
|
|
1388
|
+
it( 'preserves emoji when appending text', () => {
|
|
1389
|
+
const yText = doc.getText( 'test' );
|
|
1390
|
+
yText.insert( 0, '😀' );
|
|
1391
|
+
|
|
1392
|
+
mergeRichTextUpdate( yText, '😀x' );
|
|
1393
|
+
|
|
1394
|
+
expect( yText.toString() ).toBe( '😀x' );
|
|
1395
|
+
} );
|
|
1396
|
+
|
|
1397
|
+
it( 'preserves emoji when inserting before emoji', () => {
|
|
1398
|
+
const yText = doc.getText( 'test' );
|
|
1399
|
+
yText.insert( 0, '😀' );
|
|
1400
|
+
|
|
1401
|
+
mergeRichTextUpdate( yText, 'x😀' );
|
|
1402
|
+
|
|
1403
|
+
expect( yText.toString() ).toBe( 'x😀' );
|
|
1404
|
+
} );
|
|
1405
|
+
|
|
1406
|
+
it( 'preserves emoji when replacing text around emoji', () => {
|
|
1407
|
+
const yText = doc.getText( 'test' );
|
|
1408
|
+
yText.insert( 0, 'a😀b' );
|
|
1409
|
+
|
|
1410
|
+
mergeRichTextUpdate( yText, 'a😀c', 4 );
|
|
1411
|
+
|
|
1412
|
+
expect( yText.toString() ).toBe( 'a😀c' );
|
|
1413
|
+
} );
|
|
1414
|
+
|
|
1415
|
+
it( 'handles inserting emoji into plain text', () => {
|
|
1416
|
+
const yText = doc.getText( 'test' );
|
|
1417
|
+
yText.insert( 0, 'ab' );
|
|
1418
|
+
|
|
1419
|
+
mergeRichTextUpdate( yText, 'a😀b', 3 );
|
|
1420
|
+
|
|
1421
|
+
expect( yText.toString() ).toBe( 'a😀b' );
|
|
1422
|
+
} );
|
|
1423
|
+
|
|
1424
|
+
it( 'handles deleting emoji', () => {
|
|
1425
|
+
const yText = doc.getText( 'test' );
|
|
1426
|
+
yText.insert( 0, 'a😀b' );
|
|
1427
|
+
|
|
1428
|
+
mergeRichTextUpdate( yText, 'ab', 1 );
|
|
1429
|
+
|
|
1430
|
+
expect( yText.toString() ).toBe( 'ab' );
|
|
1431
|
+
} );
|
|
1432
|
+
|
|
1433
|
+
it( 'handles text with multiple emoji', () => {
|
|
1434
|
+
const yText = doc.getText( 'test' );
|
|
1435
|
+
yText.insert( 0, 'Hello 😀 World 🎉' );
|
|
1436
|
+
|
|
1437
|
+
mergeRichTextUpdate( yText, 'Hello 😀 Beautiful World 🎉', 19 );
|
|
1438
|
+
|
|
1439
|
+
expect( yText.toString() ).toBe( 'Hello 😀 Beautiful World 🎉' );
|
|
1440
|
+
} );
|
|
1441
|
+
|
|
1442
|
+
it( 'handles compound emoji (flag emoji)', () => {
|
|
1443
|
+
// Flag emoji like 🏳️🌈 are compound and has .length === 6 in JavaScript
|
|
1444
|
+
const yText = doc.getText( 'test' );
|
|
1445
|
+
yText.insert( 0, 'a🏳️🌈b' );
|
|
1446
|
+
|
|
1447
|
+
mergeRichTextUpdate( yText, 'a🏳️🌈xb', 7 );
|
|
1448
|
+
|
|
1449
|
+
expect( yText.toString() ).toBe( 'a🏳️🌈xb' );
|
|
1450
|
+
} );
|
|
1451
|
+
|
|
1452
|
+
it( 'handles emoji with skin tone modifier', () => {
|
|
1453
|
+
// 👋🏽 is U+1F44B U+1F3FD (wave + medium skin tone), .length === 4
|
|
1454
|
+
const yText = doc.getText( 'test' );
|
|
1455
|
+
yText.insert( 0, 'Hi 👋🏽' );
|
|
1456
|
+
|
|
1457
|
+
mergeRichTextUpdate( yText, 'Hi 👋🏽!', 6 );
|
|
1458
|
+
|
|
1459
|
+
expect( yText.toString() ).toBe( 'Hi 👋🏽!' );
|
|
1460
|
+
} );
|
|
1461
|
+
} );
|
|
1462
|
+
|
|
1463
|
+
describe( 'supplementary plane characters (non-emoji)', () => {
|
|
1464
|
+
// Characters above U+FFFF are stored as surrogate pairs in UTF-16,
|
|
1465
|
+
// so .length === 2 per character. The diff library v8 counts them
|
|
1466
|
+
// as 1 grapheme cluster, causing the same mismatch as emoji.
|
|
1467
|
+
|
|
1468
|
+
describe( 'mergeCrdtBlocks', () => {
|
|
1469
|
+
it( 'handles CJK Extension B characters (rare kanji)', () => {
|
|
1470
|
+
// 𠮷 (U+20BB7) is a real character used in Japanese names.
|
|
1471
|
+
// Surrogate pair: .length === 2.
|
|
1472
|
+
const initialBlocks: Block[] = [
|
|
1473
|
+
{
|
|
1474
|
+
name: 'core/paragraph',
|
|
1475
|
+
attributes: { content: '𠮷野家' },
|
|
1476
|
+
innerBlocks: [],
|
|
1477
|
+
clientId: 'block-1',
|
|
1478
|
+
},
|
|
1479
|
+
];
|
|
1480
|
+
|
|
1481
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1482
|
+
|
|
1483
|
+
const updatedBlocks: Block[] = [
|
|
1484
|
+
{
|
|
1485
|
+
name: 'core/paragraph',
|
|
1486
|
+
attributes: { content: '𠮷野家は美味しい' },
|
|
1487
|
+
innerBlocks: [],
|
|
1488
|
+
clientId: 'block-1',
|
|
1489
|
+
},
|
|
1490
|
+
];
|
|
1491
|
+
|
|
1492
|
+
// Cursor after '𠮷野家は美味しい' = 2+1+1+1+1+1+1+1 = 9
|
|
1493
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 9 );
|
|
1494
|
+
|
|
1495
|
+
const block = yblocks.get( 0 );
|
|
1496
|
+
const content = (
|
|
1497
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1498
|
+
).get( 'content' ) as Y.Text;
|
|
1499
|
+
expect( content.toString() ).toBe( '𠮷野家は美味しい' );
|
|
1500
|
+
} );
|
|
1501
|
+
|
|
1502
|
+
it( 'handles mathematical symbols from supplementary plane', () => {
|
|
1503
|
+
// 𝐀 (U+1D400) — .length === 2
|
|
1504
|
+
const initialBlocks: Block[] = [
|
|
1505
|
+
{
|
|
1506
|
+
name: 'core/paragraph',
|
|
1507
|
+
attributes: { content: 'Let 𝐀 be' },
|
|
1508
|
+
innerBlocks: [],
|
|
1509
|
+
clientId: 'block-1',
|
|
1510
|
+
},
|
|
1511
|
+
];
|
|
1512
|
+
|
|
1513
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1514
|
+
|
|
1515
|
+
const updatedBlocks: Block[] = [
|
|
1516
|
+
{
|
|
1517
|
+
name: 'core/paragraph',
|
|
1518
|
+
attributes: { content: 'Let 𝐀 be a matrix' },
|
|
1519
|
+
innerBlocks: [],
|
|
1520
|
+
clientId: 'block-1',
|
|
1521
|
+
},
|
|
1522
|
+
];
|
|
1523
|
+
|
|
1524
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, 18 );
|
|
1525
|
+
|
|
1526
|
+
const block = yblocks.get( 0 );
|
|
1527
|
+
const content = (
|
|
1528
|
+
block.get( 'attributes' ) as YBlockAttributes
|
|
1529
|
+
).get( 'content' ) as Y.Text;
|
|
1530
|
+
expect( content.toString() ).toBe( 'Let 𝐀 be a matrix' );
|
|
1531
|
+
} );
|
|
1532
|
+
} );
|
|
1533
|
+
|
|
1534
|
+
describe( 'mergeRichTextUpdate', () => {
|
|
1535
|
+
it( 'preserves CJK Extension B characters when appending', () => {
|
|
1536
|
+
const yText = doc.getText( 'test' );
|
|
1537
|
+
yText.insert( 0, '𠮷' );
|
|
1538
|
+
|
|
1539
|
+
mergeRichTextUpdate( yText, '𠮷x' );
|
|
1540
|
+
|
|
1541
|
+
expect( yText.toString() ).toBe( '𠮷x' );
|
|
1542
|
+
} );
|
|
1543
|
+
|
|
1544
|
+
it( 'handles inserting after CJK Extension B character', () => {
|
|
1545
|
+
const yText = doc.getText( 'test' );
|
|
1546
|
+
yText.insert( 0, 'a𠮷b' );
|
|
1547
|
+
|
|
1548
|
+
mergeRichTextUpdate( yText, 'a𠮷xb', 4 );
|
|
1549
|
+
|
|
1550
|
+
expect( yText.toString() ).toBe( 'a𠮷xb' );
|
|
1551
|
+
} );
|
|
1552
|
+
|
|
1553
|
+
it( 'handles mathematical symbols from supplementary plane', () => {
|
|
1554
|
+
// 𝐀 (U+1D400) — .length === 2
|
|
1555
|
+
const yText = doc.getText( 'test' );
|
|
1556
|
+
yText.insert( 0, 'a𝐀b' );
|
|
1557
|
+
|
|
1558
|
+
mergeRichTextUpdate( yText, 'a𝐀xb', 4 );
|
|
1559
|
+
|
|
1560
|
+
expect( yText.toString() ).toBe( 'a𝐀xb' );
|
|
1561
|
+
} );
|
|
1562
|
+
|
|
1563
|
+
it( 'handles mixed surrogate pairs and BMP text', () => {
|
|
1564
|
+
// 𠮷 (CJK Ext B) + 😀 (emoji) — both surrogate pairs
|
|
1565
|
+
const yText = doc.getText( 'test' );
|
|
1566
|
+
yText.insert( 0, '𠮷😀' );
|
|
1567
|
+
|
|
1568
|
+
mergeRichTextUpdate( yText, '𠮷😀!' );
|
|
1569
|
+
|
|
1570
|
+
expect( yText.toString() ).toBe( '𠮷😀!' );
|
|
1571
|
+
} );
|
|
1572
|
+
|
|
1573
|
+
it( 'handles musical symbols (supplementary plane)', () => {
|
|
1574
|
+
// 𝄞 (U+1D11E, Musical Symbol G Clef) — .length === 2
|
|
1575
|
+
const yText = doc.getText( 'test' );
|
|
1576
|
+
yText.insert( 0, 'a𝄞b' );
|
|
1577
|
+
|
|
1578
|
+
mergeRichTextUpdate( yText, 'a𝄞xb', 4 );
|
|
1579
|
+
|
|
1580
|
+
expect( yText.toString() ).toBe( 'a𝄞xb' );
|
|
1581
|
+
} );
|
|
1582
|
+
} );
|
|
1583
|
+
} );
|
|
1584
|
+
} );
|
|
1585
|
+
|
|
1586
|
+
describe( 'getCachedRichTextData', () => {
|
|
1587
|
+
let spy: ReturnType< typeof jest.spyOn >;
|
|
1588
|
+
|
|
1589
|
+
beforeEach( () => {
|
|
1590
|
+
spy = jest.spyOn( RichTextData, 'fromHTMLString' );
|
|
1591
|
+
} );
|
|
1592
|
+
|
|
1593
|
+
afterEach( () => {
|
|
1594
|
+
spy.mockRestore();
|
|
1595
|
+
} );
|
|
1596
|
+
|
|
1597
|
+
it( 'does not call fromHTMLString again for the same HTML string', () => {
|
|
1598
|
+
getCachedRichTextData( '<strong>cached-hit</strong>' );
|
|
1599
|
+
getCachedRichTextData( '<strong>cached-hit</strong>' );
|
|
1600
|
+
|
|
1601
|
+
expect( spy ).toHaveBeenCalledTimes( 1 );
|
|
1602
|
+
} );
|
|
1603
|
+
|
|
1604
|
+
it( 'calls fromHTMLString for each unique HTML string', () => {
|
|
1605
|
+
getCachedRichTextData( '<strong>cached-miss-a</strong>' );
|
|
1606
|
+
getCachedRichTextData( '<em>cached-miss-b</em>' );
|
|
1607
|
+
|
|
1608
|
+
expect( spy ).toHaveBeenCalledTimes( 2 );
|
|
1609
|
+
} );
|
|
1610
|
+
|
|
1611
|
+
it( 'calls fromHTMLString again for an evicted entry', () => {
|
|
1612
|
+
const cacheSize = 10;
|
|
1613
|
+
const getCachedValue = createRichTextDataCache( cacheSize );
|
|
1614
|
+
|
|
1615
|
+
const firstString = 'eviction-test-first';
|
|
1616
|
+
|
|
1617
|
+
getCachedValue( firstString );
|
|
1618
|
+
|
|
1619
|
+
for ( let i = 1; i < cacheSize; i++ ) {
|
|
1620
|
+
getCachedValue( `eviction-test-${ i }` );
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// This should push firstString out of the cache.
|
|
1624
|
+
getCachedValue( 'eviction-test-overflow' );
|
|
1625
|
+
|
|
1626
|
+
spy.mockClear();
|
|
1627
|
+
|
|
1628
|
+
// firstString was evicted, so fromHTMLString should be called again.
|
|
1629
|
+
getCachedValue( firstString );
|
|
1630
|
+
expect( spy ).toHaveBeenCalledTimes( 1 );
|
|
1631
|
+
|
|
1632
|
+
// The overflow entry is still cached, so fromHTMLString should not be called.
|
|
1633
|
+
getCachedValue( 'eviction-test-overflow' );
|
|
1634
|
+
expect( spy ).toHaveBeenCalledTimes( 1 );
|
|
1635
|
+
} );
|
|
1072
1636
|
} );
|
|
@@ -16,6 +16,11 @@ import { CRDT_RECORD_MAP_KEY } from '../../sync';
|
|
|
16
16
|
|
|
17
17
|
jest.mock( '@wordpress/data', () => ( {
|
|
18
18
|
select: jest.fn(),
|
|
19
|
+
// Needed because @wordpress/rich-text initialises its store at import time.
|
|
20
|
+
combineReducers: jest.fn( () => jest.fn( () => ( {} ) ) ),
|
|
21
|
+
createReduxStore: jest.fn( () => ( {} ) ),
|
|
22
|
+
register: jest.fn(),
|
|
23
|
+
createSelector: ( selector: Function ) => selector,
|
|
19
24
|
} ) );
|
|
20
25
|
|
|
21
26
|
jest.mock( '@wordpress/block-editor', () => ( {
|