@wordpress/core-data 7.46.0 → 7.47.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 (148) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +0 -27
  3. package/build/actions.cjs +0 -19
  4. package/build/actions.cjs.map +2 -2
  5. package/build/awareness/block-lookup.cjs +13 -0
  6. package/build/awareness/block-lookup.cjs.map +2 -2
  7. package/build/awareness/post-editor-awareness.cjs +21 -9
  8. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  9. package/build/hooks/use-entity-block-editor.cjs +4 -4
  10. package/build/hooks/use-entity-block-editor.cjs.map +2 -2
  11. package/build/hooks/use-post-editor-awareness-state.cjs +2 -1
  12. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  13. package/build/hooks/use-resource-permissions.cjs +3 -5
  14. package/build/hooks/use-resource-permissions.cjs.map +2 -2
  15. package/build/index.cjs +0 -6
  16. package/build/index.cjs.map +2 -2
  17. package/build/parsed-blocks-cache.cjs +36 -0
  18. package/build/parsed-blocks-cache.cjs.map +7 -0
  19. package/build/private-actions.cjs +25 -2
  20. package/build/private-actions.cjs.map +2 -2
  21. package/build/private-apis.cjs +9 -5
  22. package/build/private-apis.cjs.map +3 -3
  23. package/build/private-selectors.cjs +15 -0
  24. package/build/private-selectors.cjs.map +2 -2
  25. package/build/resolvers.cjs +12 -2
  26. package/build/resolvers.cjs.map +2 -2
  27. package/build/selectors.cjs +0 -15
  28. package/build/selectors.cjs.map +2 -2
  29. package/build/sync.cjs +5 -0
  30. package/build/sync.cjs.map +2 -2
  31. package/build/types.cjs +0 -16
  32. package/build/types.cjs.map +3 -3
  33. package/build/utils/block-selection-history.cjs +5 -4
  34. package/build/utils/block-selection-history.cjs.map +2 -2
  35. package/build/utils/crdt-blocks.cjs +3 -0
  36. package/build/utils/crdt-blocks.cjs.map +2 -2
  37. package/build/utils/crdt-user-selections.cjs +10 -2
  38. package/build/utils/crdt-user-selections.cjs.map +3 -3
  39. package/build/utils/crdt-utils.cjs +23 -0
  40. package/build/utils/crdt-utils.cjs.map +2 -2
  41. package/build/utils/crdt.cjs +28 -4
  42. package/build/utils/crdt.cjs.map +2 -2
  43. package/build-module/actions.mjs +0 -18
  44. package/build-module/actions.mjs.map +2 -2
  45. package/build-module/awareness/block-lookup.mjs +12 -0
  46. package/build-module/awareness/block-lookup.mjs.map +2 -2
  47. package/build-module/awareness/post-editor-awareness.mjs +26 -9
  48. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  49. package/build-module/hooks/use-entity-block-editor.mjs +2 -2
  50. package/build-module/hooks/use-entity-block-editor.mjs.map +2 -2
  51. package/build-module/hooks/use-post-editor-awareness-state.mjs +2 -1
  52. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  53. package/build-module/hooks/use-resource-permissions.mjs +3 -5
  54. package/build-module/hooks/use-resource-permissions.mjs.map +2 -2
  55. package/build-module/index.mjs +0 -4
  56. package/build-module/index.mjs.map +2 -2
  57. package/build-module/parsed-blocks-cache.mjs +10 -0
  58. package/build-module/parsed-blocks-cache.mjs.map +7 -0
  59. package/build-module/private-actions.mjs +23 -1
  60. package/build-module/private-actions.mjs.map +2 -2
  61. package/build-module/private-apis.mjs +12 -5
  62. package/build-module/private-apis.mjs.map +3 -3
  63. package/build-module/private-selectors.mjs +14 -0
  64. package/build-module/private-selectors.mjs.map +2 -2
  65. package/build-module/resolvers.mjs +12 -2
  66. package/build-module/resolvers.mjs.map +2 -2
  67. package/build-module/selectors.mjs +0 -14
  68. package/build-module/selectors.mjs.map +2 -2
  69. package/build-module/sync.mjs +4 -0
  70. package/build-module/sync.mjs.map +2 -2
  71. package/build-module/types.mjs +0 -9
  72. package/build-module/types.mjs.map +4 -4
  73. package/build-module/utils/block-selection-history.mjs +6 -4
  74. package/build-module/utils/block-selection-history.mjs.map +2 -2
  75. package/build-module/utils/crdt-blocks.mjs +3 -0
  76. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  77. package/build-module/utils/crdt-user-selections.mjs +10 -2
  78. package/build-module/utils/crdt-user-selections.mjs.map +3 -3
  79. package/build-module/utils/crdt-utils.mjs +22 -0
  80. package/build-module/utils/crdt-utils.mjs.map +2 -2
  81. package/build-module/utils/crdt.mjs +32 -5
  82. package/build-module/utils/crdt.mjs.map +2 -2
  83. package/build-types/actions.d.ts +0 -11
  84. package/build-types/actions.d.ts.map +1 -1
  85. package/build-types/awareness/block-lookup.d.ts +12 -0
  86. package/build-types/awareness/block-lookup.d.ts.map +1 -1
  87. package/build-types/awareness/post-editor-awareness.d.ts +2 -5
  88. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  89. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  90. package/build-types/hooks/use-resource-permissions.d.ts.map +1 -1
  91. package/build-types/index.d.ts +0 -8
  92. package/build-types/index.d.ts.map +1 -1
  93. package/build-types/parsed-blocks-cache.d.ts +10 -0
  94. package/build-types/parsed-blocks-cache.d.ts.map +1 -0
  95. package/build-types/private-actions.d.ts +12 -0
  96. package/build-types/private-actions.d.ts.map +1 -1
  97. package/build-types/private-apis.d.ts +20 -0
  98. package/build-types/private-apis.d.ts.map +1 -1
  99. package/build-types/private-selectors.d.ts +10 -0
  100. package/build-types/private-selectors.d.ts.map +1 -1
  101. package/build-types/queried-data/selectors.d.ts +1 -1
  102. package/build-types/queried-data/selectors.d.ts.map +1 -1
  103. package/build-types/resolvers.d.ts.map +1 -1
  104. package/build-types/selectors.d.ts +0 -9
  105. package/build-types/selectors.d.ts.map +1 -1
  106. package/build-types/sync.d.ts +6 -0
  107. package/build-types/sync.d.ts.map +1 -1
  108. package/build-types/types.d.ts +3 -10
  109. package/build-types/types.d.ts.map +1 -1
  110. package/build-types/utils/block-selection-history.d.ts.map +1 -1
  111. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  112. package/build-types/utils/crdt-user-selections.d.ts +10 -1
  113. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  114. package/build-types/utils/crdt-utils.d.ts +11 -0
  115. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  116. package/build-types/utils/crdt.d.ts +5 -1
  117. package/build-types/utils/crdt.d.ts.map +1 -1
  118. package/package.json +20 -20
  119. package/src/actions.js +0 -29
  120. package/src/awareness/block-lookup.ts +34 -0
  121. package/src/awareness/post-editor-awareness.ts +32 -14
  122. package/src/awareness/test/block-lookup.ts +70 -0
  123. package/src/awareness/test/post-editor-awareness.ts +243 -0
  124. package/src/hooks/test/use-post-editor-awareness-state.ts +3 -0
  125. package/src/hooks/test/use-resource-permissions.js +57 -0
  126. package/src/hooks/use-entity-block-editor.js +2 -2
  127. package/src/hooks/use-post-editor-awareness-state.ts +1 -0
  128. package/src/hooks/use-resource-permissions.ts +5 -7
  129. package/src/index.js +0 -7
  130. package/src/parsed-blocks-cache.js +12 -0
  131. package/src/private-actions.js +34 -0
  132. package/src/{private-apis.js → private-apis.ts} +13 -3
  133. package/src/private-selectors.ts +33 -0
  134. package/src/resolvers.js +27 -5
  135. package/src/selectors.ts +0 -32
  136. package/src/sync.ts +9 -0
  137. package/src/test/resolvers.js +13 -7
  138. package/src/types.ts +16 -11
  139. package/src/utils/block-selection-history.ts +10 -7
  140. package/src/utils/crdt-blocks.ts +24 -0
  141. package/src/utils/crdt-user-selections.ts +15 -2
  142. package/src/utils/crdt-utils.ts +41 -0
  143. package/src/utils/crdt.ts +83 -10
  144. package/src/utils/test/block-selection-history.test.ts +42 -0
  145. package/src/utils/test/crdt-blocks.ts +37 -0
  146. package/src/utils/test/crdt-user-selections.ts +39 -0
  147. package/src/utils/test/crdt-utils.ts +52 -0
  148. package/src/utils/test/crdt.ts +208 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/crdt-blocks.ts"],
4
- "sourcesContent": ["/**\n * External dependencies\n */\nimport { v4 as uuidv4 } from 'uuid';\nimport fastDeepEqual from 'fast-deep-equal/es6/index.js';\n\n/**\n * WordPress dependencies\n */\nimport { getBlockTypes } from '@wordpress/blocks';\nimport { RichTextData } from '@wordpress/rich-text';\nimport { Y } from '@wordpress/sync';\n\n/**\n * Internal dependencies\n */\nimport {\n\tasRichTextOffset,\n\tcreateYMap,\n\trichTextOffsetToHtmlIndex,\n\ttype HtmlStringIndex,\n\ttype YMapRecord,\n\ttype YMapWrap,\n} from './crdt-utils';\nimport { getCachedRichTextData } from './crdt-text';\nimport { Delta } from '../sync';\nimport { type WPBlockSelection } from '../types';\n\ninterface BlockAttributes {\n\t[ key: string ]: unknown;\n}\n\ninterface BlockAttributeSchema {\n\trole?: string;\n\ttype?: string;\n\tquery?: Record< string, BlockAttributeSchema >;\n}\n\ninterface BlockType {\n\tattributes?: Record< string, BlockAttributeSchema >;\n\tname: string;\n}\n\n// A block as represented in Gutenberg's data store.\nexport interface Block {\n\tattributes: BlockAttributes;\n\tclientId?: string;\n\tinnerBlocks: Block[];\n\tisValid?: boolean;\n\tname: string;\n\toriginalContent?: string;\n\tvalidationIssues?: string[]; // unserializable\n}\n\n// A block as represented in the CRDT document (Y.Map).\nexport interface YBlockRecord extends YMapRecord {\n\tattributes: YBlockAttributes;\n\tclientId: string;\n\tinnerBlocks: YBlocks;\n\tisValid?: boolean;\n\toriginalContent?: string;\n\tname: string;\n}\n\nexport type YBlock = YMapWrap< YBlockRecord >;\nexport type YBlocks = Y.Array< YBlock >;\n\n// Block attribute schema cannot be known at compile time, so we use Y.Map.\n// Attribute values will be typed as the union of `Y.Text` and `unknown`.\nexport type YBlockAttributes = Y.Map< Y.Text | unknown >;\n\n/**\n * Optional description of where a cursor falls.\n *\n * Used to coordinate shifting of cursor when applying changes\n * to a Y.Doc with RichText instances.\n */\nexport type MergeCursorPosition = WPBlockSelection | null;\n\nconst serializableBlocksCache = new WeakMap< WeakKey, Block[] >();\n\n/**\n * Recursively walk an attribute value and convert any RichTextData instances\n * to their string (HTML) representation. This is necessary for array-type and\n * object-type attributes, which can contain nested RichTextData.\n *\n * @param value The attribute value to serialize.\n * @return The value with all RichTextData instances replaced by strings.\n */\nfunction serializeAttributeValue( value: unknown ): unknown {\n\tif ( value instanceof RichTextData ) {\n\t\treturn value.valueOf();\n\t}\n\n\t// e.g. core/table `body`: [ { cells: [ { content: RichTextData } ] } ]\n\tif ( Array.isArray( value ) ) {\n\t\treturn value.map( serializeAttributeValue );\n\t}\n\n\t// e.g. a single row inside core/table `body`: { cells: [ ... ] }\n\tif ( value && typeof value === 'object' ) {\n\t\tconst result: Record< string, unknown > = {};\n\n\t\tfor ( const [ k, v ] of Object.entries( value ) ) {\n\t\t\tresult[ k ] = serializeAttributeValue( v );\n\t\t}\n\t\treturn result;\n\t}\n\n\treturn value;\n}\n\nfunction makeBlockAttributesSerializable(\n\tblockName: string,\n\tattributes: BlockAttributes\n): BlockAttributes {\n\tconst newAttributes = { ...attributes };\n\tfor ( const [ key, value ] of Object.entries( attributes ) ) {\n\t\tif ( isLocalAttribute( blockName, key ) ) {\n\t\t\tdelete newAttributes[ key ];\n\t\t\tcontinue;\n\t\t}\n\n\t\tnewAttributes[ key ] = serializeAttributeValue( value );\n\t}\n\treturn newAttributes;\n}\n\n/**\n * Recursively removes properties which cannot be serialized from a list of block objects.\n *\n * @param blocks Eemove unserializable properties from each block object in this set.\n * @return Copies of the provided blocks without the unserializable properties.\n */\nfunction makeBlocksSerializable( blocks: Block[] ): Block[] {\n\treturn blocks.map( ( block: Block ) => {\n\t\tconst {\n\t\t\tname,\n\t\t\tinnerBlocks,\n\t\t\tattributes,\n\t\t\t/*\n\t\t\t * Any validation issues discovered when loading a block are appended\n\t\t\t * to the block node with a logging function, which cannot be serialized.\n\t\t\t *\n\t\t\t * @see import(\"@wordpress/blocks/src/api/parser\").parseRawBlock()\n\t\t\t */\n\t\t\tvalidationIssues,\n\t\t\t...rest\n\t\t} = block;\n\n\t\treturn {\n\t\t\t...rest,\n\t\t\tname,\n\t\t\tattributes: makeBlockAttributesSerializable( name, attributes ),\n\t\t\tinnerBlocks: makeBlocksSerializable( innerBlocks ),\n\t\t};\n\t} );\n}\n\n/**\n * Recursively walk an attribute value and convert any strings that correspond\n * to rich-text schema nodes into RichTextData instances. This is the inverse\n * of serializeAttributeValue and handles nested structures like table cells.\n *\n * @param schema The attribute type definition for this value.\n * @param value The attribute value from CRDT (toJSON).\n * @return The value with rich-text strings replaced by RichTextData.\n */\nfunction deserializeAttributeValue(\n\tschema: BlockAttributeSchema | undefined,\n\tvalue: unknown\n): unknown {\n\tif ( schema?.type === 'rich-text' && typeof value === 'string' ) {\n\t\treturn getCachedRichTextData( value );\n\t}\n\n\t// e.g. core/table `body`: [ { cells: [ { content: RichTextData } ] } ]\n\tif ( Array.isArray( value ) ) {\n\t\treturn value.map( ( item ) =>\n\t\t\tdeserializeAttributeValue( schema, item )\n\t\t);\n\t}\n\n\t// e.g. a single row inside core/table `body`: { cells: [ ... ] }\n\tif ( value && typeof value === 'object' ) {\n\t\tconst result: Record< string, unknown > = {};\n\n\t\tfor ( const [ key, innerValue ] of Object.entries(\n\t\t\tvalue as Record< string, unknown >\n\t\t) ) {\n\t\t\tresult[ key ] = deserializeAttributeValue(\n\t\t\t\tschema?.query?.[ key ],\n\t\t\t\tinnerValue\n\t\t\t);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\treturn value;\n}\n\n/**\n * Convert blocks from their CRDT-serialized form back to the runtime form\n * expected by the block editor. Rich-text attributes are stored as Y.Text in\n * the CRDT document, which serializes to plain strings via toJSON(). This\n * function restores them to RichTextData instances so that block edit\n * components that rely on RichTextData methods (e.g. `.text`) work correctly.\n *\n * @param blocks Blocks as extracted from the CRDT document via toJSON().\n * @return Blocks with rich-text attributes restored to RichTextData.\n */\nexport function deserializeBlockAttributes( blocks: Block[] ): Block[] {\n\treturn blocks.map( ( block: Block ) => {\n\t\tconst { name, innerBlocks, attributes, ...rest } = block;\n\n\t\tconst newAttributes = { ...attributes };\n\n\t\tfor ( const [ key, value ] of Object.entries( attributes ) ) {\n\t\t\tconst schema = getBlockAttributeSchema( name, key );\n\n\t\t\tif ( schema ) {\n\t\t\t\tnewAttributes[ key ] = deserializeAttributeValue(\n\t\t\t\t\tschema,\n\t\t\t\t\tvalue\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\t...rest,\n\t\t\tname,\n\t\t\tattributes: newAttributes,\n\t\t\tinnerBlocks: deserializeBlockAttributes( innerBlocks ?? [] ),\n\t\t};\n\t} );\n}\n\n/**\n * @param {any} gblock\n * @param {Y.Map} yblock\n */\nfunction areBlocksEqual( gblock: Block, yblock: YBlock ): boolean {\n\tconst yblockAsJson = yblock.toJSON();\n\n\t// we must not sync clientId, as this can't be generated consistently and\n\t// hence will lead to merge conflicts.\n\tconst overwrites = {\n\t\tinnerBlocks: null,\n\t\tclientId: null,\n\t};\n\tconst res = fastDeepEqual(\n\t\tObject.assign( {}, gblock, overwrites ),\n\t\tObject.assign( {}, yblockAsJson, overwrites )\n\t);\n\tconst inners = gblock.innerBlocks || [];\n\tconst yinners = yblock.get( 'innerBlocks' );\n\treturn (\n\t\tres &&\n\t\tinners.length === yinners?.length &&\n\t\tinners.every( ( block: Block, i: number ) =>\n\t\t\tareBlocksEqual( block, yinners.get( i ) )\n\t\t)\n\t);\n}\n\nfunction createNewYAttributeMap(\n\tblockName: string,\n\tattributes: BlockAttributes\n): YBlockAttributes {\n\treturn new Y.Map(\n\t\tObject.entries( attributes ).map(\n\t\t\t( [ attributeName, attributeValue ] ) => {\n\t\t\t\treturn [\n\t\t\t\t\tattributeName,\n\t\t\t\t\tcreateNewYAttributeValue(\n\t\t\t\t\t\tblockName,\n\t\t\t\t\t\tattributeName,\n\t\t\t\t\t\tattributeValue\n\t\t\t\t\t),\n\t\t\t\t];\n\t\t\t}\n\t\t)\n\t);\n}\n\nfunction createNewYAttributeValue(\n\tblockName: string,\n\tattributeName: string,\n\tattributeValue: unknown\n): Y.Text | Y.Array< unknown > | Y.Map< unknown > | unknown {\n\tconst schema = getBlockAttributeSchema( blockName, attributeName );\n\treturn createYValueFromSchema( schema, attributeValue );\n}\n\n/**\n * Recursively create the appropriate Y.js type for a value based on its\n * block-attribute schema.\n *\n * - `rich-text` -> Y.Text\n * - `array` with query -> Y.Array of Y.Maps\n * - `object` with query -> Y.Map\n * - anything else -> plain value (unchanged)\n *\n * @param schema The attribute type definition.\n * @param value The plain JS value to convert.\n * @return A Y.js type or the original value.\n */\nfunction createYValueFromSchema(\n\tschema: BlockAttributeSchema | undefined,\n\tvalue: unknown\n): Y.Text | Y.Array< unknown > | Y.Map< unknown > | unknown {\n\tif ( ! schema ) {\n\t\treturn value;\n\t}\n\n\tif ( schema.type === 'rich-text' ) {\n\t\treturn new Y.Text( value?.toString() ?? '' );\n\t}\n\n\tif ( schema.type === 'array' && schema.query && Array.isArray( value ) ) {\n\t\tconst query = schema.query;\n\t\tconst yArray = new Y.Array< Y.Map< unknown > >();\n\n\t\tyArray.insert(\n\t\t\t0,\n\t\t\tvalue.map( ( item ) => createYMapFromQuery( query, item ) )\n\t\t);\n\n\t\treturn yArray;\n\t}\n\n\tif ( schema.type === 'object' && schema.query && isRecord( value ) ) {\n\t\treturn createYMapFromQuery( schema.query, value );\n\t}\n\n\treturn value;\n}\n\n/**\n * Type guard that narrows `unknown` to `Record< string, unknown >`.\n *\n * @param value Value to check.\n * @return True if `value` is a non-null, non-array object.\n */\nfunction isRecord( value: unknown ): value is Record< string, unknown > {\n\treturn !! value && typeof value === 'object' && ! Array.isArray( value );\n}\n\n/**\n * Create a Y.Map from a plain object, using a query schema to decide which\n * properties should become nested Y.js types (Y.Text, Y.Array, Y.Map).\n *\n * @param query The query schema defining the properties.\n * @param obj The plain object to convert.\n * @return A Y.Map with typed values.\n */\nfunction createYMapFromQuery(\n\tquery: Record< string, BlockAttributeSchema >,\n\tobj: unknown\n): Y.Map< unknown > {\n\tif ( ! isRecord( obj ) ) {\n\t\treturn new Y.Map();\n\t}\n\n\tconst entries: [ string, unknown ][] = Object.entries( obj ).map(\n\t\t( [ key, val ] ): [ string, unknown ] => {\n\t\t\tconst subSchema = query[ key ];\n\t\t\treturn [ key, createYValueFromSchema( subSchema, val ) ];\n\t\t}\n\t);\n\n\treturn new Y.Map( entries );\n}\n\nfunction createNewYBlock( block: Block ): YBlock {\n\treturn createYMap< YBlockRecord >(\n\t\tObject.fromEntries(\n\t\t\tObject.entries( block ).map( ( [ key, value ] ) => {\n\t\t\t\tswitch ( key ) {\n\t\t\t\t\tcase 'attributes': {\n\t\t\t\t\t\treturn [\n\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\tcreateNewYAttributeMap( block.name, value ),\n\t\t\t\t\t\t];\n\t\t\t\t\t}\n\n\t\t\t\t\tcase 'innerBlocks': {\n\t\t\t\t\t\tconst innerBlocks = new Y.Array();\n\n\t\t\t\t\t\t// If not an array, set to empty Y.Array.\n\t\t\t\t\t\tif ( ! Array.isArray( value ) ) {\n\t\t\t\t\t\t\treturn [ key, innerBlocks ];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tinnerBlocks.insert(\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\tvalue.map( ( innerBlock: Block ) =>\n\t\t\t\t\t\t\t\tcreateNewYBlock( innerBlock )\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\treturn [ key, innerBlocks ];\n\t\t\t\t\t}\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn [ key, value ];\n\t\t\t\t}\n\t\t\t} )\n\t\t)\n\t);\n}\n\n/**\n * Merge incoming block data into the local Y.Doc.\n * This function is called to sync local block changes to a shared Y.Doc.\n *\n * @param yblocks The blocks in the local Y.Doc.\n * @param incomingBlocks Gutenberg blocks being synced.\n * @param attributeCursor When provided, describes a selection cursor falling within a\n * RichText field associated with a specific block and attribute.\n * Derived from the changes that produced the blocks.\n */\nexport function mergeCrdtBlocks(\n\tyblocks: YBlocks,\n\tincomingBlocks: Block[],\n\tattributeCursor: MergeCursorPosition\n): void {\n\t// Ensure we are working with serializable block data.\n\tif ( ! serializableBlocksCache.has( incomingBlocks ) ) {\n\t\tserializableBlocksCache.set(\n\t\t\tincomingBlocks,\n\t\t\tmakeBlocksSerializable( incomingBlocks )\n\t\t);\n\t}\n\n\tconst incomingBlocksToSync =\n\t\tserializableBlocksCache.get( incomingBlocks ) ?? [];\n\n\t// This is a rudimentary diff implementation similar to the y-prosemirror diffing\n\t// approach.\n\t// A better implementation would also diff the textual content and represent it\n\t// using a Y.Text type.\n\t// However, at this time it makes more sense to keep this algorithm generic to\n\t// support all kinds of block types.\n\t// Ideally, we ensure that block data structure have a consistent data format.\n\t// E.g.:\n\t// - textual content (using rich-text formatting?) may always be stored under `block.text`\n\t// - local information that shouldn't be shared (e.g. clientId or isDragging) is stored under `block.private`\n\t//\n\t// @credit Kevin Jahns (dmonad)\n\t// @link https://github.com/WordPress/gutenberg/pull/68483\n\tconst numOfCommonEntries = Math.min(\n\t\tincomingBlocksToSync.length ?? 0,\n\t\tyblocks.length\n\t);\n\n\tlet left = 0;\n\tlet right = 0;\n\n\t// skip equal blocks from left\n\tfor (\n\t\t;\n\t\tleft < numOfCommonEntries &&\n\t\tareBlocksEqual( incomingBlocksToSync[ left ], yblocks.get( left ) );\n\t\tleft++\n\t) {\n\t\t/* nop */\n\t}\n\n\t// skip equal blocks from right\n\tfor (\n\t\t;\n\t\tright < numOfCommonEntries - left &&\n\t\tareBlocksEqual(\n\t\t\tincomingBlocksToSync[ incomingBlocksToSync.length - right - 1 ],\n\t\t\tyblocks.get( yblocks.length - right - 1 )\n\t\t);\n\t\tright++\n\t) {\n\t\t/* nop */\n\t}\n\n\tconst numOfUpdatesNeeded = numOfCommonEntries - left - right;\n\tconst numOfInsertionsNeeded = Math.max(\n\t\t0,\n\t\tincomingBlocksToSync.length - yblocks.length\n\t);\n\tconst numOfDeletionsNeeded = Math.max(\n\t\t0,\n\t\tyblocks.length - incomingBlocksToSync.length\n\t);\n\n\t// updates\n\tfor ( let i = 0; i < numOfUpdatesNeeded; i++, left++ ) {\n\t\tconst incomingYBlock = incomingBlocksToSync[ left ];\n\t\tconst localYBlock = yblocks.get( left );\n\n\t\tObject.entries( incomingYBlock ).forEach(\n\t\t\t( [ incomingBlockProperty, incomingBlockPropertyValue ] ) => {\n\t\t\t\tswitch ( incomingBlockProperty ) {\n\t\t\t\t\tcase 'attributes': {\n\t\t\t\t\t\tconst localAttributes = localYBlock.get(\n\t\t\t\t\t\t\tincomingBlockProperty\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst incomingAttributes = incomingBlockPropertyValue;\n\n\t\t\t\t\t\t// When the local block has no attributes, adopt the incoming set.\n\t\t\t\t\t\tif ( ! localAttributes ) {\n\t\t\t\t\t\t\tlocalYBlock.set(\n\t\t\t\t\t\t\t\tincomingBlockProperty,\n\t\t\t\t\t\t\t\tcreateNewYAttributeMap(\n\t\t\t\t\t\t\t\t\tincomingYBlock.name,\n\t\t\t\t\t\t\t\t\tincomingAttributes\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Otherwise the attributes need to be merged.\n\t\t\t\t\t\tObject.entries( incomingAttributes ).forEach(\n\t\t\t\t\t\t\t( [\n\t\t\t\t\t\t\t\tincomingAttributeName,\n\t\t\t\t\t\t\t\tincomingAttributeValue,\n\t\t\t\t\t\t\t] ) => {\n\t\t\t\t\t\t\t\tconst currentAttribute = localAttributes?.get(\n\t\t\t\t\t\t\t\t\tincomingAttributeName\n\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\tconst isExpectedType = isExpectedAttributeType(\n\t\t\t\t\t\t\t\t\tincomingYBlock.name,\n\t\t\t\t\t\t\t\t\tincomingAttributeName,\n\t\t\t\t\t\t\t\t\tcurrentAttribute\n\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t// Y types (Y.Text, Y.Array, Y.Map) cannot be\n\t\t\t\t\t\t\t\t// compared with fastDeepEqual against plain values.\n\t\t\t\t\t\t\t\t// Delegate to mergeYValue which handles no-op\n\t\t\t\t\t\t\t\t// detection at the edges.\n\t\t\t\t\t\t\t\tconst isYType =\n\t\t\t\t\t\t\t\t\tcurrentAttribute instanceof Y.AbstractType;\n\n\t\t\t\t\t\t\t\tconst isAttributeChanged =\n\t\t\t\t\t\t\t\t\t! isExpectedType ||\n\t\t\t\t\t\t\t\t\tisYType ||\n\t\t\t\t\t\t\t\t\t! fastDeepEqual(\n\t\t\t\t\t\t\t\t\t\tcurrentAttribute,\n\t\t\t\t\t\t\t\t\t\tincomingAttributeValue\n\t\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\tif ( isAttributeChanged ) {\n\t\t\t\t\t\t\t\t\tupdateYBlockAttribute(\n\t\t\t\t\t\t\t\t\t\tincomingYBlock.name,\n\t\t\t\t\t\t\t\t\t\tincomingYBlock.clientId,\n\t\t\t\t\t\t\t\t\t\tincomingAttributeName,\n\t\t\t\t\t\t\t\t\t\tincomingAttributeValue,\n\t\t\t\t\t\t\t\t\t\tlocalAttributes,\n\t\t\t\t\t\t\t\t\t\tattributeCursor\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Delete any attributes that are no longer present.\n\t\t\t\t\t\tlocalAttributes.forEach(\n\t\t\t\t\t\t\t( _attrValue: unknown, attrName: string ) => {\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t! incomingBlockPropertyValue.hasOwnProperty(\n\t\t\t\t\t\t\t\t\t\tattrName\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\tlocalAttributes.delete( attrName );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase 'innerBlocks': {\n\t\t\t\t\t\t// Recursively merge innerBlocks\n\t\t\t\t\t\tlet yInnerBlocks = localYBlock.get(\n\t\t\t\t\t\t\tincomingBlockProperty\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tif ( ! ( yInnerBlocks instanceof Y.Array ) ) {\n\t\t\t\t\t\t\tyInnerBlocks = new Y.Array< YBlock >();\n\t\t\t\t\t\t\tlocalYBlock.set(\n\t\t\t\t\t\t\t\tincomingBlockProperty,\n\t\t\t\t\t\t\t\tyInnerBlocks\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmergeCrdtBlocks(\n\t\t\t\t\t\t\tyInnerBlocks,\n\t\t\t\t\t\t\tincomingBlockPropertyValue ?? [],\n\t\t\t\t\t\t\tattributeCursor\n\t\t\t\t\t\t);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t! fastDeepEqual(\n\t\t\t\t\t\t\t\tincomingYBlock[ incomingBlockProperty ],\n\t\t\t\t\t\t\t\tlocalYBlock.get( incomingBlockProperty )\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tlocalYBlock.set(\n\t\t\t\t\t\t\t\tincomingBlockProperty,\n\t\t\t\t\t\t\t\tincomingBlockPropertyValue\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t\tlocalYBlock.forEach( ( _v, k ) => {\n\t\t\tif ( ! incomingYBlock.hasOwnProperty( k ) ) {\n\t\t\t\tlocalYBlock.delete( k );\n\t\t\t}\n\t\t} );\n\t}\n\n\t// deletes\n\tyblocks.delete( left, numOfDeletionsNeeded );\n\n\t// inserts\n\tfor ( let i = 0; i < numOfInsertionsNeeded; i++, left++ ) {\n\t\tconst newBlock = [ createNewYBlock( incomingBlocksToSync[ left ] ) ];\n\n\t\tyblocks.insert( left, newBlock );\n\t}\n\n\t// remove duplicate clientids\n\tconst knownClientIds = new Set< string >();\n\tfor ( let j = 0; j < yblocks.length; j++ ) {\n\t\tconst yblock: YBlock = yblocks.get( j );\n\n\t\tlet clientId = yblock.get( 'clientId' );\n\n\t\tif ( ! clientId ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif ( knownClientIds.has( clientId ) ) {\n\t\t\tclientId = uuidv4();\n\t\t\tyblock.set( 'clientId', clientId );\n\t\t}\n\t\tknownClientIds.add( clientId );\n\t}\n}\n\n/**\n * Compare a plain array element against a Y.Map element for equality.\n * Used by the left-right sweep diff in mergeYArray.\n *\n * @param newElement The plain object from the incoming array.\n * @param yElement The Y.Map element from the existing Y.Array.\n * @return True if the elements are deeply equal.\n */\nfunction areArrayElementsEqual(\n\tnewElement: unknown,\n\tyElement: unknown\n): boolean {\n\tif ( yElement instanceof Y.Map && isRecord( newElement ) ) {\n\t\treturn fastDeepEqual( newElement, yElement.toJSON() );\n\t}\n\n\treturn fastDeepEqual( newElement, yElement );\n}\n\n/**\n * Merge an incoming plain array into an existing Y.Array in-place.\n *\n * Uses the same left-right sweep diff approach as mergeCrdtBlocks:\n * equal elements are skipped from both ends, then the middle section\n * is updated, deleted, or inserted as needed. This preserves existing\n * Y.Map/Y.Text objects for unchanged elements, so concurrent edits\n * to those elements are not lost.\n *\n * @param yArray The existing Y.Array to update.\n * @param newValue The new plain array to merge into the Y.Array.\n * @param schema The attribute schema (must have `query`).\n * @param cursorPosition The local cursor position for rich-text delta merges.\n * @param cursorScope The selected block attribute scope for rich-text cursor hints.\n */\nfunction mergeYArray(\n\tyArray: Y.Array< unknown >,\n\tnewValue: unknown[],\n\tschema: BlockAttributeSchema,\n\tcursorPosition: MergeCursorPosition,\n\tcursorScope: RichTextCursorScope\n): void {\n\tif ( ! schema.query ) {\n\t\treturn;\n\t}\n\n\tconst query = schema.query;\n\tconst numOfCommonEntries = Math.min( newValue.length, yArray.length );\n\n\tlet left = 0;\n\tlet right = 0;\n\n\t// Skip equal elements from left.\n\tfor (\n\t\t;\n\t\tleft < numOfCommonEntries &&\n\t\tareArrayElementsEqual( newValue[ left ], yArray.get( left ) );\n\t\tleft++\n\t) {\n\t\t/* nop */\n\t}\n\n\t// Skip equal elements from right.\n\tfor (\n\t\t;\n\t\tright < numOfCommonEntries - left &&\n\t\tareArrayElementsEqual(\n\t\t\tnewValue[ newValue.length - right - 1 ],\n\t\t\tyArray.get( yArray.length - right - 1 )\n\t\t);\n\t\tright++\n\t) {\n\t\t/* nop */\n\t}\n\n\t// Updates: merge changed elements in-place.\n\tconst numOfUpdatesNeeded = numOfCommonEntries - left - right;\n\n\tfor ( let i = 0; i < numOfUpdatesNeeded; i++ ) {\n\t\tconst currentElement = yArray.get( left + i );\n\t\tconst newElement = newValue[ left + i ];\n\n\t\tif ( currentElement instanceof Y.Map && isRecord( newElement ) ) {\n\t\t\tmergeYMapValues(\n\t\t\t\tcurrentElement,\n\t\t\t\tnewElement,\n\t\t\t\tquery,\n\t\t\t\tcursorPosition,\n\t\t\t\tcursorScope\n\t\t\t);\n\t\t} else {\n\t\t\t// Element is the wrong type (e.g. partial migration) or the\n\t\t\t// incoming value is not an object. Rebuild the entire array.\n\t\t\tyArray.delete( 0, yArray.length );\n\t\t\tyArray.insert(\n\t\t\t\t0,\n\t\t\t\tnewValue.map( ( item ) => createYMapFromQuery( query, item ) )\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t}\n\n\t// Deletes.\n\tconst numOfDeletionsNeeded = Math.max( 0, yArray.length - newValue.length );\n\n\tif ( numOfDeletionsNeeded > 0 ) {\n\t\tyArray.delete( left + numOfUpdatesNeeded, numOfDeletionsNeeded );\n\t}\n\n\t// Inserts.\n\tconst numOfInsertionsNeeded = Math.max(\n\t\t0,\n\t\tnewValue.length - yArray.length\n\t);\n\n\tif ( numOfInsertionsNeeded > 0 ) {\n\t\tconst insertAt = left + numOfUpdatesNeeded;\n\t\tconst itemsToInsert: Y.Map< unknown >[] = new Array(\n\t\t\tnumOfInsertionsNeeded\n\t\t);\n\n\t\tfor ( let i = 0; i < numOfInsertionsNeeded; i++ ) {\n\t\t\titemsToInsert[ i ] = createYMapFromQuery(\n\t\t\t\tquery,\n\t\t\t\tnewValue[ insertAt + i ]\n\t\t\t);\n\t\t}\n\n\t\tyArray.insert( insertAt, itemsToInsert );\n\t}\n}\n\n/**\n * Merge a single value into a Y.Map entry, using the attribute schema to\n * decide how to merge.\n *\n * If the current value is already a matching Y.js type (Y.Text, Y.Array,\n * Y.Map), the update is merged in-place so concurrent edits are preserved.\n * Otherwise the value is replaced wholesale.\n *\n * @param schema The attribute type definition for this value.\n * @param newVal The new value to merge into the Y.Map entry.\n * @param yMap The Y.Map that owns this entry.\n * @param key The key of this entry in the Y.Map.\n * @param cursorPosition The cursor position for rich-text delta merges from the updated value.\n * @param cursorScope Indicates a specific block and attribute associated with the editor;\n * determines whether the cursor should be updated based on the change.\n */\nfunction mergeYValue(\n\tschema: BlockAttributeSchema | undefined,\n\tnewVal: unknown,\n\tyMap: Y.Map< unknown >,\n\tkey: string,\n\tcursorPosition: MergeCursorPosition,\n\tcursorScope: RichTextCursorScope\n): void {\n\tconst currentVal = yMap.get( key );\n\tif (\n\t\tschema?.type === 'rich-text' &&\n\t\ttypeof newVal === 'string' &&\n\t\tcurrentVal instanceof Y.Text\n\t) {\n\t\tmergeRichTextUpdate(\n\t\t\tcurrentVal,\n\t\t\tnewVal,\n\t\t\tresolveRichTextCursorPosition( cursorPosition, cursorScope, newVal )\n\t\t);\n\t} else if (\n\t\tschema?.type === 'array' &&\n\t\tschema.query &&\n\t\tArray.isArray( newVal ) &&\n\t\tcurrentVal instanceof Y.Array\n\t) {\n\t\tmergeYArray( currentVal, newVal, schema, cursorPosition, cursorScope );\n\t} else if (\n\t\tschema?.type === 'object' &&\n\t\tschema.query &&\n\t\tisRecord( newVal ) &&\n\t\tcurrentVal instanceof Y.Map\n\t) {\n\t\tmergeYMapValues(\n\t\t\tcurrentVal,\n\t\t\tnewVal,\n\t\t\tschema.query,\n\t\t\tcursorPosition,\n\t\t\tcursorScope\n\t\t);\n\t} else {\n\t\tconst newYValue = createYValueFromSchema( schema, newVal );\n\n\t\t// If createYValueFromSchema wrapped the value into a Y type, the\n\t\t// current value is the wrong type and needs upgrading. Otherwise,\n\t\t// only replace if the raw value actually changed.\n\t\tif ( newYValue !== newVal || ! fastDeepEqual( currentVal, newVal ) ) {\n\t\t\tyMap.set( key, newYValue );\n\t\t}\n\t}\n}\n\n/**\n * Merge an incoming plain object into an existing Y.Map in-place, using\n * the query schema to decide how each property should be merged.\n *\n * Properties present in the Y.Map but absent from `newObj` are deleted.\n *\n * @param yMap The existing Y.Map to update.\n * @param newObj The new plain object to merge into the Y.Map.\n * @param query The query schema defining property types.\n * @param cursorPosition The local cursor position for rich-text delta merges.\n * @param cursorScope The selected block attribute scope for rich-text cursor hints.\n */\nfunction mergeYMapValues(\n\tyMap: Y.Map< unknown >,\n\tnewObj: Record< string, unknown >,\n\tquery: Record< string, BlockAttributeSchema >,\n\tcursorPosition: MergeCursorPosition,\n\tcursorScope: RichTextCursorScope\n): void {\n\tfor ( const [ key, newVal ] of Object.entries( newObj ) ) {\n\t\tmergeYValue(\n\t\t\tquery[ key ],\n\t\t\tnewVal,\n\t\t\tyMap,\n\t\t\tkey,\n\t\t\tcursorPosition,\n\t\t\tcursorScope\n\t\t);\n\t}\n\n\t// Delete properties absent from the incoming object.\n\tfor ( const key of yMap.keys() ) {\n\t\tif ( ! Object.hasOwn( newObj, key ) ) {\n\t\t\tyMap.delete( key );\n\t\t}\n\t}\n}\n\n/**\n * Update a single attribute on a Yjs block attributes map (currentAttributes).\n *\n * @param blockName The block type name, e.g. 'core/paragraph'.\n * @param clientId The local clientId for the block being merged.\n * @param attributeName The name of the attribute to update, e.g. 'content'.\n * @param attributeValue The new value for the attribute.\n * @param currentAttributes The Y.Map holding the block's current attributes.\n * @param newCursorPosition The cursor position for rich-text delta merges from the updated value.\n * Notably, this may not correspond to the attribute being edited and is\n * used to determine if any cursors need shifting in response to the change.\n */\nfunction updateYBlockAttribute(\n\tblockName: string,\n\tclientId: string | undefined,\n\tattributeName: string,\n\tattributeValue: unknown,\n\tcurrentAttributes: YBlockAttributes,\n\tnewCursorPosition: MergeCursorPosition\n): void {\n\tconst schema = getBlockAttributeSchema( blockName, attributeName );\n\n\t/*\n\t * @todo There is a slight discrepancy between the attribute name and key, which might\n\t * show up when working with multiline RichText instances (of which there are no\n\t * more within Core). For those instances, a cursor might never be updated in\n\t * response to changes because its `attributeKey` won\u2019t match any of the block\u2019s\n\t * attribute names, and since updating this attribute is based on the block names,\n\t * no suitable key for the cursor scope will be created. To fix, the updating code\n\t * would need to parse multiline attributes and infer the `attributeKey` being updated.\n\t */\n\tmergeYValue(\n\t\tschema,\n\t\tattributeValue,\n\t\tcurrentAttributes,\n\t\tattributeName,\n\t\tnewCursorPosition,\n\t\t{ attributeKey: attributeName, clientId }\n\t);\n}\n\n/**\n * References the specific block and attribute associated with a RichText component.\n *\n * This is used to associate a cursor with the attribute it\u2019s editing.\n *\n * @see WPBlockSelection\n */\ninterface RichTextCursorScope {\n\tattributeKey: string;\n\tclientId: string | undefined;\n}\n\ninterface DeltaWithOps {\n\tops: Parameters< Y.Text[ 'applyDelta' ] >[ 0 ];\n}\n\n/**\n * When the provided cursor falls within the given block and attribute\u2019s scope,\n * returns an index into the RichText\u2019s serialized HTML where the cursor falls.\n *\n * The cursor scope constrains resolution to ensure that indices are only reported\n * when a cursor falls within the block and attribute being updated, since the\n * attributes being updated may not always be the ones where a cursor presently falls.\n *\n * Returned index measures JS string lengths, thus is counted in UTF-16 code units\n * and contains the syntax characters making up HTML tags, comments, character\n * references, and other non-plaintext content.\n *\n * @param cursorPosition Description of the cursor in the new value.\n * @param cursorScope Cursors should only be updated if they fall within this\n * specific block and attribute.\n * @param updatedValue New RichText value potentially containing cursor.\n * @return String length into serialized HTML for RichText instance where cursor falls.\n */\nfunction resolveRichTextCursorPosition(\n\tcursorPosition: MergeCursorPosition,\n\tcursorScope: RichTextCursorScope,\n\tupdatedValue: string\n): HtmlStringIndex | null {\n\treturn cursorPosition &&\n\t\tcursorPosition.clientId === cursorScope.clientId &&\n\t\tcursorPosition.attributeKey === cursorScope.attributeKey &&\n\t\t'number' === typeof cursorPosition.offset &&\n\t\tNumber.isInteger( cursorPosition.offset )\n\t\t? richTextOffsetToHtmlIndex(\n\t\t\t\tupdatedValue,\n\t\t\t\tasRichTextOffset( cursorPosition.offset )\n\t\t )\n\t\t: null;\n}\n\n// Cached block attribute types, populated once from getBlockTypes().\nlet cachedBlockAttributeSchemas: Map<\n\tstring,\n\tMap< string, BlockAttributeSchema >\n>;\n\n/**\n * Get the attribute type definition for a block attribute.\n *\n * @param blockName The name of the block, e.g. 'core/paragraph'.\n * @param attributeName The name of the attribute, e.g. 'content'.\n * @return The type definition of the attribute.\n */\nfunction getBlockAttributeSchema(\n\tblockName: string,\n\tattributeName: string\n): BlockAttributeSchema | undefined {\n\tif ( ! cachedBlockAttributeSchemas ) {\n\t\t// Parse the attributes for all blocks once.\n\t\tcachedBlockAttributeSchemas = new Map();\n\n\t\tfor ( const blockType of getBlockTypes() as BlockType[] ) {\n\t\t\tcachedBlockAttributeSchemas.set(\n\t\t\t\tblockType.name,\n\t\t\t\tnew Map< string, BlockAttributeSchema >(\n\t\t\t\t\tObject.entries( blockType.attributes ?? {} ).map(\n\t\t\t\t\t\t( [ name, definition ] ) => {\n\t\t\t\t\t\t\tconst { role, type, query } = definition;\n\t\t\t\t\t\t\treturn [ name, { role, type, query } ];\n\t\t\t\t\t\t}\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n\n\treturn cachedBlockAttributeSchemas.get( blockName )?.get( attributeName );\n}\n\n/**\n * Check if an attribute value is the expected type.\n *\n * @param blockName The name of the block, e.g. 'core/paragraph'.\n * @param attributeName The name of the attribute, e.g. 'content'.\n * @param attributeValue The current attribute value.\n * @return True if the attribute type is expected, false otherwise.\n */\nfunction isExpectedAttributeType(\n\tblockName: string,\n\tattributeName: string,\n\tattributeValue: unknown\n): boolean {\n\tconst schema = getBlockAttributeSchema( blockName, attributeName );\n\n\tif ( schema?.type === 'rich-text' ) {\n\t\treturn attributeValue instanceof Y.Text;\n\t}\n\n\tif ( schema?.type === 'string' ) {\n\t\treturn typeof attributeValue === 'string';\n\t}\n\n\tif ( schema?.type === 'array' && schema.query ) {\n\t\treturn attributeValue instanceof Y.Array;\n\t}\n\n\tif ( schema?.type === 'object' && schema.query ) {\n\t\treturn attributeValue instanceof Y.Map;\n\t}\n\n\treturn true;\n}\n\n/**\n * Given a block name and attribute key, return true if the attribute is local\n * and should not be synced.\n *\n * @param blockName The name of the block, e.g. 'core/image'.\n * @param attributeName The name of the attribute to check, e.g. 'blob'.\n * @return True if the attribute is local, false otherwise.\n */\nfunction isLocalAttribute( blockName: string, attributeName: string ): boolean {\n\treturn (\n\t\t'local' === getBlockAttributeSchema( blockName, attributeName )?.role\n\t);\n}\n\nlet localDoc: Y.Doc;\n\n/**\n * Given a Y.Text object and an updated string value, diff the new value and\n * apply the delta to the Y.Text.\n *\n * @param blockYText The Y.Text to update.\n * @param updatedValue The updated value.\n * @param htmlCursorIndex The cursor index in the updated HTML string.\n */\nexport function mergeRichTextUpdate(\n\tblockYText: Y.Text,\n\tupdatedValue: string,\n\thtmlCursorIndex: HtmlStringIndex | null = null\n): void {\n\t// Gutenberg does not use Yjs shared types natively, so we can only subscribe\n\t// to changes from store and apply them to Yjs types that we create and\n\t// manage. Crucially, for rich-text attributes, we do not receive granular\n\t// string updates; we get the new full string value on each change, even when\n\t// only a single character changed.\n\t//\n\t// The code below allows us to compute a delta between the current and new\n\t// value, then apply it to the Y.Text.\n\n\tconst currentValueAsDelta = new Delta( blockYText.toDelta() );\n\tconst updatedValueAsDelta = new Delta( [ { insert: updatedValue } ] );\n\tconst deltaDiff = currentValueAsDelta.diffWithCursor(\n\t\tupdatedValueAsDelta,\n\t\thtmlCursorIndex\n\t);\n\n\t/**\n\t * When there is no cursor involved, or when the diff is able to shuffle properly\n\t * around the cursor then apply that already-computed diff.\n\t *\n\t * However, `diffWithCursor()` currently fails in certain cases, producing corrupted\n\t * output. In these cases, fall back to the raw diff as that will apply cleanly,\n\t * even if it provides a less meaningful diff.\n\t *\n\t * @see Delta.diffWithCursor()\n\t */\n\tconst safeDiff =\n\t\thtmlCursorIndex === null ||\n\t\tisDeltaVerificationMatch( blockYText, deltaDiff, updatedValue )\n\t\t\t? deltaDiff\n\t\t\t: currentValueAsDelta.diff( updatedValueAsDelta );\n\n\tblockYText.applyDelta( safeDiff.ops );\n}\n\n/**\n * Verify that applying a delta to an existing Y.Text object produces the expected\n * output string.\n *\n * A stale, mis-scoped, or corrupted Delta will mutate a text value to the wrong\n * output string. This function applies the given Delta and indicates whether it\n * produces the given expected output string value.\n *\n * @param blockYText The current Y.Text before applying the candidate delta.\n * @param delta The candidate delta.\n * @param expectedValue The exact string expected after applying the delta.\n * @return Whether the candidate delta produces the expected value.\n */\nfunction isDeltaVerificationMatch(\n\tblockYText: Y.Text,\n\tdelta: DeltaWithOps,\n\texpectedValue: string\n): boolean {\n\tif ( ! localDoc ) {\n\t\t// Y.Text must be attached to a Y.Doc to be able to do operations on it.\n\t\t// Create a temporary Y.Text attached to a local Y.Doc for delta computation.\n\t\t// This is an optimization to avoid creating a new Y.Doc on every update.\n\t\tlocalDoc = new Y.Doc();\n\t}\n\n\tconst verificationYText = localDoc.getText( 'verification-text' );\n\n\t// Because this is global, it must be cleared before using.\n\tverificationYText.delete( 0, verificationYText.length );\n\tverificationYText.insert( 0, blockYText.toString() );\n\tverificationYText.applyDelta( delta.ops );\n\n\treturn verificationYText.toString() === expectedValue;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * External dependencies\n */\nimport { v4 as uuidv4 } from 'uuid';\nimport fastDeepEqual from 'fast-deep-equal/es6/index.js';\n\n/**\n * WordPress dependencies\n */\nimport { getBlockTypes } from '@wordpress/blocks';\nimport { RichTextData } from '@wordpress/rich-text';\nimport { Y } from '@wordpress/sync';\n\n/**\n * Internal dependencies\n */\nimport {\n\tasRichTextOffset,\n\tcreateYMap,\n\trichTextOffsetToHtmlIndex,\n\ttype HtmlStringIndex,\n\ttype YMapRecord,\n\ttype YMapWrap,\n} from './crdt-utils';\nimport { getCachedRichTextData } from './crdt-text';\nimport { Delta } from '../sync';\nimport { type WPBlockSelection } from '../types';\n\ninterface BlockAttributes {\n\t[ key: string ]: unknown;\n}\n\ninterface BlockAttributeSchema {\n\trole?: string;\n\ttype?: string;\n\tquery?: Record< string, BlockAttributeSchema >;\n}\n\ninterface BlockType {\n\tattributes?: Record< string, BlockAttributeSchema >;\n\tname: string;\n}\n\n// A block as represented in Gutenberg's data store.\nexport interface Block {\n\tattributes: BlockAttributes;\n\tclientId?: string;\n\tinnerBlocks: Block[];\n\tisValid?: boolean;\n\tname: string;\n\toriginalContent?: string;\n\tvalidationIssues?: string[]; // unserializable\n}\n\n// A block as represented in the CRDT document (Y.Map).\nexport interface YBlockRecord extends YMapRecord {\n\tattributes: YBlockAttributes;\n\tclientId: string;\n\tinnerBlocks: YBlocks;\n\tisValid?: boolean;\n\toriginalContent?: string;\n\tname: string;\n}\n\nexport type YBlock = YMapWrap< YBlockRecord >;\nexport type YBlocks = Y.Array< YBlock >;\n\n// Block attribute schema cannot be known at compile time, so we use Y.Map.\n// Attribute values will be typed as the union of `Y.Text` and `unknown`.\nexport type YBlockAttributes = Y.Map< Y.Text | unknown >;\n\n/**\n * Optional description of where a cursor falls.\n *\n * Used to coordinate shifting of cursor when applying changes\n * to a Y.Doc with RichText instances.\n */\nexport type MergeCursorPosition = WPBlockSelection | null;\n\nconst serializableBlocksCache = new WeakMap< WeakKey, Block[] >();\n\n/**\n * Recursively walk an attribute value and convert any RichTextData instances\n * to their string (HTML) representation. This is necessary for array-type and\n * object-type attributes, which can contain nested RichTextData.\n *\n * @param value The attribute value to serialize.\n * @return The value with all RichTextData instances replaced by strings.\n */\nfunction serializeAttributeValue( value: unknown ): unknown {\n\tif ( value instanceof RichTextData ) {\n\t\treturn value.valueOf();\n\t}\n\n\t// e.g. core/table `body`: [ { cells: [ { content: RichTextData } ] } ]\n\tif ( Array.isArray( value ) ) {\n\t\treturn value.map( serializeAttributeValue );\n\t}\n\n\t// e.g. a single row inside core/table `body`: { cells: [ ... ] }\n\tif ( value && typeof value === 'object' ) {\n\t\tconst result: Record< string, unknown > = {};\n\n\t\tfor ( const [ k, v ] of Object.entries( value ) ) {\n\t\t\tresult[ k ] = serializeAttributeValue( v );\n\t\t}\n\t\treturn result;\n\t}\n\n\treturn value;\n}\n\nfunction makeBlockAttributesSerializable(\n\tblockName: string,\n\tattributes: BlockAttributes\n): BlockAttributes {\n\tconst newAttributes = { ...attributes };\n\tfor ( const [ key, value ] of Object.entries( attributes ) ) {\n\t\tif ( isLocalAttribute( blockName, key ) ) {\n\t\t\tdelete newAttributes[ key ];\n\t\t\tcontinue;\n\t\t}\n\n\t\tnewAttributes[ key ] = serializeAttributeValue( value );\n\t}\n\treturn newAttributes;\n}\n\n/**\n * Recursively removes properties which cannot be serialized from a list of block objects.\n *\n * @param blocks Eemove unserializable properties from each block object in this set.\n * @return Copies of the provided blocks without the unserializable properties.\n */\nfunction makeBlocksSerializable( blocks: Block[] ): Block[] {\n\treturn blocks.map( ( block: Block ) => {\n\t\tconst {\n\t\t\tname,\n\t\t\tinnerBlocks,\n\t\t\tattributes,\n\t\t\t/*\n\t\t\t * Any validation issues discovered when loading a block are appended\n\t\t\t * to the block node with a logging function, which cannot be serialized.\n\t\t\t *\n\t\t\t * @see import(\"@wordpress/blocks/src/api/parser\").parseRawBlock()\n\t\t\t */\n\t\t\tvalidationIssues,\n\t\t\t...rest\n\t\t} = block;\n\n\t\treturn {\n\t\t\t...rest,\n\t\t\tname,\n\t\t\tattributes: makeBlockAttributesSerializable( name, attributes ),\n\t\t\tinnerBlocks: makeBlocksSerializable( innerBlocks ),\n\t\t};\n\t} );\n}\n\n/**\n * Recursively walk an attribute value and convert any strings that correspond\n * to rich-text schema nodes into RichTextData instances. This is the inverse\n * of serializeAttributeValue and handles nested structures like table cells.\n *\n * @param schema The attribute type definition for this value.\n * @param value The attribute value from CRDT (toJSON).\n * @return The value with rich-text strings replaced by RichTextData.\n */\nfunction deserializeAttributeValue(\n\tschema: BlockAttributeSchema | undefined,\n\tvalue: unknown\n): unknown {\n\tif ( schema?.type === 'rich-text' && typeof value === 'string' ) {\n\t\treturn getCachedRichTextData( value );\n\t}\n\n\t// e.g. core/table `body`: [ { cells: [ { content: RichTextData } ] } ]\n\tif ( Array.isArray( value ) ) {\n\t\treturn value.map( ( item ) =>\n\t\t\tdeserializeAttributeValue( schema, item )\n\t\t);\n\t}\n\n\t// e.g. a single row inside core/table `body`: { cells: [ ... ] }\n\tif ( value && typeof value === 'object' ) {\n\t\tconst result: Record< string, unknown > = {};\n\n\t\tfor ( const [ key, innerValue ] of Object.entries(\n\t\t\tvalue as Record< string, unknown >\n\t\t) ) {\n\t\t\tresult[ key ] = deserializeAttributeValue(\n\t\t\t\tschema?.query?.[ key ],\n\t\t\t\tinnerValue\n\t\t\t);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\treturn value;\n}\n\n/**\n * Convert blocks from their CRDT-serialized form back to the runtime form\n * expected by the block editor. Rich-text attributes are stored as Y.Text in\n * the CRDT document, which serializes to plain strings via toJSON(). This\n * function restores them to RichTextData instances so that block edit\n * components that rely on RichTextData methods (e.g. `.text`) work correctly.\n *\n * @param blocks Blocks as extracted from the CRDT document via toJSON().\n * @return Blocks with rich-text attributes restored to RichTextData.\n */\nexport function deserializeBlockAttributes( blocks: Block[] ): Block[] {\n\treturn blocks.map( ( block: Block ) => {\n\t\tconst { name, innerBlocks, attributes, ...rest } = block;\n\n\t\tconst newAttributes = { ...attributes };\n\n\t\tfor ( const [ key, value ] of Object.entries( attributes ) ) {\n\t\t\tconst schema = getBlockAttributeSchema( name, key );\n\n\t\t\tif ( schema ) {\n\t\t\t\tnewAttributes[ key ] = deserializeAttributeValue(\n\t\t\t\t\tschema,\n\t\t\t\t\tvalue\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\t...rest,\n\t\t\tname,\n\t\t\tattributes: newAttributes,\n\t\t\tinnerBlocks: deserializeBlockAttributes( innerBlocks ?? [] ),\n\t\t};\n\t} );\n}\n\n/**\n * @param {any} gblock\n * @param {Y.Map} yblock\n */\nfunction areBlocksEqual( gblock: Block, yblock: YBlock ): boolean {\n\tconst yblockAsJson = yblock.toJSON();\n\n\t// we must not sync clientId, as this can't be generated consistently and\n\t// hence will lead to merge conflicts.\n\tconst overwrites = {\n\t\tinnerBlocks: null,\n\t\tclientId: null,\n\t};\n\tconst res = fastDeepEqual(\n\t\tObject.assign( {}, gblock, overwrites ),\n\t\tObject.assign( {}, yblockAsJson, overwrites )\n\t);\n\tconst inners = gblock.innerBlocks || [];\n\tconst yinners = yblock.get( 'innerBlocks' );\n\treturn (\n\t\tres &&\n\t\tinners.length === yinners?.length &&\n\t\tinners.every( ( block: Block, i: number ) =>\n\t\t\tareBlocksEqual( block, yinners.get( i ) )\n\t\t)\n\t);\n}\n\nfunction createNewYAttributeMap(\n\tblockName: string,\n\tattributes: BlockAttributes\n): YBlockAttributes {\n\treturn new Y.Map(\n\t\tObject.entries( attributes ).map(\n\t\t\t( [ attributeName, attributeValue ] ) => {\n\t\t\t\treturn [\n\t\t\t\t\tattributeName,\n\t\t\t\t\tcreateNewYAttributeValue(\n\t\t\t\t\t\tblockName,\n\t\t\t\t\t\tattributeName,\n\t\t\t\t\t\tattributeValue\n\t\t\t\t\t),\n\t\t\t\t];\n\t\t\t}\n\t\t)\n\t);\n}\n\nfunction createNewYAttributeValue(\n\tblockName: string,\n\tattributeName: string,\n\tattributeValue: unknown\n): Y.Text | Y.Array< unknown > | Y.Map< unknown > | unknown {\n\tconst schema = getBlockAttributeSchema( blockName, attributeName );\n\treturn createYValueFromSchema( schema, attributeValue );\n}\n\n/**\n * Recursively create the appropriate Y.js type for a value based on its\n * block-attribute schema.\n *\n * - `rich-text` -> Y.Text\n * - `array` with query -> Y.Array of Y.Maps\n * - `object` with query -> Y.Map\n * - anything else -> plain value (unchanged)\n *\n * @param schema The attribute type definition.\n * @param value The plain JS value to convert.\n * @return A Y.js type or the original value.\n */\nfunction createYValueFromSchema(\n\tschema: BlockAttributeSchema | undefined,\n\tvalue: unknown\n): Y.Text | Y.Array< unknown > | Y.Map< unknown > | unknown {\n\tif ( ! schema ) {\n\t\treturn value;\n\t}\n\n\tif ( schema.type === 'rich-text' ) {\n\t\treturn new Y.Text( value?.toString() ?? '' );\n\t}\n\n\tif ( schema.type === 'array' && schema.query && Array.isArray( value ) ) {\n\t\tconst query = schema.query;\n\t\tconst yArray = new Y.Array< Y.Map< unknown > >();\n\n\t\tyArray.insert(\n\t\t\t0,\n\t\t\tvalue.map( ( item ) => createYMapFromQuery( query, item ) )\n\t\t);\n\n\t\treturn yArray;\n\t}\n\n\tif ( schema.type === 'object' && schema.query && isRecord( value ) ) {\n\t\treturn createYMapFromQuery( schema.query, value );\n\t}\n\n\treturn value;\n}\n\n/**\n * Type guard that narrows `unknown` to `Record< string, unknown >`.\n *\n * @param value Value to check.\n * @return True if `value` is a non-null, non-array object.\n */\nfunction isRecord( value: unknown ): value is Record< string, unknown > {\n\treturn !! value && typeof value === 'object' && ! Array.isArray( value );\n}\n\n/**\n * Create a Y.Map from a plain object, using a query schema to decide which\n * properties should become nested Y.js types (Y.Text, Y.Array, Y.Map).\n *\n * @param query The query schema defining the properties.\n * @param obj The plain object to convert.\n * @return A Y.Map with typed values.\n */\nfunction createYMapFromQuery(\n\tquery: Record< string, BlockAttributeSchema >,\n\tobj: unknown\n): Y.Map< unknown > {\n\tif ( ! isRecord( obj ) ) {\n\t\treturn new Y.Map();\n\t}\n\n\tconst entries: [ string, unknown ][] = Object.entries( obj ).map(\n\t\t( [ key, val ] ): [ string, unknown ] => {\n\t\t\tconst subSchema = query[ key ];\n\t\t\treturn [ key, createYValueFromSchema( subSchema, val ) ];\n\t\t}\n\t);\n\n\treturn new Y.Map( entries );\n}\n\nfunction createNewYBlock( block: Block ): YBlock {\n\treturn createYMap< YBlockRecord >(\n\t\tObject.fromEntries(\n\t\t\tObject.entries( block ).map( ( [ key, value ] ) => {\n\t\t\t\tswitch ( key ) {\n\t\t\t\t\tcase 'attributes': {\n\t\t\t\t\t\treturn [\n\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\tcreateNewYAttributeMap( block.name, value ),\n\t\t\t\t\t\t];\n\t\t\t\t\t}\n\n\t\t\t\t\tcase 'innerBlocks': {\n\t\t\t\t\t\tconst innerBlocks = new Y.Array();\n\n\t\t\t\t\t\t// If not an array, set to empty Y.Array.\n\t\t\t\t\t\tif ( ! Array.isArray( value ) ) {\n\t\t\t\t\t\t\treturn [ key, innerBlocks ];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tinnerBlocks.insert(\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\tvalue.map( ( innerBlock: Block ) =>\n\t\t\t\t\t\t\t\tcreateNewYBlock( innerBlock )\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\treturn [ key, innerBlocks ];\n\t\t\t\t\t}\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn [ key, value ];\n\t\t\t\t}\n\t\t\t} )\n\t\t)\n\t);\n}\n\n/**\n * Merge incoming block data into the local Y.Doc.\n * This function is called to sync local block changes to a shared Y.Doc.\n *\n * @param yblocks The blocks in the local Y.Doc.\n * @param incomingBlocks Gutenberg blocks being synced.\n * @param attributeCursor When provided, describes a selection cursor falling within a\n * RichText field associated with a specific block and attribute.\n * Derived from the changes that produced the blocks.\n */\nexport function mergeCrdtBlocks(\n\tyblocks: YBlocks,\n\tincomingBlocks: Block[],\n\tattributeCursor: MergeCursorPosition\n): void {\n\t// Ensure we are working with serializable block data.\n\tif ( ! serializableBlocksCache.has( incomingBlocks ) ) {\n\t\tserializableBlocksCache.set(\n\t\t\tincomingBlocks,\n\t\t\tmakeBlocksSerializable( incomingBlocks )\n\t\t);\n\t}\n\n\tconst incomingBlocksToSync =\n\t\tserializableBlocksCache.get( incomingBlocks ) ?? [];\n\n\t// This is a rudimentary diff implementation similar to the y-prosemirror diffing\n\t// approach.\n\t// A better implementation would also diff the textual content and represent it\n\t// using a Y.Text type.\n\t// However, at this time it makes more sense to keep this algorithm generic to\n\t// support all kinds of block types.\n\t// Ideally, we ensure that block data structure have a consistent data format.\n\t// E.g.:\n\t// - textual content (using rich-text formatting?) may always be stored under `block.text`\n\t// - local information that shouldn't be shared (e.g. clientId or isDragging) is stored under `block.private`\n\t//\n\t// @credit Kevin Jahns (dmonad)\n\t// @link https://github.com/WordPress/gutenberg/pull/68483\n\tconst numOfCommonEntries = Math.min(\n\t\tincomingBlocksToSync.length ?? 0,\n\t\tyblocks.length\n\t);\n\n\tlet left = 0;\n\tlet right = 0;\n\n\t// skip equal blocks from left\n\tfor (\n\t\t;\n\t\tleft < numOfCommonEntries &&\n\t\tareBlocksEqual( incomingBlocksToSync[ left ], yblocks.get( left ) );\n\t\tleft++\n\t) {\n\t\t/* nop */\n\t}\n\n\t// skip equal blocks from right\n\tfor (\n\t\t;\n\t\tright < numOfCommonEntries - left &&\n\t\tareBlocksEqual(\n\t\t\tincomingBlocksToSync[ incomingBlocksToSync.length - right - 1 ],\n\t\t\tyblocks.get( yblocks.length - right - 1 )\n\t\t);\n\t\tright++\n\t) {\n\t\t/* nop */\n\t}\n\n\tconst numOfUpdatesNeeded = numOfCommonEntries - left - right;\n\tconst numOfInsertionsNeeded = Math.max(\n\t\t0,\n\t\tincomingBlocksToSync.length - yblocks.length\n\t);\n\tconst numOfDeletionsNeeded = Math.max(\n\t\t0,\n\t\tyblocks.length - incomingBlocksToSync.length\n\t);\n\n\t// updates\n\tfor ( let i = 0; i < numOfUpdatesNeeded; i++, left++ ) {\n\t\tconst incomingYBlock = incomingBlocksToSync[ left ];\n\t\tconst localYBlock = yblocks.get( left );\n\n\t\tObject.entries( incomingYBlock ).forEach(\n\t\t\t( [ incomingBlockProperty, incomingBlockPropertyValue ] ) => {\n\t\t\t\tswitch ( incomingBlockProperty ) {\n\t\t\t\t\tcase 'attributes': {\n\t\t\t\t\t\tconst localAttributes = localYBlock.get(\n\t\t\t\t\t\t\tincomingBlockProperty\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst incomingAttributes = incomingBlockPropertyValue;\n\n\t\t\t\t\t\t// When the local block has no attributes, adopt the incoming set.\n\t\t\t\t\t\tif ( ! localAttributes ) {\n\t\t\t\t\t\t\tlocalYBlock.set(\n\t\t\t\t\t\t\t\tincomingBlockProperty,\n\t\t\t\t\t\t\t\tcreateNewYAttributeMap(\n\t\t\t\t\t\t\t\t\tincomingYBlock.name,\n\t\t\t\t\t\t\t\t\tincomingAttributes\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Otherwise the attributes need to be merged.\n\t\t\t\t\t\tObject.entries( incomingAttributes ).forEach(\n\t\t\t\t\t\t\t( [\n\t\t\t\t\t\t\t\tincomingAttributeName,\n\t\t\t\t\t\t\t\tincomingAttributeValue,\n\t\t\t\t\t\t\t] ) => {\n\t\t\t\t\t\t\t\tconst currentAttribute = localAttributes?.get(\n\t\t\t\t\t\t\t\t\tincomingAttributeName\n\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\tconst isExpectedType = isExpectedAttributeType(\n\t\t\t\t\t\t\t\t\tincomingYBlock.name,\n\t\t\t\t\t\t\t\t\tincomingAttributeName,\n\t\t\t\t\t\t\t\t\tcurrentAttribute\n\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t// Y types (Y.Text, Y.Array, Y.Map) cannot be\n\t\t\t\t\t\t\t\t// compared with fastDeepEqual against plain values.\n\t\t\t\t\t\t\t\t// Delegate to mergeYValue which handles no-op\n\t\t\t\t\t\t\t\t// detection at the edges.\n\t\t\t\t\t\t\t\tconst isYType =\n\t\t\t\t\t\t\t\t\tcurrentAttribute instanceof Y.AbstractType;\n\n\t\t\t\t\t\t\t\tconst isAttributeChanged =\n\t\t\t\t\t\t\t\t\t! isExpectedType ||\n\t\t\t\t\t\t\t\t\tisYType ||\n\t\t\t\t\t\t\t\t\t! fastDeepEqual(\n\t\t\t\t\t\t\t\t\t\tcurrentAttribute,\n\t\t\t\t\t\t\t\t\t\tincomingAttributeValue\n\t\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\tif ( isAttributeChanged ) {\n\t\t\t\t\t\t\t\t\tupdateYBlockAttribute(\n\t\t\t\t\t\t\t\t\t\tincomingYBlock.name,\n\t\t\t\t\t\t\t\t\t\tincomingYBlock.clientId,\n\t\t\t\t\t\t\t\t\t\tincomingAttributeName,\n\t\t\t\t\t\t\t\t\t\tincomingAttributeValue,\n\t\t\t\t\t\t\t\t\t\tlocalAttributes,\n\t\t\t\t\t\t\t\t\t\tattributeCursor\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Delete any attributes that are no longer present.\n\t\t\t\t\t\tlocalAttributes.forEach(\n\t\t\t\t\t\t\t( _attrValue: unknown, attrName: string ) => {\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t! incomingBlockPropertyValue.hasOwnProperty(\n\t\t\t\t\t\t\t\t\t\tattrName\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\tlocalAttributes.delete( attrName );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase 'innerBlocks': {\n\t\t\t\t\t\t// Recursively merge innerBlocks\n\t\t\t\t\t\tlet yInnerBlocks = localYBlock.get(\n\t\t\t\t\t\t\tincomingBlockProperty\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tif ( ! ( yInnerBlocks instanceof Y.Array ) ) {\n\t\t\t\t\t\t\tyInnerBlocks = new Y.Array< YBlock >();\n\t\t\t\t\t\t\tlocalYBlock.set(\n\t\t\t\t\t\t\t\tincomingBlockProperty,\n\t\t\t\t\t\t\t\tyInnerBlocks\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmergeCrdtBlocks(\n\t\t\t\t\t\t\tyInnerBlocks,\n\t\t\t\t\t\t\tincomingBlockPropertyValue ?? [],\n\t\t\t\t\t\t\tattributeCursor\n\t\t\t\t\t\t);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase 'clientId': {\n\t\t\t\t\t\t// Never overwrite the local block's clientId with the\n\t\t\t\t\t\t// incoming one. Some callers (e.g. the Code Editor flow\n\t\t\t\t\t\t// that parses raw HTML into blocks on every keystroke)\n\t\t\t\t\t\t// produce randomized clientIds for blocks whose content\n\t\t\t\t\t\t// has changed on every sync. Without this case the default\n\t\t\t\t\t\t// branch would replace the stable Y.Doc clientId with\n\t\t\t\t\t\t// a new one, causing remote peers to remount the block\n\t\t\t\t\t\t// and flash the block's content on reload.\n\t\t\t\t\t\t//\n\t\t\t\t\t\t// This mirrors the clientId exclusion in `areBlocksEqual`.\n\t\t\t\t\t\t// Convergence is preserved. Because we're not writing\n\t\t\t\t\t\t// to the clientId, Yjs doesn't send an update to peers\n\t\t\t\t\t\t// telling them to change the clientId, so everyone\n\t\t\t\t\t\t// sees the same clientId per block.\n\t\t\t\t\t\t// Inserts still use a new clientId via createNewYBlock,\n\t\t\t\t\t\t// and the duplicate-clientId sweep below catches any\n\t\t\t\t\t\t// edge cases. The clientId is anchored to the\n\t\t\t\t\t\t// slot in the array rather than to specific content,\n\t\t\t\t\t\t// which is consistent with areBlocksEqual ignoring\n\t\t\t\t\t\t// clientId when diffing.\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t! fastDeepEqual(\n\t\t\t\t\t\t\t\tincomingYBlock[ incomingBlockProperty ],\n\t\t\t\t\t\t\t\tlocalYBlock.get( incomingBlockProperty )\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tlocalYBlock.set(\n\t\t\t\t\t\t\t\tincomingBlockProperty,\n\t\t\t\t\t\t\t\tincomingBlockPropertyValue\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t\tlocalYBlock.forEach( ( _v, k ) => {\n\t\t\tif ( ! incomingYBlock.hasOwnProperty( k ) ) {\n\t\t\t\tlocalYBlock.delete( k );\n\t\t\t}\n\t\t} );\n\t}\n\n\t// deletes\n\tyblocks.delete( left, numOfDeletionsNeeded );\n\n\t// inserts\n\tfor ( let i = 0; i < numOfInsertionsNeeded; i++, left++ ) {\n\t\tconst newBlock = [ createNewYBlock( incomingBlocksToSync[ left ] ) ];\n\n\t\tyblocks.insert( left, newBlock );\n\t}\n\n\t// remove duplicate clientids\n\tconst knownClientIds = new Set< string >();\n\tfor ( let j = 0; j < yblocks.length; j++ ) {\n\t\tconst yblock: YBlock = yblocks.get( j );\n\n\t\tlet clientId = yblock.get( 'clientId' );\n\n\t\tif ( ! clientId ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif ( knownClientIds.has( clientId ) ) {\n\t\t\tclientId = uuidv4();\n\t\t\tyblock.set( 'clientId', clientId );\n\t\t}\n\t\tknownClientIds.add( clientId );\n\t}\n}\n\n/**\n * Compare a plain array element against a Y.Map element for equality.\n * Used by the left-right sweep diff in mergeYArray.\n *\n * @param newElement The plain object from the incoming array.\n * @param yElement The Y.Map element from the existing Y.Array.\n * @return True if the elements are deeply equal.\n */\nfunction areArrayElementsEqual(\n\tnewElement: unknown,\n\tyElement: unknown\n): boolean {\n\tif ( yElement instanceof Y.Map && isRecord( newElement ) ) {\n\t\treturn fastDeepEqual( newElement, yElement.toJSON() );\n\t}\n\n\treturn fastDeepEqual( newElement, yElement );\n}\n\n/**\n * Merge an incoming plain array into an existing Y.Array in-place.\n *\n * Uses the same left-right sweep diff approach as mergeCrdtBlocks:\n * equal elements are skipped from both ends, then the middle section\n * is updated, deleted, or inserted as needed. This preserves existing\n * Y.Map/Y.Text objects for unchanged elements, so concurrent edits\n * to those elements are not lost.\n *\n * @param yArray The existing Y.Array to update.\n * @param newValue The new plain array to merge into the Y.Array.\n * @param schema The attribute schema (must have `query`).\n * @param cursorPosition The local cursor position for rich-text delta merges.\n * @param cursorScope The selected block attribute scope for rich-text cursor hints.\n */\nfunction mergeYArray(\n\tyArray: Y.Array< unknown >,\n\tnewValue: unknown[],\n\tschema: BlockAttributeSchema,\n\tcursorPosition: MergeCursorPosition,\n\tcursorScope: RichTextCursorScope\n): void {\n\tif ( ! schema.query ) {\n\t\treturn;\n\t}\n\n\tconst query = schema.query;\n\tconst numOfCommonEntries = Math.min( newValue.length, yArray.length );\n\n\tlet left = 0;\n\tlet right = 0;\n\n\t// Skip equal elements from left.\n\tfor (\n\t\t;\n\t\tleft < numOfCommonEntries &&\n\t\tareArrayElementsEqual( newValue[ left ], yArray.get( left ) );\n\t\tleft++\n\t) {\n\t\t/* nop */\n\t}\n\n\t// Skip equal elements from right.\n\tfor (\n\t\t;\n\t\tright < numOfCommonEntries - left &&\n\t\tareArrayElementsEqual(\n\t\t\tnewValue[ newValue.length - right - 1 ],\n\t\t\tyArray.get( yArray.length - right - 1 )\n\t\t);\n\t\tright++\n\t) {\n\t\t/* nop */\n\t}\n\n\t// Updates: merge changed elements in-place.\n\tconst numOfUpdatesNeeded = numOfCommonEntries - left - right;\n\n\tfor ( let i = 0; i < numOfUpdatesNeeded; i++ ) {\n\t\tconst currentElement = yArray.get( left + i );\n\t\tconst newElement = newValue[ left + i ];\n\n\t\tif ( currentElement instanceof Y.Map && isRecord( newElement ) ) {\n\t\t\tmergeYMapValues(\n\t\t\t\tcurrentElement,\n\t\t\t\tnewElement,\n\t\t\t\tquery,\n\t\t\t\tcursorPosition,\n\t\t\t\tcursorScope\n\t\t\t);\n\t\t} else {\n\t\t\t// Element is the wrong type (e.g. partial migration) or the\n\t\t\t// incoming value is not an object. Rebuild the entire array.\n\t\t\tyArray.delete( 0, yArray.length );\n\t\t\tyArray.insert(\n\t\t\t\t0,\n\t\t\t\tnewValue.map( ( item ) => createYMapFromQuery( query, item ) )\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t}\n\n\t// Deletes.\n\tconst numOfDeletionsNeeded = Math.max( 0, yArray.length - newValue.length );\n\n\tif ( numOfDeletionsNeeded > 0 ) {\n\t\tyArray.delete( left + numOfUpdatesNeeded, numOfDeletionsNeeded );\n\t}\n\n\t// Inserts.\n\tconst numOfInsertionsNeeded = Math.max(\n\t\t0,\n\t\tnewValue.length - yArray.length\n\t);\n\n\tif ( numOfInsertionsNeeded > 0 ) {\n\t\tconst insertAt = left + numOfUpdatesNeeded;\n\t\tconst itemsToInsert: Y.Map< unknown >[] = new Array(\n\t\t\tnumOfInsertionsNeeded\n\t\t);\n\n\t\tfor ( let i = 0; i < numOfInsertionsNeeded; i++ ) {\n\t\t\titemsToInsert[ i ] = createYMapFromQuery(\n\t\t\t\tquery,\n\t\t\t\tnewValue[ insertAt + i ]\n\t\t\t);\n\t\t}\n\n\t\tyArray.insert( insertAt, itemsToInsert );\n\t}\n}\n\n/**\n * Merge a single value into a Y.Map entry, using the attribute schema to\n * decide how to merge.\n *\n * If the current value is already a matching Y.js type (Y.Text, Y.Array,\n * Y.Map), the update is merged in-place so concurrent edits are preserved.\n * Otherwise the value is replaced wholesale.\n *\n * @param schema The attribute type definition for this value.\n * @param newVal The new value to merge into the Y.Map entry.\n * @param yMap The Y.Map that owns this entry.\n * @param key The key of this entry in the Y.Map.\n * @param cursorPosition The cursor position for rich-text delta merges from the updated value.\n * @param cursorScope Indicates a specific block and attribute associated with the editor;\n * determines whether the cursor should be updated based on the change.\n */\nfunction mergeYValue(\n\tschema: BlockAttributeSchema | undefined,\n\tnewVal: unknown,\n\tyMap: Y.Map< unknown >,\n\tkey: string,\n\tcursorPosition: MergeCursorPosition,\n\tcursorScope: RichTextCursorScope\n): void {\n\tconst currentVal = yMap.get( key );\n\tif (\n\t\tschema?.type === 'rich-text' &&\n\t\ttypeof newVal === 'string' &&\n\t\tcurrentVal instanceof Y.Text\n\t) {\n\t\tmergeRichTextUpdate(\n\t\t\tcurrentVal,\n\t\t\tnewVal,\n\t\t\tresolveRichTextCursorPosition( cursorPosition, cursorScope, newVal )\n\t\t);\n\t} else if (\n\t\tschema?.type === 'array' &&\n\t\tschema.query &&\n\t\tArray.isArray( newVal ) &&\n\t\tcurrentVal instanceof Y.Array\n\t) {\n\t\tmergeYArray( currentVal, newVal, schema, cursorPosition, cursorScope );\n\t} else if (\n\t\tschema?.type === 'object' &&\n\t\tschema.query &&\n\t\tisRecord( newVal ) &&\n\t\tcurrentVal instanceof Y.Map\n\t) {\n\t\tmergeYMapValues(\n\t\t\tcurrentVal,\n\t\t\tnewVal,\n\t\t\tschema.query,\n\t\t\tcursorPosition,\n\t\t\tcursorScope\n\t\t);\n\t} else {\n\t\tconst newYValue = createYValueFromSchema( schema, newVal );\n\n\t\t// If createYValueFromSchema wrapped the value into a Y type, the\n\t\t// current value is the wrong type and needs upgrading. Otherwise,\n\t\t// only replace if the raw value actually changed.\n\t\tif ( newYValue !== newVal || ! fastDeepEqual( currentVal, newVal ) ) {\n\t\t\tyMap.set( key, newYValue );\n\t\t}\n\t}\n}\n\n/**\n * Merge an incoming plain object into an existing Y.Map in-place, using\n * the query schema to decide how each property should be merged.\n *\n * Properties present in the Y.Map but absent from `newObj` are deleted.\n *\n * @param yMap The existing Y.Map to update.\n * @param newObj The new plain object to merge into the Y.Map.\n * @param query The query schema defining property types.\n * @param cursorPosition The local cursor position for rich-text delta merges.\n * @param cursorScope The selected block attribute scope for rich-text cursor hints.\n */\nfunction mergeYMapValues(\n\tyMap: Y.Map< unknown >,\n\tnewObj: Record< string, unknown >,\n\tquery: Record< string, BlockAttributeSchema >,\n\tcursorPosition: MergeCursorPosition,\n\tcursorScope: RichTextCursorScope\n): void {\n\tfor ( const [ key, newVal ] of Object.entries( newObj ) ) {\n\t\tmergeYValue(\n\t\t\tquery[ key ],\n\t\t\tnewVal,\n\t\t\tyMap,\n\t\t\tkey,\n\t\t\tcursorPosition,\n\t\t\tcursorScope\n\t\t);\n\t}\n\n\t// Delete properties absent from the incoming object.\n\tfor ( const key of yMap.keys() ) {\n\t\tif ( ! Object.hasOwn( newObj, key ) ) {\n\t\t\tyMap.delete( key );\n\t\t}\n\t}\n}\n\n/**\n * Update a single attribute on a Yjs block attributes map (currentAttributes).\n *\n * @param blockName The block type name, e.g. 'core/paragraph'.\n * @param clientId The local clientId for the block being merged.\n * @param attributeName The name of the attribute to update, e.g. 'content'.\n * @param attributeValue The new value for the attribute.\n * @param currentAttributes The Y.Map holding the block's current attributes.\n * @param newCursorPosition The cursor position for rich-text delta merges from the updated value.\n * Notably, this may not correspond to the attribute being edited and is\n * used to determine if any cursors need shifting in response to the change.\n */\nfunction updateYBlockAttribute(\n\tblockName: string,\n\tclientId: string | undefined,\n\tattributeName: string,\n\tattributeValue: unknown,\n\tcurrentAttributes: YBlockAttributes,\n\tnewCursorPosition: MergeCursorPosition\n): void {\n\tconst schema = getBlockAttributeSchema( blockName, attributeName );\n\n\t/*\n\t * @todo There is a slight discrepancy between the attribute name and key, which might\n\t * show up when working with multiline RichText instances (of which there are no\n\t * more within Core). For those instances, a cursor might never be updated in\n\t * response to changes because its `attributeKey` won\u2019t match any of the block\u2019s\n\t * attribute names, and since updating this attribute is based on the block names,\n\t * no suitable key for the cursor scope will be created. To fix, the updating code\n\t * would need to parse multiline attributes and infer the `attributeKey` being updated.\n\t */\n\tmergeYValue(\n\t\tschema,\n\t\tattributeValue,\n\t\tcurrentAttributes,\n\t\tattributeName,\n\t\tnewCursorPosition,\n\t\t{ attributeKey: attributeName, clientId }\n\t);\n}\n\n/**\n * References the specific block and attribute associated with a RichText component.\n *\n * This is used to associate a cursor with the attribute it\u2019s editing.\n *\n * @see WPBlockSelection\n */\ninterface RichTextCursorScope {\n\tattributeKey: string;\n\tclientId: string | undefined;\n}\n\ninterface DeltaWithOps {\n\tops: Parameters< Y.Text[ 'applyDelta' ] >[ 0 ];\n}\n\n/**\n * When the provided cursor falls within the given block and attribute\u2019s scope,\n * returns an index into the RichText\u2019s serialized HTML where the cursor falls.\n *\n * The cursor scope constrains resolution to ensure that indices are only reported\n * when a cursor falls within the block and attribute being updated, since the\n * attributes being updated may not always be the ones where a cursor presently falls.\n *\n * Returned index measures JS string lengths, thus is counted in UTF-16 code units\n * and contains the syntax characters making up HTML tags, comments, character\n * references, and other non-plaintext content.\n *\n * @param cursorPosition Description of the cursor in the new value.\n * @param cursorScope Cursors should only be updated if they fall within this\n * specific block and attribute.\n * @param updatedValue New RichText value potentially containing cursor.\n * @return String length into serialized HTML for RichText instance where cursor falls.\n */\nfunction resolveRichTextCursorPosition(\n\tcursorPosition: MergeCursorPosition,\n\tcursorScope: RichTextCursorScope,\n\tupdatedValue: string\n): HtmlStringIndex | null {\n\treturn cursorPosition &&\n\t\tcursorPosition.clientId === cursorScope.clientId &&\n\t\tcursorPosition.attributeKey === cursorScope.attributeKey &&\n\t\t'number' === typeof cursorPosition.offset &&\n\t\tNumber.isInteger( cursorPosition.offset )\n\t\t? richTextOffsetToHtmlIndex(\n\t\t\t\tupdatedValue,\n\t\t\t\tasRichTextOffset( cursorPosition.offset )\n\t\t )\n\t\t: null;\n}\n\n// Cached block attribute types, populated once from getBlockTypes().\nlet cachedBlockAttributeSchemas: Map<\n\tstring,\n\tMap< string, BlockAttributeSchema >\n>;\n\n/**\n * Get the attribute type definition for a block attribute.\n *\n * @param blockName The name of the block, e.g. 'core/paragraph'.\n * @param attributeName The name of the attribute, e.g. 'content'.\n * @return The type definition of the attribute.\n */\nfunction getBlockAttributeSchema(\n\tblockName: string,\n\tattributeName: string\n): BlockAttributeSchema | undefined {\n\tif ( ! cachedBlockAttributeSchemas ) {\n\t\t// Parse the attributes for all blocks once.\n\t\tcachedBlockAttributeSchemas = new Map();\n\n\t\tfor ( const blockType of getBlockTypes() as BlockType[] ) {\n\t\t\tcachedBlockAttributeSchemas.set(\n\t\t\t\tblockType.name,\n\t\t\t\tnew Map< string, BlockAttributeSchema >(\n\t\t\t\t\tObject.entries( blockType.attributes ?? {} ).map(\n\t\t\t\t\t\t( [ name, definition ] ) => {\n\t\t\t\t\t\t\tconst { role, type, query } = definition;\n\t\t\t\t\t\t\treturn [ name, { role, type, query } ];\n\t\t\t\t\t\t}\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n\n\treturn cachedBlockAttributeSchemas.get( blockName )?.get( attributeName );\n}\n\n/**\n * Check if an attribute value is the expected type.\n *\n * @param blockName The name of the block, e.g. 'core/paragraph'.\n * @param attributeName The name of the attribute, e.g. 'content'.\n * @param attributeValue The current attribute value.\n * @return True if the attribute type is expected, false otherwise.\n */\nfunction isExpectedAttributeType(\n\tblockName: string,\n\tattributeName: string,\n\tattributeValue: unknown\n): boolean {\n\tconst schema = getBlockAttributeSchema( blockName, attributeName );\n\n\tif ( schema?.type === 'rich-text' ) {\n\t\treturn attributeValue instanceof Y.Text;\n\t}\n\n\tif ( schema?.type === 'string' ) {\n\t\treturn typeof attributeValue === 'string';\n\t}\n\n\tif ( schema?.type === 'array' && schema.query ) {\n\t\treturn attributeValue instanceof Y.Array;\n\t}\n\n\tif ( schema?.type === 'object' && schema.query ) {\n\t\treturn attributeValue instanceof Y.Map;\n\t}\n\n\treturn true;\n}\n\n/**\n * Given a block name and attribute key, return true if the attribute is local\n * and should not be synced.\n *\n * @param blockName The name of the block, e.g. 'core/image'.\n * @param attributeName The name of the attribute to check, e.g. 'blob'.\n * @return True if the attribute is local, false otherwise.\n */\nfunction isLocalAttribute( blockName: string, attributeName: string ): boolean {\n\treturn (\n\t\t'local' === getBlockAttributeSchema( blockName, attributeName )?.role\n\t);\n}\n\nlet localDoc: Y.Doc;\n\n/**\n * Given a Y.Text object and an updated string value, diff the new value and\n * apply the delta to the Y.Text.\n *\n * @param blockYText The Y.Text to update.\n * @param updatedValue The updated value.\n * @param htmlCursorIndex The cursor index in the updated HTML string.\n */\nexport function mergeRichTextUpdate(\n\tblockYText: Y.Text,\n\tupdatedValue: string,\n\thtmlCursorIndex: HtmlStringIndex | null = null\n): void {\n\t// Gutenberg does not use Yjs shared types natively, so we can only subscribe\n\t// to changes from store and apply them to Yjs types that we create and\n\t// manage. Crucially, for rich-text attributes, we do not receive granular\n\t// string updates; we get the new full string value on each change, even when\n\t// only a single character changed.\n\t//\n\t// The code below allows us to compute a delta between the current and new\n\t// value, then apply it to the Y.Text.\n\n\tconst currentValueAsDelta = new Delta( blockYText.toDelta() );\n\tconst updatedValueAsDelta = new Delta( [ { insert: updatedValue } ] );\n\tconst deltaDiff = currentValueAsDelta.diffWithCursor(\n\t\tupdatedValueAsDelta,\n\t\thtmlCursorIndex\n\t);\n\n\t/**\n\t * When there is no cursor involved, or when the diff is able to shuffle properly\n\t * around the cursor then apply that already-computed diff.\n\t *\n\t * However, `diffWithCursor()` currently fails in certain cases, producing corrupted\n\t * output. In these cases, fall back to the raw diff as that will apply cleanly,\n\t * even if it provides a less meaningful diff.\n\t *\n\t * @see Delta.diffWithCursor()\n\t */\n\tconst safeDiff =\n\t\thtmlCursorIndex === null ||\n\t\tisDeltaVerificationMatch( blockYText, deltaDiff, updatedValue )\n\t\t\t? deltaDiff\n\t\t\t: currentValueAsDelta.diff( updatedValueAsDelta );\n\n\tblockYText.applyDelta( safeDiff.ops );\n}\n\n/**\n * Verify that applying a delta to an existing Y.Text object produces the expected\n * output string.\n *\n * A stale, mis-scoped, or corrupted Delta will mutate a text value to the wrong\n * output string. This function applies the given Delta and indicates whether it\n * produces the given expected output string value.\n *\n * @param blockYText The current Y.Text before applying the candidate delta.\n * @param delta The candidate delta.\n * @param expectedValue The exact string expected after applying the delta.\n * @return Whether the candidate delta produces the expected value.\n */\nfunction isDeltaVerificationMatch(\n\tblockYText: Y.Text,\n\tdelta: DeltaWithOps,\n\texpectedValue: string\n): boolean {\n\tif ( ! localDoc ) {\n\t\t// Y.Text must be attached to a Y.Doc to be able to do operations on it.\n\t\t// Create a temporary Y.Text attached to a local Y.Doc for delta computation.\n\t\t// This is an optimization to avoid creating a new Y.Doc on every update.\n\t\tlocalDoc = new Y.Doc();\n\t}\n\n\tconst verificationYText = localDoc.getText( 'verification-text' );\n\n\t// Because this is global, it must be cleared before using.\n\tverificationYText.delete( 0, verificationYText.length );\n\tverificationYText.insert( 0, blockYText.toString() );\n\tverificationYText.applyDelta( delta.ops );\n\n\treturn verificationYText.toString() === expectedValue;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,6 +6,7 @@ import { CRDT_RECORD_MAP_KEY } from "../sync.mjs";
6
6
  import {
7
7
  asRichTextOffset,
8
8
  getRootMap,
9
+ getYTextByAttributeKey,
9
10
  richTextOffsetToHtmlIndex
10
11
  } from "./crdt-utils.mjs";
11
12
  var SelectionType = /* @__PURE__ */ ((SelectionType2) => {
@@ -16,6 +17,11 @@ var SelectionType = /* @__PURE__ */ ((SelectionType2) => {
16
17
  SelectionType2["WholeBlock"] = "whole-block";
17
18
  return SelectionType2;
18
19
  })(SelectionType || {});
20
+ var SelectionDirection = /* @__PURE__ */ ((SelectionDirection2) => {
21
+ SelectionDirection2["Forward"] = "f";
22
+ SelectionDirection2["Backward"] = "b";
23
+ return SelectionDirection2;
24
+ })(SelectionDirection || {});
19
25
  function getSelectionState(selectionStart, selectionEnd, yDoc, options) {
20
26
  const { selectionDirection } = options ?? {};
21
27
  const ymap = getRootMap(yDoc, CRDT_RECORD_MAP_KEY);
@@ -84,7 +90,7 @@ function getCursorPosition(selection, blocks) {
84
90
  return null;
85
91
  }
86
92
  const attributes = block.get("attributes");
87
- const currentYText = attributes?.get(selection.attributeKey);
93
+ const currentYText = attributes ? getYTextByAttributeKey(attributes, selection.attributeKey) : null;
88
94
  if (!(currentYText instanceof Y.Text)) {
89
95
  return null;
90
96
  }
@@ -97,7 +103,8 @@ function getCursorPosition(selection, blocks) {
97
103
  );
98
104
  return {
99
105
  relativePosition,
100
- absoluteOffset: selection.offset
106
+ absoluteOffset: selection.offset,
107
+ attributeKey: selection.attributeKey
101
108
  };
102
109
  }
103
110
  function getBlockPathForLocalClientId(clientId) {
@@ -202,6 +209,7 @@ function areCursorPositionsEqual(cursorPosition1, cursorPosition2) {
202
209
  return isRelativePositionEqual && isAbsoluteOffsetEqual;
203
210
  }
204
211
  export {
212
+ SelectionDirection,
205
213
  SelectionType,
206
214
  areSelectionsStatesEqual,
207
215
  getBlockPathForLocalClientId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/crdt-user-selections.ts"],
4
- "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { select } from '@wordpress/data';\nimport { Y } from '@wordpress/sync';\n// @ts-ignore No exported types for block editor store selectors.\nimport { store as blockEditorStore } from '@wordpress/block-editor';\n\n/**\n * Internal dependencies\n */\nimport { CRDT_RECORD_MAP_KEY } from '../sync';\nimport type { YPostRecord } from './crdt';\nimport type { YBlock, YBlocks } from './crdt-blocks';\nimport {\n\tasRichTextOffset,\n\tgetRootMap,\n\trichTextOffsetToHtmlIndex,\n} from './crdt-utils';\nimport type {\n\tAbsoluteBlockIndexPath,\n\tWPBlockSelection,\n\tSelectionState,\n\tSelectionNone,\n\tSelectionCursor,\n\tSelectionInOneBlock,\n\tSelectionInMultipleBlocks,\n\tSelectionWholeBlock,\n\tSelectionDirection,\n\tCursorPosition,\n} from '../types';\n\n/**\n * The type of selection.\n */\nexport enum SelectionType {\n\tNone = 'none',\n\tCursor = 'cursor',\n\tSelectionInOneBlock = 'selection-in-one-block',\n\tSelectionInMultipleBlocks = 'selection-in-multiple-blocks',\n\tWholeBlock = 'whole-block',\n}\n\n/**\n * Converts WordPress block editor selection to a SelectionState.\n *\n * Uses getBlockPathForLocalClientId to locate blocks in the Yjs document by\n * their tree position (index path) rather than clientId, since clientIds may\n * differ between the block-editor store and the Yjs document (e.g. in \"Show\n * Template\" mode).\n *\n * @param selectionStart - The start position of the selection\n * @param selectionEnd - The end position of the selection\n * @param yDoc - The Yjs document\n * @param options - Optional parameters\n * @param options.selectionDirection - The direction of the selection (forward or backward)\n * @return The SelectionState\n */\nexport function getSelectionState(\n\tselectionStart: WPBlockSelection,\n\tselectionEnd: WPBlockSelection,\n\tyDoc: Y.Doc,\n\toptions?: { selectionDirection?: SelectionDirection }\n): SelectionState {\n\tconst { selectionDirection } = options ?? {};\n\tconst ymap = getRootMap< YPostRecord >( yDoc, CRDT_RECORD_MAP_KEY );\n\tconst yBlocks = ymap.get( 'blocks' );\n\n\tconst isSelectionEmpty = Object.keys( selectionStart ).length === 0;\n\tconst noSelection: SelectionNone = {\n\t\ttype: SelectionType.None,\n\t};\n\n\tif ( isSelectionEmpty || ! yBlocks ) {\n\t\t// Case 1: No selection, or no blocks in the document.\n\t\treturn noSelection;\n\t}\n\n\t// When the page initially loads, selectionStart can contain an empty object `{}`.\n\tconst isSelectionInOneBlock =\n\t\tselectionStart.clientId === selectionEnd.clientId;\n\tconst isCursorOnly =\n\t\tisSelectionInOneBlock && selectionStart.offset === selectionEnd.offset;\n\tconst isSelectionAWholeBlock =\n\t\tisSelectionInOneBlock &&\n\t\tselectionStart.offset === undefined &&\n\t\tselectionEnd.offset === undefined;\n\n\tif ( isSelectionAWholeBlock ) {\n\t\t// Case 2: A whole block is selected.\n\t\tconst path = getBlockPathForLocalClientId( selectionStart.clientId );\n\t\tconst blockPosition = path\n\t\t\t? createRelativePositionForBlockPath( path, yBlocks )\n\t\t\t: null;\n\n\t\tif ( ! blockPosition ) {\n\t\t\treturn noSelection;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: SelectionType.WholeBlock,\n\t\t\tblockPosition,\n\t\t};\n\t} else if ( isCursorOnly ) {\n\t\t// Case 3: Cursor only, no text selected\n\t\tconst cursorPosition = getCursorPosition( selectionStart, yBlocks );\n\n\t\tif ( ! cursorPosition ) {\n\t\t\t// If we can't find the cursor position in block text, treat it as a non-selection.\n\t\t\treturn noSelection;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: SelectionType.Cursor,\n\t\t\tcursorPosition,\n\t\t};\n\t} else if ( isSelectionInOneBlock ) {\n\t\t// Case 4: Selection in a single block\n\t\tconst cursorStartPosition = getCursorPosition(\n\t\t\tselectionStart,\n\t\t\tyBlocks\n\t\t);\n\t\tconst cursorEndPosition = getCursorPosition( selectionEnd, yBlocks );\n\n\t\tif ( ! cursorStartPosition || ! cursorEndPosition ) {\n\t\t\t// If we can't find the cursor positions in block text, treat it as a non-selection.\n\t\t\treturn noSelection;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: SelectionType.SelectionInOneBlock,\n\t\t\tcursorStartPosition,\n\t\t\tcursorEndPosition,\n\t\t\tselectionDirection,\n\t\t};\n\t}\n\n\t// Case 5: Selection in multiple blocks\n\tconst cursorStartPosition = getCursorPosition( selectionStart, yBlocks );\n\tconst cursorEndPosition = getCursorPosition( selectionEnd, yBlocks );\n\tif ( ! cursorStartPosition || ! cursorEndPosition ) {\n\t\t// If we can't find the cursor positions in block text, treat it as a non-selection.\n\t\treturn noSelection;\n\t}\n\n\treturn {\n\t\ttype: SelectionType.SelectionInMultipleBlocks,\n\t\tcursorStartPosition,\n\t\tcursorEndPosition,\n\t\tselectionDirection,\n\t};\n}\n\n/**\n * Get the cursor position from a selection.\n *\n * @param selection - The selection.\n * @param blocks - The blocks to search through.\n * @return The cursor position, or null if not found.\n */\nfunction getCursorPosition(\n\tselection: WPBlockSelection,\n\tblocks: YBlocks\n): CursorPosition | null {\n\tconst path = getBlockPathForLocalClientId( selection.clientId );\n\tconst block = path ? findBlockByPath( path, blocks ) : null;\n\tif (\n\t\t! block ||\n\t\t! selection.attributeKey ||\n\t\tundefined === selection.offset\n\t) {\n\t\treturn null;\n\t}\n\n\tconst attributes = block.get( 'attributes' );\n\tconst currentYText = attributes?.get( selection.attributeKey );\n\n\t// If the attribute is not a Y.Text, return null.\n\tif ( ! ( currentYText instanceof Y.Text ) ) {\n\t\treturn null;\n\t}\n\n\tconst relativePosition = Y.createRelativePositionFromTypeIndex(\n\t\tcurrentYText,\n\t\trichTextOffsetToHtmlIndex(\n\t\t\tcurrentYText.toString(),\n\t\t\tasRichTextOffset( selection.offset )\n\t\t)\n\t);\n\n\treturn {\n\t\trelativePosition,\n\t\tabsoluteOffset: selection.offset,\n\t};\n}\n\n/**\n * Resolves a local block-editor clientId to its index path relative to the\n * post content blocks. This allows finding the corresponding block in the Yjs\n * document even when clientIds differ (e.g. in \"Show Template\" mode where\n * blocks are cloned).\n *\n * In template mode, the block tree includes template parts and wrapper blocks\n * around a core/post-content block. The Yjs document only contains the post\n * content blocks, so we stop the upward walk when the parent is\n * core/post-content (its inner blocks correspond to the Yjs root blocks).\n *\n * @param clientId - The local block-editor clientId to resolve.\n * @return The index path from root, or null if not resolvable.\n */\nexport function getBlockPathForLocalClientId(\n\tclientId: string\n): AbsoluteBlockIndexPath | null {\n\tconst { getBlockIndex, getBlockRootClientId, getBlockName } =\n\t\tselect( blockEditorStore );\n\n\tconst path: AbsoluteBlockIndexPath = [];\n\tlet current: string | null = clientId;\n\twhile ( current ) {\n\t\tconst index = getBlockIndex( current );\n\t\tif ( index === -1 ) {\n\t\t\treturn null;\n\t\t}\n\t\tpath.unshift( index );\n\t\tconst parent = getBlockRootClientId( current );\n\t\tif ( ! parent ) {\n\t\t\tbreak;\n\t\t}\n\t\t// If the parent is core/post-content, stop here \u2014 the Yjs doc\n\t\t// root blocks correspond to post-content's inner blocks.\n\t\tconst parentName = getBlockName( parent );\n\t\tif ( parentName === 'core/post-content' ) {\n\t\t\tbreak;\n\t\t}\n\t\tcurrent = parent;\n\t}\n\treturn path.length > 0 ? path : null;\n}\n\n/**\n * Find a block by navigating a tree index path in the Yjs block hierarchy.\n *\n * @param path - The index path, e.g. [0, 1] for blocks[0].innerBlocks[1].\n * @param blocks - The root-level Yjs blocks array.\n * @return The block Y.Map if found, null otherwise.\n */\nfunction findBlockByPath(\n\tpath: AbsoluteBlockIndexPath,\n\tblocks: YBlocks\n): YBlock | null {\n\tlet currentBlocks = blocks;\n\tfor ( let i = 0; i < path.length; i++ ) {\n\t\tif ( path[ i ] >= currentBlocks.length ) {\n\t\t\treturn null;\n\t\t}\n\t\tconst block = currentBlocks.get( path[ i ] );\n\t\tif ( ! block ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( i === path.length - 1 ) {\n\t\t\treturn block;\n\t\t}\n\t\tcurrentBlocks =\n\t\t\tblock.get( 'innerBlocks' ) ?? ( new Y.Array() as YBlocks );\n\t}\n\treturn null;\n}\n\n/**\n * Create a Y.RelativePosition for a block by navigating a tree index path.\n *\n * @param path - The index path, e.g. [0, 1] for blocks[0].innerBlocks[1].\n * @param blocks - The root-level Yjs blocks array.\n * @return A Y.RelativePosition for the block, or null if the path is invalid.\n */\nfunction createRelativePositionForBlockPath(\n\tpath: AbsoluteBlockIndexPath,\n\tblocks: YBlocks\n): Y.RelativePosition | null {\n\tlet currentBlocks = blocks;\n\tfor ( let i = 0; i < path.length; i++ ) {\n\t\tif ( path[ i ] >= currentBlocks.length ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( i === path.length - 1 ) {\n\t\t\treturn Y.createRelativePositionFromTypeIndex(\n\t\t\t\tcurrentBlocks,\n\t\t\t\tpath[ i ]\n\t\t\t);\n\t\t}\n\t\tconst block = currentBlocks.get( path[ i ] );\n\t\tcurrentBlocks =\n\t\t\tblock?.get( 'innerBlocks' ) ?? ( new Y.Array() as YBlocks );\n\t}\n\treturn null;\n}\n\n/**\n * Check if two selection states are equal.\n *\n * @param selection1 - The first selection state.\n * @param selection2 - The second selection state.\n * @return True if the selection states are equal, false otherwise.\n */\nexport function areSelectionsStatesEqual(\n\tselection1: SelectionState,\n\tselection2: SelectionState\n): boolean {\n\tif ( selection1.type !== selection2.type ) {\n\t\treturn false;\n\t}\n\n\tswitch ( selection1.type ) {\n\t\tcase SelectionType.None:\n\t\t\treturn true;\n\n\t\tcase SelectionType.Cursor:\n\t\t\treturn areCursorPositionsEqual(\n\t\t\t\tselection1.cursorPosition,\n\t\t\t\t( selection2 as SelectionCursor ).cursorPosition\n\t\t\t);\n\n\t\tcase SelectionType.SelectionInOneBlock:\n\t\t\treturn (\n\t\t\t\tareCursorPositionsEqual(\n\t\t\t\t\tselection1.cursorStartPosition,\n\t\t\t\t\t( selection2 as SelectionInOneBlock ).cursorStartPosition\n\t\t\t\t) &&\n\t\t\t\tareCursorPositionsEqual(\n\t\t\t\t\tselection1.cursorEndPosition,\n\t\t\t\t\t( selection2 as SelectionInOneBlock ).cursorEndPosition\n\t\t\t\t) &&\n\t\t\t\tselection1.selectionDirection ===\n\t\t\t\t\t( selection2 as SelectionInOneBlock ).selectionDirection\n\t\t\t);\n\n\t\tcase SelectionType.SelectionInMultipleBlocks:\n\t\t\treturn (\n\t\t\t\tareCursorPositionsEqual(\n\t\t\t\t\tselection1.cursorStartPosition,\n\t\t\t\t\t( selection2 as SelectionInMultipleBlocks )\n\t\t\t\t\t\t.cursorStartPosition\n\t\t\t\t) &&\n\t\t\t\tareCursorPositionsEqual(\n\t\t\t\t\tselection1.cursorEndPosition,\n\t\t\t\t\t( selection2 as SelectionInMultipleBlocks )\n\t\t\t\t\t\t.cursorEndPosition\n\t\t\t\t) &&\n\t\t\t\tselection1.selectionDirection ===\n\t\t\t\t\t( selection2 as SelectionInMultipleBlocks )\n\t\t\t\t\t\t.selectionDirection\n\t\t\t);\n\t\tcase SelectionType.WholeBlock:\n\t\t\treturn Y.compareRelativePositions(\n\t\t\t\tselection1.blockPosition,\n\t\t\t\t( selection2 as SelectionWholeBlock ).blockPosition\n\t\t\t);\n\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * Check if two cursor positions are equal.\n *\n * @param cursorPosition1 - The first cursor position.\n * @param cursorPosition2 - The second cursor position.\n * @return True if the cursor positions are equal, false otherwise.\n */\nfunction areCursorPositionsEqual(\n\tcursorPosition1: CursorPosition,\n\tcursorPosition2: CursorPosition\n): boolean {\n\tconst isRelativePositionEqual = Y.compareRelativePositions(\n\t\tcursorPosition1.relativePosition,\n\t\tcursorPosition2.relativePosition\n\t);\n\n\t// Ensure a change in calculated absolute offset results in a treating the cursor as modified.\n\t// This is necessary because Y.Text relative positions can remain the same after text changes.\n\tconst isAbsoluteOffsetEqual =\n\t\tcursorPosition1.absoluteOffset === cursorPosition2.absoluteOffset;\n\n\treturn isRelativePositionEqual && isAbsoluteOffsetEqual;\n}\n"],
5
- "mappings": "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",
6
- "names": ["SelectionType", "cursorStartPosition", "cursorEndPosition"]
4
+ "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { select } from '@wordpress/data';\nimport { Y } from '@wordpress/sync';\n// @ts-ignore No exported types for block editor store selectors.\nimport { store as blockEditorStore } from '@wordpress/block-editor';\n\n/**\n * Internal dependencies\n */\nimport { CRDT_RECORD_MAP_KEY } from '../sync';\nimport type { YPostRecord } from './crdt';\nimport type { YBlock, YBlocks } from './crdt-blocks';\nimport {\n\tasRichTextOffset,\n\tgetRootMap,\n\tgetYTextByAttributeKey,\n\trichTextOffsetToHtmlIndex,\n} from './crdt-utils';\nimport type {\n\tAbsoluteBlockIndexPath,\n\tWPBlockSelection,\n\tSelectionState,\n\tSelectionNone,\n\tSelectionCursor,\n\tSelectionInOneBlock,\n\tSelectionInMultipleBlocks,\n\tSelectionWholeBlock,\n\tCursorPosition,\n} from '../types';\n\n/**\n * The type of selection.\n */\nexport enum SelectionType {\n\tNone = 'none',\n\tCursor = 'cursor',\n\tSelectionInOneBlock = 'selection-in-one-block',\n\tSelectionInMultipleBlocks = 'selection-in-multiple-blocks',\n\tWholeBlock = 'whole-block',\n}\n\n/**\n * The direction of a text selection, indicating where the caret sits.\n */\nexport enum SelectionDirection {\n\t/** The caret is at the end of the selection (default / left-to-right). */\n\tForward = 'f',\n\t/** The caret is at the start of the selection (right-to-left). */\n\tBackward = 'b',\n}\n\n/**\n * Converts WordPress block editor selection to a SelectionState.\n *\n * Uses getBlockPathForLocalClientId to locate blocks in the Yjs document by\n * their tree position (index path) rather than clientId, since clientIds may\n * differ between the block-editor store and the Yjs document (e.g. in \"Show\n * Template\" mode).\n *\n * @param selectionStart - The start position of the selection\n * @param selectionEnd - The end position of the selection\n * @param yDoc - The Yjs document\n * @param options - Optional parameters\n * @param options.selectionDirection - The direction of the selection (forward or backward)\n * @return The SelectionState\n */\nexport function getSelectionState(\n\tselectionStart: WPBlockSelection,\n\tselectionEnd: WPBlockSelection,\n\tyDoc: Y.Doc,\n\toptions?: { selectionDirection?: SelectionDirection }\n): SelectionState {\n\tconst { selectionDirection } = options ?? {};\n\tconst ymap = getRootMap< YPostRecord >( yDoc, CRDT_RECORD_MAP_KEY );\n\tconst yBlocks = ymap.get( 'blocks' );\n\n\tconst isSelectionEmpty = Object.keys( selectionStart ).length === 0;\n\tconst noSelection: SelectionNone = {\n\t\ttype: SelectionType.None,\n\t};\n\n\tif ( isSelectionEmpty || ! yBlocks ) {\n\t\t// Case 1: No selection, or no blocks in the document.\n\t\treturn noSelection;\n\t}\n\n\t// When the page initially loads, selectionStart can contain an empty object `{}`.\n\tconst isSelectionInOneBlock =\n\t\tselectionStart.clientId === selectionEnd.clientId;\n\tconst isCursorOnly =\n\t\tisSelectionInOneBlock && selectionStart.offset === selectionEnd.offset;\n\tconst isSelectionAWholeBlock =\n\t\tisSelectionInOneBlock &&\n\t\tselectionStart.offset === undefined &&\n\t\tselectionEnd.offset === undefined;\n\n\tif ( isSelectionAWholeBlock ) {\n\t\t// Case 2: A whole block is selected.\n\t\tconst path = getBlockPathForLocalClientId( selectionStart.clientId );\n\t\tconst blockPosition = path\n\t\t\t? createRelativePositionForBlockPath( path, yBlocks )\n\t\t\t: null;\n\n\t\tif ( ! blockPosition ) {\n\t\t\treturn noSelection;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: SelectionType.WholeBlock,\n\t\t\tblockPosition,\n\t\t};\n\t} else if ( isCursorOnly ) {\n\t\t// Case 3: Cursor only, no text selected\n\t\tconst cursorPosition = getCursorPosition( selectionStart, yBlocks );\n\n\t\tif ( ! cursorPosition ) {\n\t\t\t// If we can't find the cursor position in block text, treat it as a non-selection.\n\t\t\treturn noSelection;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: SelectionType.Cursor,\n\t\t\tcursorPosition,\n\t\t};\n\t} else if ( isSelectionInOneBlock ) {\n\t\t// Case 4: Selection in a single block\n\t\tconst cursorStartPosition = getCursorPosition(\n\t\t\tselectionStart,\n\t\t\tyBlocks\n\t\t);\n\t\tconst cursorEndPosition = getCursorPosition( selectionEnd, yBlocks );\n\n\t\tif ( ! cursorStartPosition || ! cursorEndPosition ) {\n\t\t\t// If we can't find the cursor positions in block text, treat it as a non-selection.\n\t\t\treturn noSelection;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: SelectionType.SelectionInOneBlock,\n\t\t\tcursorStartPosition,\n\t\t\tcursorEndPosition,\n\t\t\tselectionDirection,\n\t\t};\n\t}\n\n\t// Case 5: Selection in multiple blocks\n\tconst cursorStartPosition = getCursorPosition( selectionStart, yBlocks );\n\tconst cursorEndPosition = getCursorPosition( selectionEnd, yBlocks );\n\tif ( ! cursorStartPosition || ! cursorEndPosition ) {\n\t\t// If we can't find the cursor positions in block text, treat it as a non-selection.\n\t\treturn noSelection;\n\t}\n\n\treturn {\n\t\ttype: SelectionType.SelectionInMultipleBlocks,\n\t\tcursorStartPosition,\n\t\tcursorEndPosition,\n\t\tselectionDirection,\n\t};\n}\n\n/**\n * Get the cursor position from a selection.\n *\n * @param selection - The selection.\n * @param blocks - The blocks to search through.\n * @return The cursor position, or null if not found.\n */\nfunction getCursorPosition(\n\tselection: WPBlockSelection,\n\tblocks: YBlocks\n): CursorPosition | null {\n\tconst path = getBlockPathForLocalClientId( selection.clientId );\n\tconst block = path ? findBlockByPath( path, blocks ) : null;\n\tif (\n\t\t! block ||\n\t\t! selection.attributeKey ||\n\t\tundefined === selection.offset\n\t) {\n\t\treturn null;\n\t}\n\n\tconst attributes = block.get( 'attributes' );\n\tconst currentYText = attributes\n\t\t? getYTextByAttributeKey( attributes, selection.attributeKey )\n\t\t: null;\n\n\t// If the attribute is not a Y.Text, return null.\n\tif ( ! ( currentYText instanceof Y.Text ) ) {\n\t\treturn null;\n\t}\n\n\tconst relativePosition = Y.createRelativePositionFromTypeIndex(\n\t\tcurrentYText,\n\t\trichTextOffsetToHtmlIndex(\n\t\t\tcurrentYText.toString(),\n\t\t\tasRichTextOffset( selection.offset )\n\t\t)\n\t);\n\n\treturn {\n\t\trelativePosition,\n\t\tabsoluteOffset: selection.offset,\n\t\tattributeKey: selection.attributeKey,\n\t};\n}\n\n/**\n * Resolves a local block-editor clientId to its index path relative to the\n * post content blocks. This allows finding the corresponding block in the Yjs\n * document even when clientIds differ (e.g. in \"Show Template\" mode where\n * blocks are cloned).\n *\n * In template mode, the block tree includes template parts and wrapper blocks\n * around a core/post-content block. The Yjs document only contains the post\n * content blocks, so we stop the upward walk when the parent is\n * core/post-content (its inner blocks correspond to the Yjs root blocks).\n *\n * @param clientId - The local block-editor clientId to resolve.\n * @return The index path from root, or null if not resolvable.\n */\nexport function getBlockPathForLocalClientId(\n\tclientId: string\n): AbsoluteBlockIndexPath | null {\n\tconst { getBlockIndex, getBlockRootClientId, getBlockName } =\n\t\tselect( blockEditorStore );\n\n\tconst path: AbsoluteBlockIndexPath = [];\n\tlet current: string | null = clientId;\n\twhile ( current ) {\n\t\tconst index = getBlockIndex( current );\n\t\tif ( index === -1 ) {\n\t\t\treturn null;\n\t\t}\n\t\tpath.unshift( index );\n\t\tconst parent = getBlockRootClientId( current );\n\t\tif ( ! parent ) {\n\t\t\tbreak;\n\t\t}\n\t\t// If the parent is core/post-content, stop here \u2014 the Yjs doc\n\t\t// root blocks correspond to post-content's inner blocks.\n\t\tconst parentName = getBlockName( parent );\n\t\tif ( parentName === 'core/post-content' ) {\n\t\t\tbreak;\n\t\t}\n\t\tcurrent = parent;\n\t}\n\treturn path.length > 0 ? path : null;\n}\n\n/**\n * Find a block by navigating a tree index path in the Yjs block hierarchy.\n *\n * @param path - The index path, e.g. [0, 1] for blocks[0].innerBlocks[1].\n * @param blocks - The root-level Yjs blocks array.\n * @return The block Y.Map if found, null otherwise.\n */\nfunction findBlockByPath(\n\tpath: AbsoluteBlockIndexPath,\n\tblocks: YBlocks\n): YBlock | null {\n\tlet currentBlocks = blocks;\n\tfor ( let i = 0; i < path.length; i++ ) {\n\t\tif ( path[ i ] >= currentBlocks.length ) {\n\t\t\treturn null;\n\t\t}\n\t\tconst block = currentBlocks.get( path[ i ] );\n\t\tif ( ! block ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( i === path.length - 1 ) {\n\t\t\treturn block;\n\t\t}\n\t\tcurrentBlocks =\n\t\t\tblock.get( 'innerBlocks' ) ?? ( new Y.Array() as YBlocks );\n\t}\n\treturn null;\n}\n\n/**\n * Create a Y.RelativePosition for a block by navigating a tree index path.\n *\n * @param path - The index path, e.g. [0, 1] for blocks[0].innerBlocks[1].\n * @param blocks - The root-level Yjs blocks array.\n * @return A Y.RelativePosition for the block, or null if the path is invalid.\n */\nfunction createRelativePositionForBlockPath(\n\tpath: AbsoluteBlockIndexPath,\n\tblocks: YBlocks\n): Y.RelativePosition | null {\n\tlet currentBlocks = blocks;\n\tfor ( let i = 0; i < path.length; i++ ) {\n\t\tif ( path[ i ] >= currentBlocks.length ) {\n\t\t\treturn null;\n\t\t}\n\t\tif ( i === path.length - 1 ) {\n\t\t\treturn Y.createRelativePositionFromTypeIndex(\n\t\t\t\tcurrentBlocks,\n\t\t\t\tpath[ i ]\n\t\t\t);\n\t\t}\n\t\tconst block = currentBlocks.get( path[ i ] );\n\t\tcurrentBlocks =\n\t\t\tblock?.get( 'innerBlocks' ) ?? ( new Y.Array() as YBlocks );\n\t}\n\treturn null;\n}\n\n/**\n * Check if two selection states are equal.\n *\n * @param selection1 - The first selection state.\n * @param selection2 - The second selection state.\n * @return True if the selection states are equal, false otherwise.\n */\nexport function areSelectionsStatesEqual(\n\tselection1: SelectionState,\n\tselection2: SelectionState\n): boolean {\n\tif ( selection1.type !== selection2.type ) {\n\t\treturn false;\n\t}\n\n\tswitch ( selection1.type ) {\n\t\tcase SelectionType.None:\n\t\t\treturn true;\n\n\t\tcase SelectionType.Cursor:\n\t\t\treturn areCursorPositionsEqual(\n\t\t\t\tselection1.cursorPosition,\n\t\t\t\t( selection2 as SelectionCursor ).cursorPosition\n\t\t\t);\n\n\t\tcase SelectionType.SelectionInOneBlock:\n\t\t\treturn (\n\t\t\t\tareCursorPositionsEqual(\n\t\t\t\t\tselection1.cursorStartPosition,\n\t\t\t\t\t( selection2 as SelectionInOneBlock ).cursorStartPosition\n\t\t\t\t) &&\n\t\t\t\tareCursorPositionsEqual(\n\t\t\t\t\tselection1.cursorEndPosition,\n\t\t\t\t\t( selection2 as SelectionInOneBlock ).cursorEndPosition\n\t\t\t\t) &&\n\t\t\t\tselection1.selectionDirection ===\n\t\t\t\t\t( selection2 as SelectionInOneBlock ).selectionDirection\n\t\t\t);\n\n\t\tcase SelectionType.SelectionInMultipleBlocks:\n\t\t\treturn (\n\t\t\t\tareCursorPositionsEqual(\n\t\t\t\t\tselection1.cursorStartPosition,\n\t\t\t\t\t( selection2 as SelectionInMultipleBlocks )\n\t\t\t\t\t\t.cursorStartPosition\n\t\t\t\t) &&\n\t\t\t\tareCursorPositionsEqual(\n\t\t\t\t\tselection1.cursorEndPosition,\n\t\t\t\t\t( selection2 as SelectionInMultipleBlocks )\n\t\t\t\t\t\t.cursorEndPosition\n\t\t\t\t) &&\n\t\t\t\tselection1.selectionDirection ===\n\t\t\t\t\t( selection2 as SelectionInMultipleBlocks )\n\t\t\t\t\t\t.selectionDirection\n\t\t\t);\n\t\tcase SelectionType.WholeBlock:\n\t\t\treturn Y.compareRelativePositions(\n\t\t\t\tselection1.blockPosition,\n\t\t\t\t( selection2 as SelectionWholeBlock ).blockPosition\n\t\t\t);\n\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/**\n * Check if two cursor positions are equal.\n *\n * @param cursorPosition1 - The first cursor position.\n * @param cursorPosition2 - The second cursor position.\n * @return True if the cursor positions are equal, false otherwise.\n */\nfunction areCursorPositionsEqual(\n\tcursorPosition1: CursorPosition,\n\tcursorPosition2: CursorPosition\n): boolean {\n\tconst isRelativePositionEqual = Y.compareRelativePositions(\n\t\tcursorPosition1.relativePosition,\n\t\tcursorPosition2.relativePosition\n\t);\n\n\t// Ensure a change in calculated absolute offset results in a treating the cursor as modified.\n\t// This is necessary because Y.Text relative positions can remain the same after text changes.\n\tconst isAbsoluteOffsetEqual =\n\t\tcursorPosition1.absoluteOffset === cursorPosition2.absoluteOffset;\n\n\treturn isRelativePositionEqual && isAbsoluteOffsetEqual;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["SelectionType", "SelectionDirection", "cursorStartPosition", "cursorEndPosition"]
7
7
  }
@@ -17,6 +17,27 @@ function asRichTextOffset(offset) {
17
17
  function asHtmlStringIndex(index) {
18
18
  return index;
19
19
  }
20
+ function getYTextByAttributeKey(attributes, attributeKey) {
21
+ const directValue = attributes.get(attributeKey);
22
+ if (directValue instanceof Y.Text) {
23
+ return directValue;
24
+ }
25
+ let value = attributes;
26
+ for (const pathPart of attributeKey.split(".")) {
27
+ if (value instanceof Y.Map) {
28
+ value = value.get(pathPart);
29
+ } else if (value instanceof Y.Array) {
30
+ const index = Number.parseInt(pathPart, 10);
31
+ if (!Number.isSafeInteger(index) || index < 0 || index.toString() !== pathPart) {
32
+ return null;
33
+ }
34
+ value = value.get(index);
35
+ } else {
36
+ return null;
37
+ }
38
+ }
39
+ return value instanceof Y.Text ? value : null;
40
+ }
20
41
  function findBlockByClientIdInDoc(blockId, ydoc) {
21
42
  const ymap = getRootMap(ydoc, CRDT_RECORD_MAP_KEY);
22
43
  const blocks = ymap.get("blocks");
@@ -98,6 +119,7 @@ export {
98
119
  createYMap,
99
120
  findBlockByClientIdInDoc,
100
121
  getRootMap,
122
+ getYTextByAttributeKey,
101
123
  htmlIndexToRichTextOffset,
102
124
  isYMap,
103
125
  richTextOffsetToHtmlIndex
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/crdt-utils.ts"],
4
- "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { Y } from '@wordpress/sync';\nimport { create, insert, toHTMLString } from '@wordpress/rich-text';\n\n/**\n * Internal dependencies\n */\nimport type { YBlock, YBlocks } from './crdt-blocks';\nimport type { YPostRecord } from './crdt';\nimport { CRDT_RECORD_MAP_KEY } from '../sync';\n\n/**\n * A YMapRecord represents the shape of the data stored in a Y.Map.\n */\nexport type YMapRecord = Record< string, unknown >;\n\n/**\n * A wrapper around Y.Map to provide type safety. The generic type accepted by\n * Y.Map represents the union of possible values of the map, which are varied in\n * many cases. This type is accurate, but its non-specificity requires aggressive\n * type narrowing or type casting / destruction with `as`.\n *\n * This type provides type enhancements so that the correct value type can be\n * inferred based on the provided key. It is just a type wrap / overlay, and\n * does not change the runtime behavior of Y.Map.\n *\n * This interface cannot extend Y.Map directly due to the limitations of\n * TypeScript's structural typing. One negative consequence of this is that\n * `instanceof` checks against Y.Map continue to work at runtime but will blur\n * the type at compile time. To navigate this, use the `isYMap` function below.\n */\nexport interface YMapWrap< T extends YMapRecord > extends Y.AbstractType< T > {\n\tdelete: < K extends keyof T >( key: K ) => void;\n\tforEach: (\n\t\tcallback: (\n\t\t\tvalue: T[ keyof T ],\n\t\t\tkey: keyof T,\n\t\t\tmap: YMapWrap< T >\n\t\t) => void\n\t) => void;\n\thas: < K extends keyof T >( key: K ) => boolean;\n\tget: < K extends keyof T >( key: K ) => T[ K ] | undefined;\n\tset: < K extends keyof T >( key: K, value: T[ K ] ) => void;\n\ttoJSON: () => T;\n\t// add types for other Y.Map methods as needed\n}\n\n/**\n * Get or create a root-level Map for the given Y.Doc. Use this instead of\n * doc.getMap() for additional type safety.\n *\n * @param doc Y.Doc\n * @param key Map key\n */\nexport function getRootMap< T extends YMapRecord >(\n\tdoc: Y.Doc,\n\tkey: string\n): YMapWrap< T > {\n\treturn doc.getMap< T >( key ) as unknown as YMapWrap< T >;\n}\n\n/**\n * Create a new Y.Map (provided with YMapWrap type), optionally initialized with\n * data. Use this instead of `new Y.Map()` for additional type safety.\n *\n * @param partial Partial data to initialize the map with.\n */\nexport function createYMap< T extends YMapRecord >(\n\tpartial: Partial< T > = {}\n): YMapWrap< T > {\n\treturn new Y.Map( Object.entries( partial ) ) as unknown as YMapWrap< T >;\n}\n\n/**\n * Type guard to check if a value is a Y.Map without losing type information.\n *\n * @param value Value to check.\n */\nexport function isYMap< T extends YMapRecord >(\n\tvalue: YMapWrap< T > | undefined\n): value is YMapWrap< T > {\n\treturn value instanceof Y.Map;\n}\n\ndeclare const richTextOffsetBrand: unique symbol;\ndeclare const htmlStringIndexBrand: unique symbol;\n\n/**\n * Branded type to prevent confusion between HTML string indices and RichText offsets.\n *\n * @see asRichTextOffset()\n */\nexport type RichTextOffset = number & {\n\treadonly [ richTextOffsetBrand ]: 'RichTextOffset';\n};\n\n/**\n * Branded type to prevent confusion between HTML string indices and RichText offsets.\n *\n * @see asHtmlStringIndex()\n */\nexport type HtmlStringIndex = number & {\n\treadonly [ htmlStringIndexBrand ]: 'HtmlStringIndex';\n};\n\n/**\n * Brand a number as an offset into a RichText\u2019s text content.\n *\n * @param offset The rich-text offset to brand.\n * @return The branded rich-text offset.\n */\nexport function asRichTextOffset( offset: number ): RichTextOffset {\n\treturn offset as RichTextOffset;\n}\n\n/**\n * Brand a number as a string index into serialized HTML.\n *\n * @param index The HTML string index to brand.\n * @return The branded HTML string index.\n */\nexport function asHtmlStringIndex( index: number ): HtmlStringIndex {\n\treturn index as HtmlStringIndex;\n}\n\n/**\n * Given a block ID and a Y.Doc, find the block in the document.\n *\n * @param blockId The block ID to find\n * @param ydoc The Y.Doc to find the block in\n * @return The block, or null if the block is not found\n */\nexport function findBlockByClientIdInDoc(\n\tblockId: string,\n\tydoc: Y.Doc\n): YBlock | null {\n\tconst ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );\n\tconst blocks = ymap.get( 'blocks' );\n\n\tif ( ! ( blocks instanceof Y.Array ) ) {\n\t\treturn null;\n\t}\n\n\treturn findBlockByClientIdInBlocks( blockId, blocks );\n}\n\n// Marker for insertion.\nconst MARKER_START = 0xe000;\n\n/**\n * Pick a marker character that does not appear in `text`. Returns the marker\n * or `null` if all candidates are present (extremely unlikely in practice).\n *\n * @param text The string to check for existing marker characters.\n */\nfunction pickMarker( text: string ): string | null {\n\tconst tryCount = 0x10;\n\n\t// Scan the unicode private use area for the first code point not present\n\t// in the text.\n\tfor ( let code = MARKER_START; code < MARKER_START + tryCount; code++ ) {\n\t\tconst candidate = String.fromCharCode( code );\n\n\t\tif ( ! text.includes( candidate ) ) {\n\t\t\treturn candidate;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n/**\n * Convert an HTML character index (counting tag characters) to a rich-text\n * offset (counting only text characters). Used on read paths where Y.Text\n * resolves to an HTML index but the block editor expects a text offset.\n *\n * @param html The full HTML string from Y.Text.\n * @param htmlIndex The HTML character index.\n * @return The corresponding rich-text offset.\n */\nexport function htmlIndexToRichTextOffset(\n\thtml: string,\n\thtmlIndex: HtmlStringIndex\n): RichTextOffset {\n\tif ( ! html.includes( '<' ) && ! html.includes( '&' ) ) {\n\t\treturn asRichTextOffset( htmlIndex );\n\t}\n\n\tconst marker = pickMarker( html );\n\tif ( ! marker ) {\n\t\treturn asRichTextOffset( htmlIndex );\n\t}\n\n\t// Insert marker and let create() do the parsing.\n\tconst withMarker =\n\t\thtml.slice( 0, htmlIndex ) + marker + html.slice( htmlIndex );\n\tconst value = create( { html: withMarker } );\n\tconst markerPos = value.text.indexOf( marker );\n\n\treturn asRichTextOffset( markerPos === -1 ? htmlIndex : markerPos );\n}\n\n/**\n * Convert a rich-text offset (counting only text characters) to an HTML\n * character index (counting tag characters). Used on write paths where the\n * block editor provides a text offset but Y.Text expects an HTML index.\n *\n * @param html The full HTML string from Y.Text.\n * @param richTextOffset The rich-text text offset.\n * @return The corresponding HTML character index.\n */\nexport function richTextOffsetToHtmlIndex(\n\thtml: string,\n\trichTextOffset: RichTextOffset\n): HtmlStringIndex {\n\tif ( ! html.includes( '<' ) && ! html.includes( '&' ) ) {\n\t\treturn asHtmlStringIndex( richTextOffset );\n\t}\n\n\tconst marker = pickMarker( html );\n\tif ( ! marker ) {\n\t\treturn asHtmlStringIndex( richTextOffset );\n\t}\n\n\tconst value = create( { html } );\n\tconst markerValue = create( { text: marker } );\n\t// The marker must inherit the formatting at the insertion point so that\n\t// toHTMLString does not split surrounding tags (e.g. <strong>) around it.\n\tif ( value.formats[ richTextOffset ] ) {\n\t\tmarkerValue.formats[ 0 ] = value.formats[ richTextOffset ];\n\t}\n\n\tconst withMarker = insert(\n\t\tvalue,\n\t\tmarkerValue,\n\t\trichTextOffset,\n\t\trichTextOffset\n\t);\n\n\tconst htmlWithMarker = toHTMLString( { value: withMarker } );\n\tconst markerIndex = htmlWithMarker.indexOf( marker );\n\treturn asHtmlStringIndex(\n\t\tmarkerIndex === -1 ? richTextOffset : markerIndex\n\t);\n}\n\nfunction findBlockByClientIdInBlocks(\n\tblockId: string,\n\tblocks: YBlocks\n): YBlock | null {\n\tfor ( const block of blocks ) {\n\t\tif ( block.get( 'clientId' ) === blockId ) {\n\t\t\treturn block;\n\t\t}\n\n\t\tconst innerBlocks = block.get( 'innerBlocks' );\n\n\t\tif ( innerBlocks && innerBlocks.length > 0 ) {\n\t\t\tconst innerBlock = findBlockByClientIdInBlocks(\n\t\t\t\tblockId,\n\t\t\t\tinnerBlocks\n\t\t\t);\n\n\t\t\tif ( innerBlock ) {\n\t\t\t\treturn innerBlock;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n"],
5
- "mappings": ";AAGA,SAAS,SAAS;AAClB,SAAS,QAAQ,QAAQ,oBAAoB;AAO7C,SAAS,2BAA2B;AA6C7B,SAAS,WACf,KACA,KACgB;AAChB,SAAO,IAAI,OAAa,GAAI;AAC7B;AAQO,SAAS,WACf,UAAwB,CAAC,GACT;AAChB,SAAO,IAAI,EAAE,IAAK,OAAO,QAAS,OAAQ,CAAE;AAC7C;AAOO,SAAS,OACf,OACyB;AACzB,SAAO,iBAAiB,EAAE;AAC3B;AA6BO,SAAS,iBAAkB,QAAiC;AAClE,SAAO;AACR;AAQO,SAAS,kBAAmB,OAAiC;AACnE,SAAO;AACR;AASO,SAAS,yBACf,SACA,MACgB;AAChB,QAAM,OAAO,WAA2B,MAAM,mBAAoB;AAClE,QAAM,SAAS,KAAK,IAAK,QAAS;AAElC,MAAK,EAAI,kBAAkB,EAAE,QAAU;AACtC,WAAO;AAAA,EACR;AAEA,SAAO,4BAA6B,SAAS,MAAO;AACrD;AAGA,IAAM,eAAe;AAQrB,SAAS,WAAY,MAA8B;AAClD,QAAM,WAAW;AAIjB,WAAU,OAAO,cAAc,OAAO,eAAe,UAAU,QAAS;AACvE,UAAM,YAAY,OAAO,aAAc,IAAK;AAE5C,QAAK,CAAE,KAAK,SAAU,SAAU,GAAI;AACnC,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SAAO;AACR;AAWO,SAAS,0BACf,MACA,WACiB;AACjB,MAAK,CAAE,KAAK,SAAU,GAAI,KAAK,CAAE,KAAK,SAAU,GAAI,GAAI;AACvD,WAAO,iBAAkB,SAAU;AAAA,EACpC;AAEA,QAAM,SAAS,WAAY,IAAK;AAChC,MAAK,CAAE,QAAS;AACf,WAAO,iBAAkB,SAAU;AAAA,EACpC;AAGA,QAAM,aACL,KAAK,MAAO,GAAG,SAAU,IAAI,SAAS,KAAK,MAAO,SAAU;AAC7D,QAAM,QAAQ,OAAQ,EAAE,MAAM,WAAW,CAAE;AAC3C,QAAM,YAAY,MAAM,KAAK,QAAS,MAAO;AAE7C,SAAO,iBAAkB,cAAc,KAAK,YAAY,SAAU;AACnE;AAWO,SAAS,0BACf,MACA,gBACkB;AAClB,MAAK,CAAE,KAAK,SAAU,GAAI,KAAK,CAAE,KAAK,SAAU,GAAI,GAAI;AACvD,WAAO,kBAAmB,cAAe;AAAA,EAC1C;AAEA,QAAM,SAAS,WAAY,IAAK;AAChC,MAAK,CAAE,QAAS;AACf,WAAO,kBAAmB,cAAe;AAAA,EAC1C;AAEA,QAAM,QAAQ,OAAQ,EAAE,KAAK,CAAE;AAC/B,QAAM,cAAc,OAAQ,EAAE,MAAM,OAAO,CAAE;AAG7C,MAAK,MAAM,QAAS,cAAe,GAAI;AACtC,gBAAY,QAAS,CAAE,IAAI,MAAM,QAAS,cAAe;AAAA,EAC1D;AAEA,QAAM,aAAa;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AAEA,QAAM,iBAAiB,aAAc,EAAE,OAAO,WAAW,CAAE;AAC3D,QAAM,cAAc,eAAe,QAAS,MAAO;AACnD,SAAO;AAAA,IACN,gBAAgB,KAAK,iBAAiB;AAAA,EACvC;AACD;AAEA,SAAS,4BACR,SACA,QACgB;AAChB,aAAY,SAAS,QAAS;AAC7B,QAAK,MAAM,IAAK,UAAW,MAAM,SAAU;AAC1C,aAAO;AAAA,IACR;AAEA,UAAM,cAAc,MAAM,IAAK,aAAc;AAE7C,QAAK,eAAe,YAAY,SAAS,GAAI;AAC5C,YAAM,aAAa;AAAA,QAClB;AAAA,QACA;AAAA,MACD;AAEA,UAAK,YAAa;AACjB,eAAO;AAAA,MACR;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AACR;",
4
+ "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { Y } from '@wordpress/sync';\nimport { create, insert, toHTMLString } from '@wordpress/rich-text';\n\n/**\n * Internal dependencies\n */\nimport type { YBlock, YBlocks } from './crdt-blocks';\nimport type { YPostRecord } from './crdt';\nimport { CRDT_RECORD_MAP_KEY } from '../sync';\n\n/**\n * A YMapRecord represents the shape of the data stored in a Y.Map.\n */\nexport type YMapRecord = Record< string, unknown >;\n\n/**\n * A wrapper around Y.Map to provide type safety. The generic type accepted by\n * Y.Map represents the union of possible values of the map, which are varied in\n * many cases. This type is accurate, but its non-specificity requires aggressive\n * type narrowing or type casting / destruction with `as`.\n *\n * This type provides type enhancements so that the correct value type can be\n * inferred based on the provided key. It is just a type wrap / overlay, and\n * does not change the runtime behavior of Y.Map.\n *\n * This interface cannot extend Y.Map directly due to the limitations of\n * TypeScript's structural typing. One negative consequence of this is that\n * `instanceof` checks against Y.Map continue to work at runtime but will blur\n * the type at compile time. To navigate this, use the `isYMap` function below.\n */\nexport interface YMapWrap< T extends YMapRecord > extends Y.AbstractType< T > {\n\tdelete: < K extends keyof T >( key: K ) => void;\n\tforEach: (\n\t\tcallback: (\n\t\t\tvalue: T[ keyof T ],\n\t\t\tkey: keyof T,\n\t\t\tmap: YMapWrap< T >\n\t\t) => void\n\t) => void;\n\thas: < K extends keyof T >( key: K ) => boolean;\n\tget: < K extends keyof T >( key: K ) => T[ K ] | undefined;\n\tset: < K extends keyof T >( key: K, value: T[ K ] ) => void;\n\ttoJSON: () => T;\n\t// add types for other Y.Map methods as needed\n}\n\n/**\n * Get or create a root-level Map for the given Y.Doc. Use this instead of\n * doc.getMap() for additional type safety.\n *\n * @param doc Y.Doc\n * @param key Map key\n */\nexport function getRootMap< T extends YMapRecord >(\n\tdoc: Y.Doc,\n\tkey: string\n): YMapWrap< T > {\n\treturn doc.getMap< T >( key ) as unknown as YMapWrap< T >;\n}\n\n/**\n * Create a new Y.Map (provided with YMapWrap type), optionally initialized with\n * data. Use this instead of `new Y.Map()` for additional type safety.\n *\n * @param partial Partial data to initialize the map with.\n */\nexport function createYMap< T extends YMapRecord >(\n\tpartial: Partial< T > = {}\n): YMapWrap< T > {\n\treturn new Y.Map( Object.entries( partial ) ) as unknown as YMapWrap< T >;\n}\n\n/**\n * Type guard to check if a value is a Y.Map without losing type information.\n *\n * @param value Value to check.\n */\nexport function isYMap< T extends YMapRecord >(\n\tvalue: YMapWrap< T > | undefined\n): value is YMapWrap< T > {\n\treturn value instanceof Y.Map;\n}\n\ndeclare const richTextOffsetBrand: unique symbol;\ndeclare const htmlStringIndexBrand: unique symbol;\n\n/**\n * Branded type to prevent confusion between HTML string indices and RichText offsets.\n *\n * @see asRichTextOffset()\n */\nexport type RichTextOffset = number & {\n\treadonly [ richTextOffsetBrand ]: 'RichTextOffset';\n};\n\n/**\n * Branded type to prevent confusion between HTML string indices and RichText offsets.\n *\n * @see asHtmlStringIndex()\n */\nexport type HtmlStringIndex = number & {\n\treadonly [ htmlStringIndexBrand ]: 'HtmlStringIndex';\n};\n\n/**\n * Brand a number as an offset into a RichText\u2019s text content.\n *\n * @param offset The rich-text offset to brand.\n * @return The branded rich-text offset.\n */\nexport function asRichTextOffset( offset: number ): RichTextOffset {\n\treturn offset as RichTextOffset;\n}\n\n/**\n * Brand a number as a string index into serialized HTML.\n *\n * @param index The HTML string index to brand.\n * @return The branded HTML string index.\n */\nexport function asHtmlStringIndex( index: number ): HtmlStringIndex {\n\treturn index as HtmlStringIndex;\n}\n\n/**\n * Resolve a selection attribute key to a Y.Text value.\n *\n * RichText identifiers are normally top-level block attribute keys, but nested\n * rich-text fields can provide a dot path such as `body.0.cells.0.content`.\n *\n * @param attributes The block attributes map.\n * @param attributeKey The top-level attribute key or nested attribute path.\n * @return The matching Y.Text, or null if the path is not a rich-text field.\n */\nexport function getYTextByAttributeKey(\n\tattributes: Y.Map< unknown >,\n\tattributeKey: string\n): Y.Text | null {\n\tconst directValue = attributes.get( attributeKey );\n\tif ( directValue instanceof Y.Text ) {\n\t\treturn directValue;\n\t}\n\n\tlet value: unknown = attributes;\n\tfor ( const pathPart of attributeKey.split( '.' ) ) {\n\t\tif ( value instanceof Y.Map ) {\n\t\t\tvalue = value.get( pathPart );\n\t\t} else if ( value instanceof Y.Array ) {\n\t\t\tconst index = Number.parseInt( pathPart, 10 );\n\t\t\tif (\n\t\t\t\t! Number.isSafeInteger( index ) ||\n\t\t\t\tindex < 0 ||\n\t\t\t\tindex.toString() !== pathPart\n\t\t\t) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tvalue = value.get( index );\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn value instanceof Y.Text ? value : null;\n}\n\n/**\n * Given a block ID and a Y.Doc, find the block in the document.\n *\n * @param blockId The block ID to find\n * @param ydoc The Y.Doc to find the block in\n * @return The block, or null if the block is not found\n */\nexport function findBlockByClientIdInDoc(\n\tblockId: string,\n\tydoc: Y.Doc\n): YBlock | null {\n\tconst ymap = getRootMap< YPostRecord >( ydoc, CRDT_RECORD_MAP_KEY );\n\tconst blocks = ymap.get( 'blocks' );\n\n\tif ( ! ( blocks instanceof Y.Array ) ) {\n\t\treturn null;\n\t}\n\n\treturn findBlockByClientIdInBlocks( blockId, blocks );\n}\n\n// Marker for insertion.\nconst MARKER_START = 0xe000;\n\n/**\n * Pick a marker character that does not appear in `text`. Returns the marker\n * or `null` if all candidates are present (extremely unlikely in practice).\n *\n * @param text The string to check for existing marker characters.\n */\nfunction pickMarker( text: string ): string | null {\n\tconst tryCount = 0x10;\n\n\t// Scan the unicode private use area for the first code point not present\n\t// in the text.\n\tfor ( let code = MARKER_START; code < MARKER_START + tryCount; code++ ) {\n\t\tconst candidate = String.fromCharCode( code );\n\n\t\tif ( ! text.includes( candidate ) ) {\n\t\t\treturn candidate;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n/**\n * Convert an HTML character index (counting tag characters) to a rich-text\n * offset (counting only text characters). Used on read paths where Y.Text\n * resolves to an HTML index but the block editor expects a text offset.\n *\n * @param html The full HTML string from Y.Text.\n * @param htmlIndex The HTML character index.\n * @return The corresponding rich-text offset.\n */\nexport function htmlIndexToRichTextOffset(\n\thtml: string,\n\thtmlIndex: HtmlStringIndex\n): RichTextOffset {\n\tif ( ! html.includes( '<' ) && ! html.includes( '&' ) ) {\n\t\treturn asRichTextOffset( htmlIndex );\n\t}\n\n\tconst marker = pickMarker( html );\n\tif ( ! marker ) {\n\t\treturn asRichTextOffset( htmlIndex );\n\t}\n\n\t// Insert marker and let create() do the parsing.\n\tconst withMarker =\n\t\thtml.slice( 0, htmlIndex ) + marker + html.slice( htmlIndex );\n\tconst value = create( { html: withMarker } );\n\tconst markerPos = value.text.indexOf( marker );\n\n\treturn asRichTextOffset( markerPos === -1 ? htmlIndex : markerPos );\n}\n\n/**\n * Convert a rich-text offset (counting only text characters) to an HTML\n * character index (counting tag characters). Used on write paths where the\n * block editor provides a text offset but Y.Text expects an HTML index.\n *\n * @param html The full HTML string from Y.Text.\n * @param richTextOffset The rich-text text offset.\n * @return The corresponding HTML character index.\n */\nexport function richTextOffsetToHtmlIndex(\n\thtml: string,\n\trichTextOffset: RichTextOffset\n): HtmlStringIndex {\n\tif ( ! html.includes( '<' ) && ! html.includes( '&' ) ) {\n\t\treturn asHtmlStringIndex( richTextOffset );\n\t}\n\n\tconst marker = pickMarker( html );\n\tif ( ! marker ) {\n\t\treturn asHtmlStringIndex( richTextOffset );\n\t}\n\n\tconst value = create( { html } );\n\tconst markerValue = create( { text: marker } );\n\t// The marker must inherit the formatting at the insertion point so that\n\t// toHTMLString does not split surrounding tags (e.g. <strong>) around it.\n\tif ( value.formats[ richTextOffset ] ) {\n\t\tmarkerValue.formats[ 0 ] = value.formats[ richTextOffset ];\n\t}\n\n\tconst withMarker = insert(\n\t\tvalue,\n\t\tmarkerValue,\n\t\trichTextOffset,\n\t\trichTextOffset\n\t);\n\n\tconst htmlWithMarker = toHTMLString( { value: withMarker } );\n\tconst markerIndex = htmlWithMarker.indexOf( marker );\n\treturn asHtmlStringIndex(\n\t\tmarkerIndex === -1 ? richTextOffset : markerIndex\n\t);\n}\n\nfunction findBlockByClientIdInBlocks(\n\tblockId: string,\n\tblocks: YBlocks\n): YBlock | null {\n\tfor ( const block of blocks ) {\n\t\tif ( block.get( 'clientId' ) === blockId ) {\n\t\t\treturn block;\n\t\t}\n\n\t\tconst innerBlocks = block.get( 'innerBlocks' );\n\n\t\tif ( innerBlocks && innerBlocks.length > 0 ) {\n\t\t\tconst innerBlock = findBlockByClientIdInBlocks(\n\t\t\t\tblockId,\n\t\t\t\tinnerBlocks\n\t\t\t);\n\n\t\t\tif ( innerBlock ) {\n\t\t\t\treturn innerBlock;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,9 @@
1
1
  // packages/core-data/src/utils/crdt.ts
2
2
  import fastDeepEqual from "fast-deep-equal/es6/index.js";
3
- import { __unstableSerializeAndClean } from "@wordpress/blocks";
3
+ import {
4
+ __unstableSerializeAndClean,
5
+ parse
6
+ } from "@wordpress/blocks";
4
7
  import {
5
8
  Y
6
9
  } from "@wordpress/sync";
@@ -53,7 +56,18 @@ function applyPostChangesToCRDTDoc(ydoc, changes, syncedProperties) {
53
56
  }
54
57
  switch (key) {
55
58
  case "blocks": {
56
- if (!newValue) {
59
+ const newCursorPosition = parseCursorSelection(
60
+ changes.selection
61
+ );
62
+ const rawContent = getRawValue(changes.content);
63
+ if (!newValue && typeof rawContent === "string") {
64
+ mergeContentWithoutBlocks(
65
+ ymap,
66
+ rawContent,
67
+ newCursorPosition
68
+ );
69
+ break;
70
+ } else if (!newValue) {
57
71
  ymap.set(key, void 0);
58
72
  break;
59
73
  }
@@ -62,9 +76,6 @@ function applyPostChangesToCRDTDoc(ydoc, changes, syncedProperties) {
62
76
  currentBlocks = new Y.Array();
63
77
  ymap.set(key, currentBlocks);
64
78
  }
65
- const newCursorPosition = parseCursorSelection(
66
- changes.selection
67
- );
68
79
  mergeCrdtBlocks(currentBlocks, newValue, newCursorPosition);
69
80
  break;
70
81
  }
@@ -130,6 +141,18 @@ function applyPostChangesToCRDTDoc(ydoc, changes, syncedProperties) {
130
141
  }, 0);
131
142
  }
132
143
  }
144
+ function mergeContentWithoutBlocks(ymap, rawContent, cursorPosition) {
145
+ let currentBlocks = ymap.get("blocks");
146
+ if (!(currentBlocks instanceof Y.Array)) {
147
+ currentBlocks = new Y.Array();
148
+ ymap.set("blocks", currentBlocks);
149
+ }
150
+ mergeCrdtBlocks(
151
+ currentBlocks,
152
+ parse(rawContent),
153
+ cursorPosition
154
+ );
155
+ }
133
156
  function parseCursorSelection(selection) {
134
157
  const selectionStart = selection?.selectionStart;
135
158
  return selectionStart?.clientId && selectionStart.attributeKey && "number" === typeof selectionStart.offset && Number.isInteger(selectionStart.offset) ? {
@@ -209,6 +232,10 @@ function getPostChangesFromCRDTDoc(ydoc, editedRecord, syncedProperties) {
209
232
  changes.blocks
210
233
  );
211
234
  }
235
+ if (changes.blocks && !changes.content) {
236
+ const capturedBlocks = changes.blocks;
237
+ changes.content = () => __unstableSerializeAndClean(capturedBlocks);
238
+ }
212
239
  if ("object" === typeof changes.meta) {
213
240
  changes.meta = {
214
241
  ...editedRecord.meta,