@wordpress/core-data 7.45.0 → 7.45.1-next.v.202605131006.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/build/actions.cjs +8 -6
- package/build/actions.cjs.map +2 -2
- package/build/awareness/post-editor-awareness.cjs +1 -1
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/resolvers.cjs +2 -1
- package/build/resolvers.cjs.map +2 -2
- package/build/types.cjs.map +2 -2
- package/build/utils/block-selection-history.cjs +4 -1
- package/build/utils/block-selection-history.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +157 -89
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-selection.cjs +1 -1
- package/build/utils/crdt-selection.cjs.map +2 -2
- package/build/utils/crdt-user-selections.cjs +4 -1
- package/build/utils/crdt-user-selections.cjs.map +2 -2
- package/build/utils/crdt-utils.cjs +18 -6
- package/build/utils/crdt-utils.cjs.map +2 -2
- package/build/utils/crdt.cjs +12 -2
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +8 -6
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/post-editor-awareness.mjs +5 -2
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/resolvers.mjs +2 -1
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/types.mjs.map +2 -2
- package/build-module/utils/block-selection-history.mjs +5 -1
- package/build-module/utils/block-selection-history.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +162 -90
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-selection.mjs +2 -1
- package/build-module/utils/crdt-selection.mjs.map +2 -2
- package/build-module/utils/crdt-user-selections.mjs +9 -2
- package/build-module/utils/crdt-user-selections.mjs.map +2 -2
- package/build-module/utils/crdt-utils.mjs +16 -6
- package/build-module/utils/crdt-utils.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +13 -2
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts +177 -64
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/awareness-state.d.ts.map +1 -1
- package/build-types/awareness/base-awareness.d.ts +0 -3
- package/build-types/awareness/base-awareness.d.ts.map +1 -1
- package/build-types/awareness/post-editor-awareness.d.ts +1 -8
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/awareness/typed-awareness.d.ts.map +1 -1
- package/build-types/batch/create-batch.d.ts +1 -1
- package/build-types/batch/create-batch.d.ts.map +1 -1
- package/build-types/batch/default-processor.d.ts.map +1 -1
- package/build-types/batch/index.d.ts +2 -2
- package/build-types/batch/index.d.ts.map +1 -1
- package/build-types/entities.d.ts +114 -87
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/entity-context.d.ts +1 -1
- package/build-types/entity-context.d.ts.map +1 -1
- package/build-types/entity-provider.d.ts +2 -2
- package/build-types/entity-provider.d.ts.map +1 -1
- package/build-types/entity-types/attachment.d.ts.map +1 -1
- package/build-types/entity-types/base-entity-records.d.ts.map +1 -1
- package/build-types/entity-types/base.d.ts.map +1 -1
- package/build-types/entity-types/comment.d.ts.map +1 -1
- package/build-types/entity-types/font-collection.d.ts.map +1 -1
- package/build-types/entity-types/font-family.d.ts.map +1 -1
- package/build-types/entity-types/global-styles-revision.d.ts.map +1 -1
- package/build-types/entity-types/icon.d.ts.map +1 -1
- package/build-types/entity-types/menu-location.d.ts.map +1 -1
- package/build-types/entity-types/nav-menu-item.d.ts.map +1 -1
- package/build-types/entity-types/nav-menu.d.ts.map +1 -1
- package/build-types/entity-types/page.d.ts.map +1 -1
- package/build-types/entity-types/plugin.d.ts.map +1 -1
- package/build-types/entity-types/post-revision.d.ts.map +1 -1
- package/build-types/entity-types/post-status.d.ts.map +1 -1
- package/build-types/entity-types/post.d.ts.map +1 -1
- package/build-types/entity-types/settings.d.ts.map +1 -1
- package/build-types/entity-types/sidebar.d.ts.map +1 -1
- package/build-types/entity-types/taxonomy.d.ts.map +1 -1
- package/build-types/entity-types/term.d.ts.map +1 -1
- package/build-types/entity-types/theme.d.ts.map +1 -1
- package/build-types/entity-types/type.d.ts.map +1 -1
- package/build-types/entity-types/user.d.ts.map +1 -1
- package/build-types/entity-types/widget-type.d.ts.map +1 -1
- package/build-types/entity-types/widget.d.ts.map +1 -1
- package/build-types/entity-types/wp-template-part.d.ts.map +1 -1
- package/build-types/entity-types/wp-template.d.ts.map +1 -1
- package/build-types/fetch/__experimental-fetch-url-data.d.ts +2 -5
- package/build-types/fetch/__experimental-fetch-url-data.d.ts.map +1 -1
- package/build-types/fetch/index.d.ts +3 -3
- package/build-types/fetch/index.d.ts.map +1 -1
- package/build-types/footnotes/get-footnotes-order.d.ts.map +1 -1
- package/build-types/footnotes/get-rich-text-values-cached.d.ts.map +1 -1
- package/build-types/footnotes/index.d.ts +1 -1
- package/build-types/footnotes/index.d.ts.map +1 -1
- package/build-types/hooks/use-entity-block-editor.d.ts +1 -1
- package/build-types/hooks/use-entity-block-editor.d.ts.map +1 -1
- package/build-types/hooks/use-entity-id.d.ts.map +1 -1
- package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
- package/build-types/hooks/use-resource-permissions.d.ts.map +1 -1
- package/build-types/index.d.ts +155 -153
- package/build-types/index.d.ts.map +1 -1
- package/build-types/locks/actions.d.ts +1 -1
- package/build-types/locks/actions.d.ts.map +1 -1
- package/build-types/locks/engine.d.ts +1 -1
- package/build-types/locks/engine.d.ts.map +1 -1
- package/build-types/locks/reducer.d.ts.map +1 -1
- package/build-types/locks/selectors.d.ts +2 -2
- package/build-types/locks/selectors.d.ts.map +1 -1
- package/build-types/locks/utils.d.ts +5 -5
- package/build-types/locks/utils.d.ts.map +1 -1
- package/build-types/name.d.ts +1 -1
- package/build-types/name.d.ts.map +1 -1
- package/build-types/private-actions.d.ts +45 -29
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-apis.d.ts +1 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/queried-data/actions.d.ts +3 -3
- package/build-types/queried-data/actions.d.ts.map +1 -1
- package/build-types/queried-data/get-query-parts.d.ts +10 -34
- package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
- package/build-types/queried-data/index.d.ts +3 -3
- package/build-types/queried-data/index.d.ts.map +1 -1
- package/build-types/queried-data/reducer.d.ts +7 -23
- package/build-types/queried-data/reducer.d.ts.map +1 -1
- package/build-types/queried-data/selectors.d.ts +3 -3
- package/build-types/queried-data/selectors.d.ts.map +1 -1
- package/build-types/reducer.d.ts +40 -32
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts +130 -47
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +1 -1
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/types.d.ts +61 -6
- 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/conservative-map-item.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +19 -9
- 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-user-selections.d.ts.map +1 -1
- package/build-types/utils/crdt-utils.d.ts +35 -2
- package/build-types/utils/crdt-utils.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/forward-resolver.d.ts +2 -2
- package/build-types/utils/forward-resolver.d.ts.map +1 -1
- package/build-types/utils/get-nested-value.d.ts.map +1 -1
- package/build-types/utils/get-normalized-comma-separable.d.ts +1 -1
- package/build-types/utils/get-normalized-comma-separable.d.ts.map +1 -1
- package/build-types/utils/if-matching-action.d.ts +3 -3
- package/build-types/utils/if-matching-action.d.ts.map +1 -1
- package/build-types/utils/index.d.ts +12 -12
- package/build-types/utils/index.d.ts.map +1 -1
- package/build-types/utils/is-numeric-id.d.ts.map +1 -1
- package/build-types/utils/log-entity-deprecation.d.ts +1 -1
- package/build-types/utils/log-entity-deprecation.d.ts.map +1 -1
- package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -1
- package/build-types/utils/receive-intermediate-results.d.ts +1 -1
- package/build-types/utils/receive-intermediate-results.d.ts.map +1 -1
- package/build-types/utils/replace-action.d.ts +3 -3
- package/build-types/utils/replace-action.d.ts.map +1 -1
- package/build-types/utils/set-nested-value.d.ts.map +1 -1
- package/build-types/utils/user-permissions.d.ts +3 -3
- package/build-types/utils/user-permissions.d.ts.map +1 -1
- package/build-types/utils/with-weak-map-cache.d.ts +1 -1
- package/build-types/utils/with-weak-map-cache.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/actions.js +7 -9
- package/src/awareness/post-editor-awareness.ts +5 -2
- package/src/resolvers.js +2 -1
- package/src/test/actions.js +58 -0
- package/src/test/resolvers.js +115 -2
- package/src/test/rtc-rich-text-offset-space.test.js +204 -0
- package/src/types.ts +63 -6
- package/src/utils/block-selection-history.ts +5 -1
- package/src/utils/crdt-blocks.ts +316 -116
- package/src/utils/crdt-selection.ts +2 -1
- package/src/utils/crdt-user-selections.ts +9 -2
- package/src/utils/crdt-utils.ts +53 -10
- package/src/utils/crdt.ts +30 -4
- package/src/utils/test/crdt-blocks.ts +74 -18
- package/src/utils/test/crdt-utils.ts +18 -2
- package/src/utils/test/rtc-rich-text-cursor-scope.test.js +267 -0
- package/src/utils/test/rtc-rich-text-offset-space.test.js +469 -0
package/src/utils/crdt-blocks.ts
CHANGED
|
@@ -14,9 +14,17 @@ import { Y } from '@wordpress/sync';
|
|
|
14
14
|
/**
|
|
15
15
|
* Internal dependencies
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
asRichTextOffset,
|
|
19
|
+
createYMap,
|
|
20
|
+
richTextOffsetToHtmlIndex,
|
|
21
|
+
type HtmlStringIndex,
|
|
22
|
+
type YMapRecord,
|
|
23
|
+
type YMapWrap,
|
|
24
|
+
} from './crdt-utils';
|
|
18
25
|
import { getCachedRichTextData } from './crdt-text';
|
|
19
26
|
import { Delta } from '../sync';
|
|
27
|
+
import { type WPBlockSelection } from '../types';
|
|
20
28
|
|
|
21
29
|
interface BlockAttributes {
|
|
22
30
|
[ key: string ]: unknown;
|
|
@@ -61,6 +69,14 @@ export type YBlocks = Y.Array< YBlock >;
|
|
|
61
69
|
// Attribute values will be typed as the union of `Y.Text` and `unknown`.
|
|
62
70
|
export type YBlockAttributes = Y.Map< Y.Text | unknown >;
|
|
63
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Optional description of where a cursor falls.
|
|
74
|
+
*
|
|
75
|
+
* Used to coordinate shifting of cursor when applying changes
|
|
76
|
+
* to a Y.Doc with RichText instances.
|
|
77
|
+
*/
|
|
78
|
+
export type MergeCursorPosition = WPBlockSelection | null;
|
|
79
|
+
|
|
64
80
|
const serializableBlocksCache = new WeakMap< WeakKey, Block[] >();
|
|
65
81
|
|
|
66
82
|
/**
|
|
@@ -110,10 +126,28 @@ function makeBlockAttributesSerializable(
|
|
|
110
126
|
return newAttributes;
|
|
111
127
|
}
|
|
112
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Recursively removes properties which cannot be serialized from a list of block objects.
|
|
131
|
+
*
|
|
132
|
+
* @param blocks Eemove unserializable properties from each block object in this set.
|
|
133
|
+
* @return Copies of the provided blocks without the unserializable properties.
|
|
134
|
+
*/
|
|
113
135
|
function makeBlocksSerializable( blocks: Block[] ): Block[] {
|
|
114
136
|
return blocks.map( ( block: Block ) => {
|
|
115
|
-
const {
|
|
116
|
-
|
|
137
|
+
const {
|
|
138
|
+
name,
|
|
139
|
+
innerBlocks,
|
|
140
|
+
attributes,
|
|
141
|
+
/*
|
|
142
|
+
* Any validation issues discovered when loading a block are appended
|
|
143
|
+
* to the block node with a logging function, which cannot be serialized.
|
|
144
|
+
*
|
|
145
|
+
* @see import("@wordpress/blocks/src/api/parser").parseRawBlock()
|
|
146
|
+
*/
|
|
147
|
+
validationIssues,
|
|
148
|
+
...rest
|
|
149
|
+
} = block;
|
|
150
|
+
|
|
117
151
|
return {
|
|
118
152
|
...rest,
|
|
119
153
|
name,
|
|
@@ -381,14 +415,16 @@ function createNewYBlock( block: Block ): YBlock {
|
|
|
381
415
|
* Merge incoming block data into the local Y.Doc.
|
|
382
416
|
* This function is called to sync local block changes to a shared Y.Doc.
|
|
383
417
|
*
|
|
384
|
-
* @param yblocks
|
|
385
|
-
* @param incomingBlocks
|
|
386
|
-
* @param
|
|
418
|
+
* @param yblocks The blocks in the local Y.Doc.
|
|
419
|
+
* @param incomingBlocks Gutenberg blocks being synced.
|
|
420
|
+
* @param attributeCursor When provided, describes a selection cursor falling within a
|
|
421
|
+
* RichText field associated with a specific block and attribute.
|
|
422
|
+
* Derived from the changes that produced the blocks.
|
|
387
423
|
*/
|
|
388
424
|
export function mergeCrdtBlocks(
|
|
389
425
|
yblocks: YBlocks,
|
|
390
426
|
incomingBlocks: Block[],
|
|
391
|
-
|
|
427
|
+
attributeCursor: MergeCursorPosition
|
|
392
428
|
): void {
|
|
393
429
|
// Ensure we are working with serializable block data.
|
|
394
430
|
if ( ! serializableBlocksCache.has( incomingBlocks ) ) {
|
|
@@ -397,7 +433,9 @@ export function mergeCrdtBlocks(
|
|
|
397
433
|
makeBlocksSerializable( incomingBlocks )
|
|
398
434
|
);
|
|
399
435
|
}
|
|
400
|
-
|
|
436
|
+
|
|
437
|
+
const incomingBlocksToSync =
|
|
438
|
+
serializableBlocksCache.get( incomingBlocks ) ?? [];
|
|
401
439
|
|
|
402
440
|
// This is a rudimentary diff implementation similar to the y-prosemirror diffing
|
|
403
441
|
// approach.
|
|
@@ -413,7 +451,7 @@ export function mergeCrdtBlocks(
|
|
|
413
451
|
// @credit Kevin Jahns (dmonad)
|
|
414
452
|
// @link https://github.com/WordPress/gutenberg/pull/68483
|
|
415
453
|
const numOfCommonEntries = Math.min(
|
|
416
|
-
|
|
454
|
+
incomingBlocksToSync.length ?? 0,
|
|
417
455
|
yblocks.length
|
|
418
456
|
);
|
|
419
457
|
|
|
@@ -424,7 +462,7 @@ export function mergeCrdtBlocks(
|
|
|
424
462
|
for (
|
|
425
463
|
;
|
|
426
464
|
left < numOfCommonEntries &&
|
|
427
|
-
areBlocksEqual(
|
|
465
|
+
areBlocksEqual( incomingBlocksToSync[ left ], yblocks.get( left ) );
|
|
428
466
|
left++
|
|
429
467
|
) {
|
|
430
468
|
/* nop */
|
|
@@ -435,7 +473,7 @@ export function mergeCrdtBlocks(
|
|
|
435
473
|
;
|
|
436
474
|
right < numOfCommonEntries - left &&
|
|
437
475
|
areBlocksEqual(
|
|
438
|
-
|
|
476
|
+
incomingBlocksToSync[ incomingBlocksToSync.length - right - 1 ],
|
|
439
477
|
yblocks.get( yblocks.length - right - 1 )
|
|
440
478
|
);
|
|
441
479
|
right++
|
|
@@ -446,108 +484,139 @@ export function mergeCrdtBlocks(
|
|
|
446
484
|
const numOfUpdatesNeeded = numOfCommonEntries - left - right;
|
|
447
485
|
const numOfInsertionsNeeded = Math.max(
|
|
448
486
|
0,
|
|
449
|
-
|
|
487
|
+
incomingBlocksToSync.length - yblocks.length
|
|
450
488
|
);
|
|
451
489
|
const numOfDeletionsNeeded = Math.max(
|
|
452
490
|
0,
|
|
453
|
-
yblocks.length -
|
|
491
|
+
yblocks.length - incomingBlocksToSync.length
|
|
454
492
|
);
|
|
455
493
|
|
|
456
494
|
// updates
|
|
457
495
|
for ( let i = 0; i < numOfUpdatesNeeded; i++, left++ ) {
|
|
458
|
-
const
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
Object.entries( block ).forEach( ( [ key, value ] ) => {
|
|
462
|
-
switch ( key ) {
|
|
463
|
-
case 'attributes': {
|
|
464
|
-
const currentAttributes = yblock.get( key );
|
|
496
|
+
const incomingYBlock = incomingBlocksToSync[ left ];
|
|
497
|
+
const localYBlock = yblocks.get( left );
|
|
465
498
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
499
|
+
Object.entries( incomingYBlock ).forEach(
|
|
500
|
+
( [ incomingBlockProperty, incomingBlockPropertyValue ] ) => {
|
|
501
|
+
switch ( incomingBlockProperty ) {
|
|
502
|
+
case 'attributes': {
|
|
503
|
+
const localAttributes = localYBlock.get(
|
|
504
|
+
incomingBlockProperty
|
|
471
505
|
);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
attributeName,
|
|
483
|
-
currentAttribute
|
|
506
|
+
const incomingAttributes = incomingBlockPropertyValue;
|
|
507
|
+
|
|
508
|
+
// When the local block has no attributes, adopt the incoming set.
|
|
509
|
+
if ( ! localAttributes ) {
|
|
510
|
+
localYBlock.set(
|
|
511
|
+
incomingBlockProperty,
|
|
512
|
+
createNewYAttributeMap(
|
|
513
|
+
incomingYBlock.name,
|
|
514
|
+
incomingAttributes
|
|
515
|
+
)
|
|
484
516
|
);
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
485
519
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
! isExpectedType ||
|
|
495
|
-
isYType ||
|
|
496
|
-
! fastDeepEqual(
|
|
497
|
-
currentAttribute,
|
|
498
|
-
attributeValue
|
|
520
|
+
// Otherwise the attributes need to be merged.
|
|
521
|
+
Object.entries( incomingAttributes ).forEach(
|
|
522
|
+
( [
|
|
523
|
+
incomingAttributeName,
|
|
524
|
+
incomingAttributeValue,
|
|
525
|
+
] ) => {
|
|
526
|
+
const currentAttribute = localAttributes?.get(
|
|
527
|
+
incomingAttributeName
|
|
499
528
|
);
|
|
500
529
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
attributeValue,
|
|
506
|
-
currentAttributes,
|
|
507
|
-
cursorPosition
|
|
530
|
+
const isExpectedType = isExpectedAttributeType(
|
|
531
|
+
incomingYBlock.name,
|
|
532
|
+
incomingAttributeName,
|
|
533
|
+
currentAttribute
|
|
508
534
|
);
|
|
535
|
+
|
|
536
|
+
// Y types (Y.Text, Y.Array, Y.Map) cannot be
|
|
537
|
+
// compared with fastDeepEqual against plain values.
|
|
538
|
+
// Delegate to mergeYValue which handles no-op
|
|
539
|
+
// detection at the edges.
|
|
540
|
+
const isYType =
|
|
541
|
+
currentAttribute instanceof Y.AbstractType;
|
|
542
|
+
|
|
543
|
+
const isAttributeChanged =
|
|
544
|
+
! isExpectedType ||
|
|
545
|
+
isYType ||
|
|
546
|
+
! fastDeepEqual(
|
|
547
|
+
currentAttribute,
|
|
548
|
+
incomingAttributeValue
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
if ( isAttributeChanged ) {
|
|
552
|
+
updateYBlockAttribute(
|
|
553
|
+
incomingYBlock.name,
|
|
554
|
+
incomingYBlock.clientId,
|
|
555
|
+
incomingAttributeName,
|
|
556
|
+
incomingAttributeValue,
|
|
557
|
+
localAttributes,
|
|
558
|
+
attributeCursor
|
|
559
|
+
);
|
|
560
|
+
}
|
|
509
561
|
}
|
|
510
|
-
|
|
511
|
-
);
|
|
562
|
+
);
|
|
512
563
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
564
|
+
// Delete any attributes that are no longer present.
|
|
565
|
+
localAttributes.forEach(
|
|
566
|
+
( _attrValue: unknown, attrName: string ) => {
|
|
567
|
+
if (
|
|
568
|
+
! incomingBlockPropertyValue.hasOwnProperty(
|
|
569
|
+
attrName
|
|
570
|
+
)
|
|
571
|
+
) {
|
|
572
|
+
localAttributes.delete( attrName );
|
|
573
|
+
}
|
|
518
574
|
}
|
|
519
|
-
|
|
520
|
-
);
|
|
575
|
+
);
|
|
521
576
|
|
|
522
|
-
|
|
523
|
-
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
524
579
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
580
|
+
case 'innerBlocks': {
|
|
581
|
+
// Recursively merge innerBlocks
|
|
582
|
+
let yInnerBlocks = localYBlock.get(
|
|
583
|
+
incomingBlockProperty
|
|
584
|
+
);
|
|
528
585
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
586
|
+
if ( ! ( yInnerBlocks instanceof Y.Array ) ) {
|
|
587
|
+
yInnerBlocks = new Y.Array< YBlock >();
|
|
588
|
+
localYBlock.set(
|
|
589
|
+
incomingBlockProperty,
|
|
590
|
+
yInnerBlocks
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
mergeCrdtBlocks(
|
|
595
|
+
yInnerBlocks,
|
|
596
|
+
incomingBlockPropertyValue ?? [],
|
|
597
|
+
attributeCursor
|
|
598
|
+
);
|
|
599
|
+
break;
|
|
532
600
|
}
|
|
533
601
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
602
|
+
default:
|
|
603
|
+
if (
|
|
604
|
+
! fastDeepEqual(
|
|
605
|
+
incomingYBlock[ incomingBlockProperty ],
|
|
606
|
+
localYBlock.get( incomingBlockProperty )
|
|
607
|
+
)
|
|
608
|
+
) {
|
|
609
|
+
localYBlock.set(
|
|
610
|
+
incomingBlockProperty,
|
|
611
|
+
incomingBlockPropertyValue
|
|
612
|
+
);
|
|
613
|
+
}
|
|
540
614
|
}
|
|
541
|
-
|
|
542
|
-
default:
|
|
543
|
-
if ( ! fastDeepEqual( block[ key ], yblock.get( key ) ) ) {
|
|
544
|
-
yblock.set( key, value );
|
|
545
|
-
}
|
|
546
615
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if ( !
|
|
550
|
-
|
|
616
|
+
);
|
|
617
|
+
localYBlock.forEach( ( _v, k ) => {
|
|
618
|
+
if ( ! incomingYBlock.hasOwnProperty( k ) ) {
|
|
619
|
+
localYBlock.delete( k );
|
|
551
620
|
}
|
|
552
621
|
} );
|
|
553
622
|
}
|
|
@@ -557,7 +626,7 @@ export function mergeCrdtBlocks(
|
|
|
557
626
|
|
|
558
627
|
// inserts
|
|
559
628
|
for ( let i = 0; i < numOfInsertionsNeeded; i++, left++ ) {
|
|
560
|
-
const newBlock = [ createNewYBlock(
|
|
629
|
+
const newBlock = [ createNewYBlock( incomingBlocksToSync[ left ] ) ];
|
|
561
630
|
|
|
562
631
|
yblocks.insert( left, newBlock );
|
|
563
632
|
}
|
|
@@ -613,12 +682,14 @@ function areArrayElementsEqual(
|
|
|
613
682
|
* @param newValue The new plain array to merge into the Y.Array.
|
|
614
683
|
* @param schema The attribute schema (must have `query`).
|
|
615
684
|
* @param cursorPosition The local cursor position for rich-text delta merges.
|
|
685
|
+
* @param cursorScope The selected block attribute scope for rich-text cursor hints.
|
|
616
686
|
*/
|
|
617
687
|
function mergeYArray(
|
|
618
688
|
yArray: Y.Array< unknown >,
|
|
619
689
|
newValue: unknown[],
|
|
620
690
|
schema: BlockAttributeSchema,
|
|
621
|
-
cursorPosition:
|
|
691
|
+
cursorPosition: MergeCursorPosition,
|
|
692
|
+
cursorScope: RichTextCursorScope
|
|
622
693
|
): void {
|
|
623
694
|
if ( ! schema.query ) {
|
|
624
695
|
return;
|
|
@@ -665,7 +736,8 @@ function mergeYArray(
|
|
|
665
736
|
currentElement,
|
|
666
737
|
newElement,
|
|
667
738
|
query,
|
|
668
|
-
cursorPosition
|
|
739
|
+
cursorPosition,
|
|
740
|
+
cursorScope
|
|
669
741
|
);
|
|
670
742
|
} else {
|
|
671
743
|
// Element is the wrong type (e.g. partial migration) or the
|
|
@@ -721,14 +793,17 @@ function mergeYArray(
|
|
|
721
793
|
* @param newVal The new value to merge into the Y.Map entry.
|
|
722
794
|
* @param yMap The Y.Map that owns this entry.
|
|
723
795
|
* @param key The key of this entry in the Y.Map.
|
|
724
|
-
* @param cursorPosition The
|
|
796
|
+
* @param cursorPosition The cursor position for rich-text delta merges from the updated value.
|
|
797
|
+
* @param cursorScope Indicates a specific block and attribute associated with the editor;
|
|
798
|
+
* determines whether the cursor should be updated based on the change.
|
|
725
799
|
*/
|
|
726
800
|
function mergeYValue(
|
|
727
801
|
schema: BlockAttributeSchema | undefined,
|
|
728
802
|
newVal: unknown,
|
|
729
803
|
yMap: Y.Map< unknown >,
|
|
730
804
|
key: string,
|
|
731
|
-
cursorPosition:
|
|
805
|
+
cursorPosition: MergeCursorPosition,
|
|
806
|
+
cursorScope: RichTextCursorScope
|
|
732
807
|
): void {
|
|
733
808
|
const currentVal = yMap.get( key );
|
|
734
809
|
if (
|
|
@@ -736,21 +811,31 @@ function mergeYValue(
|
|
|
736
811
|
typeof newVal === 'string' &&
|
|
737
812
|
currentVal instanceof Y.Text
|
|
738
813
|
) {
|
|
739
|
-
mergeRichTextUpdate(
|
|
814
|
+
mergeRichTextUpdate(
|
|
815
|
+
currentVal,
|
|
816
|
+
newVal,
|
|
817
|
+
resolveRichTextCursorPosition( cursorPosition, cursorScope, newVal )
|
|
818
|
+
);
|
|
740
819
|
} else if (
|
|
741
820
|
schema?.type === 'array' &&
|
|
742
821
|
schema.query &&
|
|
743
822
|
Array.isArray( newVal ) &&
|
|
744
823
|
currentVal instanceof Y.Array
|
|
745
824
|
) {
|
|
746
|
-
mergeYArray( currentVal, newVal, schema, cursorPosition );
|
|
825
|
+
mergeYArray( currentVal, newVal, schema, cursorPosition, cursorScope );
|
|
747
826
|
} else if (
|
|
748
827
|
schema?.type === 'object' &&
|
|
749
828
|
schema.query &&
|
|
750
829
|
isRecord( newVal ) &&
|
|
751
830
|
currentVal instanceof Y.Map
|
|
752
831
|
) {
|
|
753
|
-
mergeYMapValues(
|
|
832
|
+
mergeYMapValues(
|
|
833
|
+
currentVal,
|
|
834
|
+
newVal,
|
|
835
|
+
schema.query,
|
|
836
|
+
cursorPosition,
|
|
837
|
+
cursorScope
|
|
838
|
+
);
|
|
754
839
|
} else {
|
|
755
840
|
const newYValue = createYValueFromSchema( schema, newVal );
|
|
756
841
|
|
|
@@ -773,15 +858,24 @@ function mergeYValue(
|
|
|
773
858
|
* @param newObj The new plain object to merge into the Y.Map.
|
|
774
859
|
* @param query The query schema defining property types.
|
|
775
860
|
* @param cursorPosition The local cursor position for rich-text delta merges.
|
|
861
|
+
* @param cursorScope The selected block attribute scope for rich-text cursor hints.
|
|
776
862
|
*/
|
|
777
863
|
function mergeYMapValues(
|
|
778
864
|
yMap: Y.Map< unknown >,
|
|
779
865
|
newObj: Record< string, unknown >,
|
|
780
866
|
query: Record< string, BlockAttributeSchema >,
|
|
781
|
-
cursorPosition:
|
|
867
|
+
cursorPosition: MergeCursorPosition,
|
|
868
|
+
cursorScope: RichTextCursorScope
|
|
782
869
|
): void {
|
|
783
870
|
for ( const [ key, newVal ] of Object.entries( newObj ) ) {
|
|
784
|
-
mergeYValue(
|
|
871
|
+
mergeYValue(
|
|
872
|
+
query[ key ],
|
|
873
|
+
newVal,
|
|
874
|
+
yMap,
|
|
875
|
+
key,
|
|
876
|
+
cursorPosition,
|
|
877
|
+
cursorScope
|
|
878
|
+
);
|
|
785
879
|
}
|
|
786
880
|
|
|
787
881
|
// Delete properties absent from the incoming object.
|
|
@@ -796,29 +890,94 @@ function mergeYMapValues(
|
|
|
796
890
|
* Update a single attribute on a Yjs block attributes map (currentAttributes).
|
|
797
891
|
*
|
|
798
892
|
* @param blockName The block type name, e.g. 'core/paragraph'.
|
|
893
|
+
* @param clientId The local clientId for the block being merged.
|
|
799
894
|
* @param attributeName The name of the attribute to update, e.g. 'content'.
|
|
800
895
|
* @param attributeValue The new value for the attribute.
|
|
801
896
|
* @param currentAttributes The Y.Map holding the block's current attributes.
|
|
802
|
-
* @param
|
|
897
|
+
* @param newCursorPosition The cursor position for rich-text delta merges from the updated value.
|
|
898
|
+
* Notably, this may not correspond to the attribute being edited and is
|
|
899
|
+
* used to determine if any cursors need shifting in response to the change.
|
|
803
900
|
*/
|
|
804
901
|
function updateYBlockAttribute(
|
|
805
902
|
blockName: string,
|
|
903
|
+
clientId: string | undefined,
|
|
806
904
|
attributeName: string,
|
|
807
905
|
attributeValue: unknown,
|
|
808
906
|
currentAttributes: YBlockAttributes,
|
|
809
|
-
|
|
907
|
+
newCursorPosition: MergeCursorPosition
|
|
810
908
|
): void {
|
|
811
909
|
const schema = getBlockAttributeSchema( blockName, attributeName );
|
|
812
910
|
|
|
911
|
+
/*
|
|
912
|
+
* @todo There is a slight discrepancy between the attribute name and key, which might
|
|
913
|
+
* show up when working with multiline RichText instances (of which there are no
|
|
914
|
+
* more within Core). For those instances, a cursor might never be updated in
|
|
915
|
+
* response to changes because its `attributeKey` won’t match any of the block’s
|
|
916
|
+
* attribute names, and since updating this attribute is based on the block names,
|
|
917
|
+
* no suitable key for the cursor scope will be created. To fix, the updating code
|
|
918
|
+
* would need to parse multiline attributes and infer the `attributeKey` being updated.
|
|
919
|
+
*/
|
|
813
920
|
mergeYValue(
|
|
814
921
|
schema,
|
|
815
922
|
attributeValue,
|
|
816
923
|
currentAttributes,
|
|
817
924
|
attributeName,
|
|
818
|
-
|
|
925
|
+
newCursorPosition,
|
|
926
|
+
{ attributeKey: attributeName, clientId }
|
|
819
927
|
);
|
|
820
928
|
}
|
|
821
929
|
|
|
930
|
+
/**
|
|
931
|
+
* References the specific block and attribute associated with a RichText component.
|
|
932
|
+
*
|
|
933
|
+
* This is used to associate a cursor with the attribute it’s editing.
|
|
934
|
+
*
|
|
935
|
+
* @see WPBlockSelection
|
|
936
|
+
*/
|
|
937
|
+
interface RichTextCursorScope {
|
|
938
|
+
attributeKey: string;
|
|
939
|
+
clientId: string | undefined;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
interface DeltaWithOps {
|
|
943
|
+
ops: Parameters< Y.Text[ 'applyDelta' ] >[ 0 ];
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* When the provided cursor falls within the given block and attribute’s scope,
|
|
948
|
+
* returns an index into the RichText’s serialized HTML where the cursor falls.
|
|
949
|
+
*
|
|
950
|
+
* The cursor scope constrains resolution to ensure that indices are only reported
|
|
951
|
+
* when a cursor falls within the block and attribute being updated, since the
|
|
952
|
+
* attributes being updated may not always be the ones where a cursor presently falls.
|
|
953
|
+
*
|
|
954
|
+
* Returned index measures JS string lengths, thus is counted in UTF-16 code units
|
|
955
|
+
* and contains the syntax characters making up HTML tags, comments, character
|
|
956
|
+
* references, and other non-plaintext content.
|
|
957
|
+
*
|
|
958
|
+
* @param cursorPosition Description of the cursor in the new value.
|
|
959
|
+
* @param cursorScope Cursors should only be updated if they fall within this
|
|
960
|
+
* specific block and attribute.
|
|
961
|
+
* @param updatedValue New RichText value potentially containing cursor.
|
|
962
|
+
* @return String length into serialized HTML for RichText instance where cursor falls.
|
|
963
|
+
*/
|
|
964
|
+
function resolveRichTextCursorPosition(
|
|
965
|
+
cursorPosition: MergeCursorPosition,
|
|
966
|
+
cursorScope: RichTextCursorScope,
|
|
967
|
+
updatedValue: string
|
|
968
|
+
): HtmlStringIndex | null {
|
|
969
|
+
return cursorPosition &&
|
|
970
|
+
cursorPosition.clientId === cursorScope.clientId &&
|
|
971
|
+
cursorPosition.attributeKey === cursorScope.attributeKey &&
|
|
972
|
+
'number' === typeof cursorPosition.offset &&
|
|
973
|
+
Number.isInteger( cursorPosition.offset )
|
|
974
|
+
? richTextOffsetToHtmlIndex(
|
|
975
|
+
updatedValue,
|
|
976
|
+
asRichTextOffset( cursorPosition.offset )
|
|
977
|
+
)
|
|
978
|
+
: null;
|
|
979
|
+
}
|
|
980
|
+
|
|
822
981
|
// Cached block attribute types, populated once from getBlockTypes().
|
|
823
982
|
let cachedBlockAttributeSchemas: Map<
|
|
824
983
|
string,
|
|
@@ -912,14 +1071,14 @@ let localDoc: Y.Doc;
|
|
|
912
1071
|
* Given a Y.Text object and an updated string value, diff the new value and
|
|
913
1072
|
* apply the delta to the Y.Text.
|
|
914
1073
|
*
|
|
915
|
-
* @param blockYText
|
|
916
|
-
* @param updatedValue
|
|
917
|
-
* @param
|
|
1074
|
+
* @param blockYText The Y.Text to update.
|
|
1075
|
+
* @param updatedValue The updated value.
|
|
1076
|
+
* @param htmlCursorIndex The cursor index in the updated HTML string.
|
|
918
1077
|
*/
|
|
919
1078
|
export function mergeRichTextUpdate(
|
|
920
1079
|
blockYText: Y.Text,
|
|
921
1080
|
updatedValue: string,
|
|
922
|
-
|
|
1081
|
+
htmlCursorIndex: HtmlStringIndex | null = null
|
|
923
1082
|
): void {
|
|
924
1083
|
// Gutenberg does not use Yjs shared types natively, so we can only subscribe
|
|
925
1084
|
// to changes from store and apply them to Yjs types that we create and
|
|
@@ -930,22 +1089,63 @@ export function mergeRichTextUpdate(
|
|
|
930
1089
|
// The code below allows us to compute a delta between the current and new
|
|
931
1090
|
// value, then apply it to the Y.Text.
|
|
932
1091
|
|
|
1092
|
+
const currentValueAsDelta = new Delta( blockYText.toDelta() );
|
|
1093
|
+
const updatedValueAsDelta = new Delta( [ { insert: updatedValue } ] );
|
|
1094
|
+
const deltaDiff = currentValueAsDelta.diffWithCursor(
|
|
1095
|
+
updatedValueAsDelta,
|
|
1096
|
+
htmlCursorIndex
|
|
1097
|
+
);
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* When there is no cursor involved, or when the diff is able to shuffle properly
|
|
1101
|
+
* around the cursor then apply that already-computed diff.
|
|
1102
|
+
*
|
|
1103
|
+
* However, `diffWithCursor()` currently fails in certain cases, producing corrupted
|
|
1104
|
+
* output. In these cases, fall back to the raw diff as that will apply cleanly,
|
|
1105
|
+
* even if it provides a less meaningful diff.
|
|
1106
|
+
*
|
|
1107
|
+
* @see Delta.diffWithCursor()
|
|
1108
|
+
*/
|
|
1109
|
+
const safeDiff =
|
|
1110
|
+
htmlCursorIndex === null ||
|
|
1111
|
+
isDeltaVerificationMatch( blockYText, deltaDiff, updatedValue )
|
|
1112
|
+
? deltaDiff
|
|
1113
|
+
: currentValueAsDelta.diff( updatedValueAsDelta );
|
|
1114
|
+
|
|
1115
|
+
blockYText.applyDelta( safeDiff.ops );
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Verify that applying a delta to an existing Y.Text object produces the expected
|
|
1120
|
+
* output string.
|
|
1121
|
+
*
|
|
1122
|
+
* A stale, mis-scoped, or corrupted Delta will mutate a text value to the wrong
|
|
1123
|
+
* output string. This function applies the given Delta and indicates whether it
|
|
1124
|
+
* produces the given expected output string value.
|
|
1125
|
+
*
|
|
1126
|
+
* @param blockYText The current Y.Text before applying the candidate delta.
|
|
1127
|
+
* @param delta The candidate delta.
|
|
1128
|
+
* @param expectedValue The exact string expected after applying the delta.
|
|
1129
|
+
* @return Whether the candidate delta produces the expected value.
|
|
1130
|
+
*/
|
|
1131
|
+
function isDeltaVerificationMatch(
|
|
1132
|
+
blockYText: Y.Text,
|
|
1133
|
+
delta: DeltaWithOps,
|
|
1134
|
+
expectedValue: string
|
|
1135
|
+
): boolean {
|
|
933
1136
|
if ( ! localDoc ) {
|
|
934
1137
|
// Y.Text must be attached to a Y.Doc to be able to do operations on it.
|
|
935
1138
|
// Create a temporary Y.Text attached to a local Y.Doc for delta computation.
|
|
1139
|
+
// This is an optimization to avoid creating a new Y.Doc on every update.
|
|
936
1140
|
localDoc = new Y.Doc();
|
|
937
1141
|
}
|
|
938
1142
|
|
|
939
|
-
const
|
|
940
|
-
localYText.delete( 0, localYText.length );
|
|
941
|
-
localYText.insert( 0, updatedValue );
|
|
1143
|
+
const verificationYText = localDoc.getText( 'verification-text' );
|
|
942
1144
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
cursorPosition
|
|
948
|
-
);
|
|
1145
|
+
// Because this is global, it must be cleared before using.
|
|
1146
|
+
verificationYText.delete( 0, verificationYText.length );
|
|
1147
|
+
verificationYText.insert( 0, blockYText.toString() );
|
|
1148
|
+
verificationYText.applyDelta( delta.ops );
|
|
949
1149
|
|
|
950
|
-
|
|
1150
|
+
return verificationYText.toString() === expectedValue;
|
|
951
1151
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type YSelection,
|
|
19
19
|
} from './block-selection-history';
|
|
20
20
|
import {
|
|
21
|
+
asHtmlStringIndex,
|
|
21
22
|
findBlockByClientIdInDoc,
|
|
22
23
|
htmlIndexToRichTextOffset,
|
|
23
24
|
} from './crdt-utils';
|
|
@@ -78,7 +79,7 @@ function convertYSelectionToBlockSelection(
|
|
|
78
79
|
attributeKey,
|
|
79
80
|
offset: htmlIndexToRichTextOffset(
|
|
80
81
|
absolutePosition.type.toString(),
|
|
81
|
-
absolutePosition.index
|
|
82
|
+
asHtmlStringIndex( absolutePosition.index )
|
|
82
83
|
),
|
|
83
84
|
};
|
|
84
85
|
}
|
|
@@ -12,7 +12,11 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
|
|
|
12
12
|
import { CRDT_RECORD_MAP_KEY } from '../sync';
|
|
13
13
|
import type { YPostRecord } from './crdt';
|
|
14
14
|
import type { YBlock, YBlocks } from './crdt-blocks';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
asRichTextOffset,
|
|
17
|
+
getRootMap,
|
|
18
|
+
richTextOffsetToHtmlIndex,
|
|
19
|
+
} from './crdt-utils';
|
|
16
20
|
import type {
|
|
17
21
|
AbsoluteBlockIndexPath,
|
|
18
22
|
WPBlockSelection,
|
|
@@ -178,7 +182,10 @@ function getCursorPosition(
|
|
|
178
182
|
|
|
179
183
|
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
180
184
|
currentYText,
|
|
181
|
-
richTextOffsetToHtmlIndex(
|
|
185
|
+
richTextOffsetToHtmlIndex(
|
|
186
|
+
currentYText.toString(),
|
|
187
|
+
asRichTextOffset( selection.offset )
|
|
188
|
+
)
|
|
182
189
|
);
|
|
183
190
|
|
|
184
191
|
return {
|