@wordpress/core-data 7.40.2-next.v.202602271551.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/actions.cjs +1 -1
  3. package/build/actions.cjs.map +2 -2
  4. package/build/awareness/types.cjs.map +1 -1
  5. package/build/entities.cjs +2 -2
  6. package/build/entities.cjs.map +2 -2
  7. package/build/hooks/use-post-editor-awareness-state.cjs +38 -0
  8. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  9. package/build/private-actions.cjs +7 -2
  10. package/build/private-actions.cjs.map +2 -2
  11. package/build/private-apis.cjs +4 -1
  12. package/build/private-apis.cjs.map +2 -2
  13. package/build/private-selectors.cjs +7 -2
  14. package/build/private-selectors.cjs.map +2 -2
  15. package/build/reducer.cjs +11 -1
  16. package/build/reducer.cjs.map +2 -2
  17. package/build/selectors.cjs.map +2 -2
  18. package/build/sync.cjs +6 -3
  19. package/build/sync.cjs.map +2 -2
  20. package/build-module/actions.mjs +1 -1
  21. package/build-module/actions.mjs.map +2 -2
  22. package/build-module/entities.mjs +2 -2
  23. package/build-module/entities.mjs.map +2 -2
  24. package/build-module/hooks/use-post-editor-awareness-state.mjs +37 -0
  25. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  26. package/build-module/private-actions.mjs +5 -1
  27. package/build-module/private-actions.mjs.map +2 -2
  28. package/build-module/private-apis.mjs +6 -2
  29. package/build-module/private-apis.mjs.map +2 -2
  30. package/build-module/private-selectors.mjs +5 -1
  31. package/build-module/private-selectors.mjs.map +2 -2
  32. package/build-module/reducer.mjs +10 -1
  33. package/build-module/reducer.mjs.map +2 -2
  34. package/build-module/selectors.mjs.map +2 -2
  35. package/build-module/sync.mjs +4 -2
  36. package/build-module/sync.mjs.map +2 -2
  37. package/build-types/awareness/types.d.ts +5 -0
  38. package/build-types/awareness/types.d.ts.map +1 -1
  39. package/build-types/entities.d.ts +1 -1
  40. package/build-types/entities.d.ts.map +1 -1
  41. package/build-types/hooks/use-post-editor-awareness-state.d.ts +10 -1
  42. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  43. package/build-types/index.d.ts.map +1 -1
  44. package/build-types/private-actions.d.ts +1 -0
  45. package/build-types/private-actions.d.ts.map +1 -1
  46. package/build-types/private-apis.d.ts.map +1 -1
  47. package/build-types/private-selectors.d.ts +7 -0
  48. package/build-types/private-selectors.d.ts.map +1 -1
  49. package/build-types/reducer.d.ts +15 -0
  50. package/build-types/reducer.d.ts.map +1 -1
  51. package/build-types/selectors.d.ts +1 -0
  52. package/build-types/selectors.d.ts.map +1 -1
  53. package/build-types/sync.d.ts +2 -2
  54. package/build-types/sync.d.ts.map +1 -1
  55. package/package.json +18 -18
  56. package/src/actions.js +2 -2
  57. package/src/awareness/types.ts +6 -0
  58. package/src/entities.js +3 -3
  59. package/src/hooks/use-post-editor-awareness-state.ts +70 -0
  60. package/src/private-actions.js +13 -0
  61. package/src/private-apis.js +4 -0
  62. package/src/private-selectors.ts +10 -0
  63. package/src/reducer.js +21 -0
  64. package/src/selectors.ts +1 -0
  65. package/src/sync.ts +2 -0
  66. package/src/test/entities.js +19 -15
  67. package/src/utils/test/crdt-blocks.ts +347 -0
@@ -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,
@@ -1069,4 +1070,350 @@ describe( 'crdt-blocks', () => {
1069
1070
  expect( attrs2.has( 'caption' ) ).toBe( false );
1070
1071
  } );
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
+ } );
1418
+ } );
1072
1419
  } );