@wordpress/core-data 7.41.2-next.v.202603161435.0 → 7.43.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 +3 -1
- package/build/awareness/post-editor-awareness.cjs +12 -5
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/entities.cjs +30 -5
- package/build/entities.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +1 -1
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/private-actions.cjs +10 -0
- package/build/private-actions.cjs.map +2 -2
- package/build/private-selectors.cjs +20 -3
- package/build/private-selectors.cjs.map +2 -2
- package/build/queried-data/get-query-parts.cjs +8 -0
- package/build/queried-data/get-query-parts.cjs.map +2 -2
- package/build/queried-data/reducer.cjs +15 -9
- package/build/queried-data/reducer.cjs.map +2 -2
- package/build/queried-data/selectors.cjs +10 -2
- package/build/queried-data/selectors.cjs.map +2 -2
- package/build/reducer.cjs +22 -3
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +16 -8
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +16 -8
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +3 -0
- package/build/sync.cjs.map +2 -2
- package/build/types.cjs.map +2 -2
- 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 +66 -5
- 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 +1 -1
- package/build/utils/crdt-user-selections.cjs.map +2 -2
- package/build/utils/crdt-utils.cjs +54 -2
- package/build/utils/crdt-utils.cjs.map +2 -2
- package/build/utils/crdt.cjs +9 -23
- package/build/utils/crdt.cjs.map +2 -2
- package/build/utils/index.cjs +0 -3
- package/build/utils/index.cjs.map +2 -2
- package/build-module/awareness/post-editor-awareness.mjs +12 -5
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/entities.mjs +30 -5
- package/build-module/entities.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +1 -1
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/private-actions.mjs +9 -0
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-selectors.mjs +19 -3
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/queried-data/get-query-parts.mjs +8 -0
- package/build-module/queried-data/get-query-parts.mjs.map +2 -2
- package/build-module/queried-data/reducer.mjs +15 -9
- package/build-module/queried-data/reducer.mjs.map +2 -2
- package/build-module/queried-data/selectors.mjs +10 -2
- package/build-module/queried-data/selectors.mjs.map +2 -2
- package/build-module/reducer.mjs +20 -2
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +14 -7
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +16 -9
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +2 -0
- package/build-module/sync.mjs.map +2 -2
- package/build-module/types.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 +65 -5
- 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 +2 -2
- package/build-module/utils/crdt-user-selections.mjs.map +2 -2
- 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 +10 -23
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-module/utils/index.mjs +8 -10
- package/build-module/utils/index.mjs.map +2 -2
- 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/entities.d.ts.map +1 -1
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-actions.d.ts +10 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +11 -4
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/queried-data/get-query-parts.d.ts +13 -14
- package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
- package/build-types/queried-data/reducer.d.ts +11 -5
- package/build-types/queried-data/reducer.d.ts.map +1 -1
- package/build-types/queried-data/selectors.d.ts.map +1 -1
- package/build-types/reducer.d.ts +11 -0
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts +3 -0
- 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 +4 -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 +1 -2
- 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 +6 -7
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/index.d.ts +0 -1
- 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/awareness/post-editor-awareness.ts +13 -6
- package/src/awareness/test/post-editor-awareness.ts +15 -10
- package/src/entities.js +36 -5
- package/src/hooks/test/use-post-editor-awareness-state.ts +3 -3
- package/src/hooks/use-post-editor-awareness-state.ts +1 -1
- package/src/private-actions.js +18 -0
- package/src/private-selectors.ts +37 -4
- package/src/queried-data/get-query-parts.js +14 -7
- package/src/queried-data/reducer.js +28 -15
- package/src/queried-data/selectors.js +11 -3
- package/src/queried-data/test/get-query-parts.js +34 -0
- package/src/queried-data/test/reducer.js +78 -8
- package/src/queried-data/test/selectors.js +171 -0
- package/src/reducer.js +31 -0
- package/src/resolvers.js +23 -7
- package/src/selectors.ts +20 -19
- package/src/sync.ts +2 -0
- package/src/test/entities.js +185 -1
- package/src/types.ts +8 -2
- package/src/utils/block-selection-history.ts +5 -2
- package/src/utils/crdt-blocks.ts +115 -5
- package/src/utils/crdt-selection.ts +8 -2
- package/src/utils/crdt-text.ts +43 -0
- package/src/utils/crdt-user-selections.ts +13 -13
- package/src/utils/crdt-utils.ts +99 -0
- package/src/utils/crdt.ts +18 -30
- package/src/utils/index.js +0 -1
- package/src/utils/test/crdt-blocks.ts +199 -0
- 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 +229 -54
- package/build/utils/is-raw-attribute.cjs +0 -29
- package/build/utils/is-raw-attribute.cjs.map +0 -7
- package/build-module/utils/is-raw-attribute.mjs +0 -8
- package/build-module/utils/is-raw-attribute.mjs.map +0 -7
- package/build-types/utils/is-raw-attribute.d.ts +0 -10
- package/build-types/utils/is-raw-attribute.d.ts.map +0 -1
- package/src/utils/is-raw-attribute.js +0 -11
- package/src/utils/test/is-raw-attribute.js +0 -22
package/src/test/entities.js
CHANGED
|
@@ -8,6 +8,10 @@ jest.mock( '../sync', () => ( {
|
|
|
8
8
|
...jest.requireActual( '../sync' ),
|
|
9
9
|
getSyncManager: jest.fn(),
|
|
10
10
|
} ) );
|
|
11
|
+
jest.mock( '../utils/crdt', () => ( {
|
|
12
|
+
...jest.requireActual( '../utils/crdt' ),
|
|
13
|
+
applyPostChangesToCRDTDoc: jest.fn(),
|
|
14
|
+
} ) );
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Internal dependencies
|
|
@@ -19,7 +23,10 @@ import {
|
|
|
19
23
|
additionalEntityConfigLoaders,
|
|
20
24
|
} from '../entities';
|
|
21
25
|
import { getSyncManager } from '../sync';
|
|
22
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
applyPostChangesToCRDTDoc,
|
|
28
|
+
POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
|
|
29
|
+
} from '../utils/crdt';
|
|
23
30
|
|
|
24
31
|
describe( 'getMethodName', () => {
|
|
25
32
|
it( 'should return the right method name for an entity with the root kind', () => {
|
|
@@ -127,6 +134,183 @@ describe( 'prePersistPostType', () => {
|
|
|
127
134
|
} );
|
|
128
135
|
} );
|
|
129
136
|
|
|
137
|
+
describe( 'loadPostTypeEntities', () => {
|
|
138
|
+
let originalCollaborationEnabled;
|
|
139
|
+
|
|
140
|
+
beforeEach( () => {
|
|
141
|
+
apiFetch.mockReset();
|
|
142
|
+
applyPostChangesToCRDTDoc.mockReset();
|
|
143
|
+
originalCollaborationEnabled = window._wpCollaborationEnabled;
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
afterEach( () => {
|
|
147
|
+
window._wpCollaborationEnabled = originalCollaborationEnabled;
|
|
148
|
+
} );
|
|
149
|
+
|
|
150
|
+
it( 'should include custom taxonomy rest_bases in synced properties when collaboration is enabled', async () => {
|
|
151
|
+
window._wpCollaborationEnabled = true;
|
|
152
|
+
|
|
153
|
+
const mockPostTypes = {
|
|
154
|
+
book: {
|
|
155
|
+
name: 'Books',
|
|
156
|
+
rest_base: 'books',
|
|
157
|
+
rest_namespace: 'wp/v2',
|
|
158
|
+
taxonomies: [ 'genre', 'audience' ],
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const mockTaxonomies = {
|
|
162
|
+
genre: {
|
|
163
|
+
name: 'Genres',
|
|
164
|
+
rest_base: 'genres',
|
|
165
|
+
rest_namespace: 'wp/v2',
|
|
166
|
+
},
|
|
167
|
+
audience: {
|
|
168
|
+
name: 'Audiences',
|
|
169
|
+
rest_base: 'audiences',
|
|
170
|
+
rest_namespace: 'wp/v2',
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
apiFetch
|
|
175
|
+
.mockResolvedValueOnce( mockPostTypes )
|
|
176
|
+
.mockResolvedValueOnce( mockTaxonomies );
|
|
177
|
+
|
|
178
|
+
const postTypeLoader = additionalEntityConfigLoaders.find(
|
|
179
|
+
( loader ) => loader.kind === 'postType'
|
|
180
|
+
);
|
|
181
|
+
const entities = await postTypeLoader.loadEntities();
|
|
182
|
+
const bookEntity = entities.find( ( e ) => e.name === 'book' );
|
|
183
|
+
|
|
184
|
+
bookEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
|
|
185
|
+
|
|
186
|
+
expect( applyPostChangesToCRDTDoc ).toHaveBeenCalledWith(
|
|
187
|
+
{},
|
|
188
|
+
{},
|
|
189
|
+
expect.any( Set )
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
|
|
193
|
+
expect( syncedProperties ).toContain( 'genres' );
|
|
194
|
+
expect( syncedProperties ).toContain( 'audiences' );
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
it( 'should not fetch taxonomies when collaboration is disabled', async () => {
|
|
198
|
+
window._wpCollaborationEnabled = false;
|
|
199
|
+
|
|
200
|
+
const mockPostTypes = {
|
|
201
|
+
post: {
|
|
202
|
+
name: 'Posts',
|
|
203
|
+
rest_base: 'posts',
|
|
204
|
+
rest_namespace: 'wp/v2',
|
|
205
|
+
taxonomies: [ 'category', 'post_tag' ],
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
apiFetch.mockResolvedValueOnce( mockPostTypes );
|
|
210
|
+
|
|
211
|
+
const postTypeLoader = additionalEntityConfigLoaders.find(
|
|
212
|
+
( loader ) => loader.kind === 'postType'
|
|
213
|
+
);
|
|
214
|
+
const entities = await postTypeLoader.loadEntities();
|
|
215
|
+
const postEntity = entities.find( ( e ) => e.name === 'post' );
|
|
216
|
+
|
|
217
|
+
postEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
|
|
218
|
+
|
|
219
|
+
// Only one apiFetch call (post types), no taxonomy fetch.
|
|
220
|
+
expect( apiFetch ).toHaveBeenCalledTimes( 1 );
|
|
221
|
+
|
|
222
|
+
const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
|
|
223
|
+
expect( syncedProperties ).not.toContain( 'categories' );
|
|
224
|
+
expect( syncedProperties ).not.toContain( 'tags' );
|
|
225
|
+
} );
|
|
226
|
+
|
|
227
|
+
it( 'should skip taxonomy rest_base when taxonomy is not found in fetched taxonomies', async () => {
|
|
228
|
+
window._wpCollaborationEnabled = true;
|
|
229
|
+
|
|
230
|
+
const mockPostTypes = {
|
|
231
|
+
book: {
|
|
232
|
+
name: 'Books',
|
|
233
|
+
rest_base: 'books',
|
|
234
|
+
rest_namespace: 'wp/v2',
|
|
235
|
+
taxonomies: [ 'genre', 'missing_taxonomy' ],
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
const mockTaxonomies = {
|
|
239
|
+
genre: {
|
|
240
|
+
name: 'Genres',
|
|
241
|
+
rest_base: 'genres',
|
|
242
|
+
rest_namespace: 'wp/v2',
|
|
243
|
+
},
|
|
244
|
+
// 'missing_taxonomy' is intentionally absent.
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
apiFetch
|
|
248
|
+
.mockResolvedValueOnce( mockPostTypes )
|
|
249
|
+
.mockResolvedValueOnce( mockTaxonomies );
|
|
250
|
+
|
|
251
|
+
const postTypeLoader = additionalEntityConfigLoaders.find(
|
|
252
|
+
( loader ) => loader.kind === 'postType'
|
|
253
|
+
);
|
|
254
|
+
const entities = await postTypeLoader.loadEntities();
|
|
255
|
+
const bookEntity = entities.find( ( e ) => e.name === 'book' );
|
|
256
|
+
|
|
257
|
+
bookEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
|
|
258
|
+
|
|
259
|
+
const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
|
|
260
|
+
expect( syncedProperties ).toContain( 'genres' );
|
|
261
|
+
// missing_taxonomy has no rest_base entry, so nothing should be added for it.
|
|
262
|
+
expect( syncedProperties.size ).toBe( 16 ); // 15 base + 1 taxonomy (genres)
|
|
263
|
+
} );
|
|
264
|
+
|
|
265
|
+
it( 'should include base synced properties regardless of taxonomies', async () => {
|
|
266
|
+
window._wpCollaborationEnabled = true;
|
|
267
|
+
|
|
268
|
+
const mockPostTypes = {
|
|
269
|
+
page: {
|
|
270
|
+
name: 'Pages',
|
|
271
|
+
rest_base: 'pages',
|
|
272
|
+
rest_namespace: 'wp/v2',
|
|
273
|
+
taxonomies: [],
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
apiFetch
|
|
278
|
+
.mockResolvedValueOnce( mockPostTypes )
|
|
279
|
+
.mockResolvedValueOnce( {} );
|
|
280
|
+
|
|
281
|
+
const postTypeLoader = additionalEntityConfigLoaders.find(
|
|
282
|
+
( loader ) => loader.kind === 'postType'
|
|
283
|
+
);
|
|
284
|
+
const entities = await postTypeLoader.loadEntities();
|
|
285
|
+
const pageEntity = entities.find( ( e ) => e.name === 'page' );
|
|
286
|
+
|
|
287
|
+
pageEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
|
|
288
|
+
|
|
289
|
+
const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
|
|
290
|
+
const expectedBase = [
|
|
291
|
+
'author',
|
|
292
|
+
'blocks',
|
|
293
|
+
'content',
|
|
294
|
+
'comment_status',
|
|
295
|
+
'date',
|
|
296
|
+
'excerpt',
|
|
297
|
+
'featured_media',
|
|
298
|
+
'format',
|
|
299
|
+
'meta',
|
|
300
|
+
'ping_status',
|
|
301
|
+
'slug',
|
|
302
|
+
'status',
|
|
303
|
+
'sticky',
|
|
304
|
+
'template',
|
|
305
|
+
'title',
|
|
306
|
+
];
|
|
307
|
+
for ( const prop of expectedBase ) {
|
|
308
|
+
expect( syncedProperties ).toContain( prop );
|
|
309
|
+
}
|
|
310
|
+
expect( syncedProperties.size ).toBe( 15 );
|
|
311
|
+
} );
|
|
312
|
+
} );
|
|
313
|
+
|
|
130
314
|
describe( 'loadTaxonomyEntities', () => {
|
|
131
315
|
beforeEach( () => {
|
|
132
316
|
apiFetch.mockReset();
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
|
-
import type { Y } from '@wordpress/sync';
|
|
4
|
+
import type { ConnectionStatusDisconnected, Y } from '@wordpress/sync';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Internal dependencies
|
|
8
8
|
*/
|
|
9
9
|
import type { SelectionType } from './utils/crdt-user-selections';
|
|
10
10
|
|
|
11
|
+
export type { ConnectionStatus } from '@wordpress/sync';
|
|
12
|
+
|
|
13
|
+
export type ConnectionError = NonNullable<
|
|
14
|
+
ConnectionStatusDisconnected[ 'error' ]
|
|
15
|
+
>;
|
|
16
|
+
|
|
11
17
|
export interface AnyFunction {
|
|
12
18
|
( ...args: any[] ): any;
|
|
13
19
|
}
|
|
@@ -127,6 +133,6 @@ export type SelectionState =
|
|
|
127
133
|
| SelectionWholeBlock;
|
|
128
134
|
|
|
129
135
|
export interface ResolvedSelection {
|
|
130
|
-
|
|
136
|
+
richTextOffset: number | null;
|
|
131
137
|
localClientId: string | null;
|
|
132
138
|
}
|
|
@@ -9,7 +9,10 @@ import { Y } from '@wordpress/sync';
|
|
|
9
9
|
/**
|
|
10
10
|
* Internal dependencies
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
findBlockByClientIdInDoc,
|
|
14
|
+
richTextOffsetToHtmlIndex,
|
|
15
|
+
} from './crdt-utils';
|
|
13
16
|
import type { WPBlockSelection, WPSelection } from '../types';
|
|
14
17
|
|
|
15
18
|
// Default size for selection history (not including current selection)
|
|
@@ -163,7 +166,7 @@ function convertWPBlockSelectionToSelection(
|
|
|
163
166
|
const offset = selection.offset ?? 0;
|
|
164
167
|
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
165
168
|
changedYText,
|
|
166
|
-
offset
|
|
169
|
+
richTextOffsetToHtmlIndex( changedYText.toString(), offset )
|
|
167
170
|
);
|
|
168
171
|
|
|
169
172
|
return {
|
package/src/utils/crdt-blocks.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { Y } from '@wordpress/sync';
|
|
|
16
16
|
* Internal dependencies
|
|
17
17
|
*/
|
|
18
18
|
import { createYMap, type YMapRecord, type YMapWrap } from './crdt-utils';
|
|
19
|
+
import { getCachedRichTextData } from './crdt-text';
|
|
19
20
|
import { Delta } from '../sync';
|
|
20
21
|
|
|
21
22
|
interface BlockAttributes {
|
|
@@ -25,6 +26,7 @@ interface BlockAttributes {
|
|
|
25
26
|
interface BlockAttributeType {
|
|
26
27
|
role?: string;
|
|
27
28
|
type?: string;
|
|
29
|
+
query?: Record< string, BlockAttributeType >;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
interface BlockType {
|
|
@@ -62,6 +64,37 @@ export type YBlockAttributes = Y.Map< Y.Text | unknown >;
|
|
|
62
64
|
|
|
63
65
|
const serializableBlocksCache = new WeakMap< WeakKey, Block[] >();
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Recursively walk an attribute value and convert any RichTextData instances
|
|
69
|
+
* to their string (HTML) representation. This is necessary for array-type and
|
|
70
|
+
* object-type attributes, which can contain nested RichTextData.
|
|
71
|
+
*
|
|
72
|
+
* @param value The attribute value to serialize.
|
|
73
|
+
* @return The value with all RichTextData instances replaced by strings.
|
|
74
|
+
*/
|
|
75
|
+
function serializeAttributeValue( value: unknown ): unknown {
|
|
76
|
+
if ( value instanceof RichTextData ) {
|
|
77
|
+
return value.valueOf();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// e.g. core/table `body`: [ { cells: [ { content: RichTextData } ] } ]
|
|
81
|
+
if ( Array.isArray( value ) ) {
|
|
82
|
+
return value.map( serializeAttributeValue );
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// e.g. a single row inside core/table `body`: { cells: [ ... ] }
|
|
86
|
+
if ( value && typeof value === 'object' ) {
|
|
87
|
+
const result: Record< string, unknown > = {};
|
|
88
|
+
|
|
89
|
+
for ( const [ k, v ] of Object.entries( value ) ) {
|
|
90
|
+
result[ k ] = serializeAttributeValue( v );
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
65
98
|
function makeBlockAttributesSerializable(
|
|
66
99
|
blockName: string,
|
|
67
100
|
attributes: BlockAttributes
|
|
@@ -73,9 +106,7 @@ function makeBlockAttributesSerializable(
|
|
|
73
106
|
continue;
|
|
74
107
|
}
|
|
75
108
|
|
|
76
|
-
|
|
77
|
-
newAttributes[ key ] = value.valueOf();
|
|
78
|
-
}
|
|
109
|
+
newAttributes[ key ] = serializeAttributeValue( value );
|
|
79
110
|
}
|
|
80
111
|
return newAttributes;
|
|
81
112
|
}
|
|
@@ -93,6 +124,85 @@ function makeBlocksSerializable( blocks: Block[] ): Block[] {
|
|
|
93
124
|
} );
|
|
94
125
|
}
|
|
95
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Recursively walk an attribute value and convert any strings that correspond
|
|
129
|
+
* to rich-text schema nodes into RichTextData instances. This is the inverse
|
|
130
|
+
* of serializeAttributeValue and handles nested structures like table cells.
|
|
131
|
+
*
|
|
132
|
+
* @param schema The attribute type definition for this value.
|
|
133
|
+
* @param value The attribute value from CRDT (toJSON).
|
|
134
|
+
* @return The value with rich-text strings replaced by RichTextData.
|
|
135
|
+
*/
|
|
136
|
+
function deserializeAttributeValue(
|
|
137
|
+
schema: BlockAttributeType | undefined,
|
|
138
|
+
value: unknown
|
|
139
|
+
): unknown {
|
|
140
|
+
if ( schema?.type === 'rich-text' && typeof value === 'string' ) {
|
|
141
|
+
return getCachedRichTextData( value );
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// e.g. core/table `body`: [ { cells: [ { content: RichTextData } ] } ]
|
|
145
|
+
if ( Array.isArray( value ) ) {
|
|
146
|
+
return value.map( ( item ) =>
|
|
147
|
+
deserializeAttributeValue( schema, item )
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// e.g. a single row inside core/table `body`: { cells: [ ... ] }
|
|
152
|
+
if ( value && typeof value === 'object' ) {
|
|
153
|
+
const result: Record< string, unknown > = {};
|
|
154
|
+
|
|
155
|
+
for ( const [ key, innerValue ] of Object.entries(
|
|
156
|
+
value as Record< string, unknown >
|
|
157
|
+
) ) {
|
|
158
|
+
result[ key ] = deserializeAttributeValue(
|
|
159
|
+
schema?.query?.[ key ],
|
|
160
|
+
innerValue
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Convert blocks from their CRDT-serialized form back to the runtime form
|
|
172
|
+
* expected by the block editor. Rich-text attributes are stored as Y.Text in
|
|
173
|
+
* the CRDT document, which serializes to plain strings via toJSON(). This
|
|
174
|
+
* function restores them to RichTextData instances so that block edit
|
|
175
|
+
* components that rely on RichTextData methods (e.g. `.text`) work correctly.
|
|
176
|
+
*
|
|
177
|
+
* @param blocks Blocks as extracted from the CRDT document via toJSON().
|
|
178
|
+
* @return Blocks with rich-text attributes restored to RichTextData.
|
|
179
|
+
*/
|
|
180
|
+
export function deserializeBlockAttributes( blocks: Block[] ): Block[] {
|
|
181
|
+
return blocks.map( ( block: Block ) => {
|
|
182
|
+
const { name, innerBlocks, attributes, ...rest } = block;
|
|
183
|
+
|
|
184
|
+
const newAttributes = { ...attributes };
|
|
185
|
+
|
|
186
|
+
for ( const [ key, value ] of Object.entries( attributes ) ) {
|
|
187
|
+
const schema = getBlockAttributeType( name, key );
|
|
188
|
+
|
|
189
|
+
if ( schema ) {
|
|
190
|
+
newAttributes[ key ] = deserializeAttributeValue(
|
|
191
|
+
schema,
|
|
192
|
+
value
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
...rest,
|
|
199
|
+
name,
|
|
200
|
+
attributes: newAttributes,
|
|
201
|
+
innerBlocks: deserializeBlockAttributes( innerBlocks ?? [] ),
|
|
202
|
+
};
|
|
203
|
+
} );
|
|
204
|
+
}
|
|
205
|
+
|
|
96
206
|
/**
|
|
97
207
|
* @param {any} gblock
|
|
98
208
|
* @param {Y.Map} yblock
|
|
@@ -452,8 +562,8 @@ function getBlockAttributeType(
|
|
|
452
562
|
new Map< string, BlockAttributeType >(
|
|
453
563
|
Object.entries( blockType.attributes ?? {} ).map(
|
|
454
564
|
( [ name, definition ] ) => {
|
|
455
|
-
const { role, type } = definition;
|
|
456
|
-
return [ name, { role, type } ];
|
|
565
|
+
const { role, type, query } = definition;
|
|
566
|
+
return [ name, { role, type, query } ];
|
|
457
567
|
}
|
|
458
568
|
)
|
|
459
569
|
)
|
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
type YFullSelection,
|
|
19
19
|
type YSelection,
|
|
20
20
|
} from './block-selection-history';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
findBlockByClientIdInDoc,
|
|
23
|
+
htmlIndexToRichTextOffset,
|
|
24
|
+
} from './crdt-utils';
|
|
22
25
|
import type { WPBlockSelection, WPSelection } from '../types';
|
|
23
26
|
|
|
24
27
|
// WeakMap to store BlockSelectionHistory instances per Y.Doc
|
|
@@ -74,7 +77,10 @@ function convertYSelectionToBlockSelection(
|
|
|
74
77
|
return {
|
|
75
78
|
clientId,
|
|
76
79
|
attributeKey,
|
|
77
|
-
offset:
|
|
80
|
+
offset: htmlIndexToRichTextOffset(
|
|
81
|
+
absolutePosition.type.toString(),
|
|
82
|
+
absolutePosition.index
|
|
83
|
+
),
|
|
78
84
|
};
|
|
79
85
|
}
|
|
80
86
|
} else if ( ySelection.type === YSelectionType.BlockSelection ) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { RichTextData } from '@wordpress/rich-text';
|
|
5
|
+
|
|
6
|
+
const RICH_TEXT_CACHE_MAX_SIZE = 500;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns a function that converts HTML strings to RichTextData instances,
|
|
10
|
+
* using a FIFO cache bounded by `maxSize` to avoid re-parsing identical
|
|
11
|
+
* strings. Repeated calls with the same string return the cached instance
|
|
12
|
+
* without re-running the HTML parser and DOM traversal.
|
|
13
|
+
*
|
|
14
|
+
* @param maxSize Maximum number of entries to hold in the cache.
|
|
15
|
+
* @return A cached version of RichTextData.fromHTMLString.
|
|
16
|
+
*/
|
|
17
|
+
export function createRichTextDataCache(
|
|
18
|
+
maxSize: number
|
|
19
|
+
): ( value: string ) => RichTextData {
|
|
20
|
+
const cache = new Map< string, RichTextData >();
|
|
21
|
+
|
|
22
|
+
return function ( value: string ): RichTextData {
|
|
23
|
+
const cached = cache.get( value );
|
|
24
|
+
|
|
25
|
+
if ( cached ) {
|
|
26
|
+
return cached;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = RichTextData.fromHTMLString( value );
|
|
30
|
+
|
|
31
|
+
if ( cache.size >= maxSize ) {
|
|
32
|
+
// Evict the oldest entry (Map preserves insertion order).
|
|
33
|
+
cache.delete( cache.keys().next().value! );
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
cache.set( value, result );
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const getCachedRichTextData = createRichTextDataCache(
|
|
42
|
+
RICH_TEXT_CACHE_MAX_SIZE
|
|
43
|
+
);
|
|
@@ -12,18 +12,18 @@ 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 { getRootMap } from './crdt-utils';
|
|
16
|
-
import type {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
import { getRootMap, richTextOffsetToHtmlIndex } from './crdt-utils';
|
|
16
|
+
import type {
|
|
17
|
+
AbsoluteBlockIndexPath,
|
|
18
|
+
WPBlockSelection,
|
|
19
|
+
SelectionState,
|
|
20
|
+
SelectionNone,
|
|
21
|
+
SelectionCursor,
|
|
22
|
+
SelectionInOneBlock,
|
|
23
|
+
SelectionInMultipleBlocks,
|
|
24
|
+
SelectionWholeBlock,
|
|
25
|
+
SelectionDirection,
|
|
26
|
+
CursorPosition,
|
|
27
27
|
} from '../types';
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -178,7 +178,7 @@ function getCursorPosition(
|
|
|
178
178
|
|
|
179
179
|
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
180
180
|
currentYText,
|
|
181
|
-
selection.offset
|
|
181
|
+
richTextOffsetToHtmlIndex( currentYText.toString(), selection.offset )
|
|
182
182
|
);
|
|
183
183
|
|
|
184
184
|
return {
|
package/src/utils/crdt-utils.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
4
|
import { Y } from '@wordpress/sync';
|
|
5
|
+
import { create, insert, toHTMLString } from '@wordpress/rich-text';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Internal dependencies
|
|
@@ -104,6 +105,104 @@ export function findBlockByClientIdInDoc(
|
|
|
104
105
|
return findBlockByClientIdInBlocks( blockId, blocks );
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
// Marker for insertion.
|
|
109
|
+
const MARKER_START = 0xe000;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Pick a marker character that does not appear in `text`. Returns the marker
|
|
113
|
+
* or `null` if all candidates are present (extremely unlikely in practice).
|
|
114
|
+
*
|
|
115
|
+
* @param text The string to check for existing marker characters.
|
|
116
|
+
*/
|
|
117
|
+
function pickMarker( text: string ): string | null {
|
|
118
|
+
const tryCount = 0x10;
|
|
119
|
+
|
|
120
|
+
// Scan the unicode private use area for the first code point not present
|
|
121
|
+
// in the text.
|
|
122
|
+
for ( let code = MARKER_START; code < MARKER_START + tryCount; code++ ) {
|
|
123
|
+
const candidate = String.fromCharCode( code );
|
|
124
|
+
|
|
125
|
+
if ( ! text.includes( candidate ) ) {
|
|
126
|
+
return candidate;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Convert an HTML character index (counting tag characters) to a rich-text
|
|
135
|
+
* offset (counting only text characters). Used on read paths where Y.Text
|
|
136
|
+
* resolves to an HTML index but the block editor expects a text offset.
|
|
137
|
+
*
|
|
138
|
+
* @param html The full HTML string from Y.Text.
|
|
139
|
+
* @param htmlIndex The HTML character index.
|
|
140
|
+
* @return The corresponding rich-text offset.
|
|
141
|
+
*/
|
|
142
|
+
export function htmlIndexToRichTextOffset(
|
|
143
|
+
html: string,
|
|
144
|
+
htmlIndex: number
|
|
145
|
+
): number {
|
|
146
|
+
if ( ! html.includes( '<' ) && ! html.includes( '&' ) ) {
|
|
147
|
+
return htmlIndex;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const marker = pickMarker( html );
|
|
151
|
+
if ( ! marker ) {
|
|
152
|
+
return htmlIndex;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Insert marker and let create() do the parsing.
|
|
156
|
+
const withMarker =
|
|
157
|
+
html.slice( 0, htmlIndex ) + marker + html.slice( htmlIndex );
|
|
158
|
+
const value = create( { html: withMarker } );
|
|
159
|
+
const markerPos = value.text.indexOf( marker );
|
|
160
|
+
|
|
161
|
+
return markerPos === -1 ? htmlIndex : markerPos;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Convert a rich-text offset (counting only text characters) to an HTML
|
|
166
|
+
* character index (counting tag characters). Used on write paths where the
|
|
167
|
+
* block editor provides a text offset but Y.Text expects an HTML index.
|
|
168
|
+
*
|
|
169
|
+
* @param html The full HTML string from Y.Text.
|
|
170
|
+
* @param richTextOffset The rich-text text offset.
|
|
171
|
+
* @return The corresponding HTML character index.
|
|
172
|
+
*/
|
|
173
|
+
export function richTextOffsetToHtmlIndex(
|
|
174
|
+
html: string,
|
|
175
|
+
richTextOffset: number
|
|
176
|
+
): number {
|
|
177
|
+
if ( ! html.includes( '<' ) && ! html.includes( '&' ) ) {
|
|
178
|
+
return richTextOffset;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const marker = pickMarker( html );
|
|
182
|
+
if ( ! marker ) {
|
|
183
|
+
return richTextOffset;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const value = create( { html } );
|
|
187
|
+
const markerValue = create( { text: marker } );
|
|
188
|
+
// The marker must inherit the formatting at the insertion point so that
|
|
189
|
+
// toHTMLString does not split surrounding tags (e.g. <strong>) around it.
|
|
190
|
+
if ( value.formats[ richTextOffset ] ) {
|
|
191
|
+
markerValue.formats[ 0 ] = value.formats[ richTextOffset ];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const withMarker = insert(
|
|
195
|
+
value,
|
|
196
|
+
markerValue,
|
|
197
|
+
richTextOffset,
|
|
198
|
+
richTextOffset
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const htmlWithMarker = toHTMLString( { value: withMarker } );
|
|
202
|
+
const markerIndex = htmlWithMarker.indexOf( marker );
|
|
203
|
+
return markerIndex === -1 ? richTextOffset : markerIndex;
|
|
204
|
+
}
|
|
205
|
+
|
|
107
206
|
function findBlockByClientIdInBlocks(
|
|
108
207
|
blockId: string,
|
|
109
208
|
blocks: YBlocks
|