UNPKG

@wordpress/core-data

Version:
595 lines (593 loc) 19.9 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // packages/core-data/src/utils/crdt-blocks.ts var crdt_blocks_exports = {}; __export(crdt_blocks_exports, { deserializeBlockAttributes: () => deserializeBlockAttributes, mergeCrdtBlocks: () => mergeCrdtBlocks, mergeRichTextUpdate: () => mergeRichTextUpdate }); module.exports = __toCommonJS(crdt_blocks_exports); var import_uuid = require("uuid"); var import_es6 = __toESM(require("fast-deep-equal/es6/index.js")); var import_blocks = require("@wordpress/blocks"); var import_rich_text = require("@wordpress/rich-text"); var import_sync = require("@wordpress/sync"); var import_crdt_utils = require("./crdt-utils.cjs"); var import_crdt_text = require("./crdt-text.cjs"); var import_sync2 = require("../sync.cjs"); var serializableBlocksCache = /* @__PURE__ */ new WeakMap(); function serializeAttributeValue(value) { if (value instanceof import_rich_text.RichTextData) { return value.valueOf(); } if (Array.isArray(value)) { return value.map(serializeAttributeValue); } if (value && typeof value === "object") { const result = {}; for (const [k, v] of Object.entries(value)) { result[k] = serializeAttributeValue(v); } return result; } return value; } function makeBlockAttributesSerializable(blockName, attributes) { const newAttributes = { ...attributes }; for (const [key, value] of Object.entries(attributes)) { if (isLocalAttribute(blockName, key)) { delete newAttributes[key]; continue; } newAttributes[key] = serializeAttributeValue(value); } return newAttributes; } function makeBlocksSerializable(blocks) { return blocks.map((block) => { const { name, innerBlocks, attributes, /* * Any validation issues discovered when loading a block are appended * to the block node with a logging function, which cannot be serialized. * * @see import("@wordpress/blocks/src/api/parser").parseRawBlock() */ validationIssues, ...rest } = block; return { ...rest, name, attributes: makeBlockAttributesSerializable(name, attributes), innerBlocks: makeBlocksSerializable(innerBlocks) }; }); } function deserializeAttributeValue(schema, value) { if (schema?.type === "rich-text" && typeof value === "string") { return (0, import_crdt_text.getCachedRichTextData)(value); } if (Array.isArray(value)) { return value.map( (item) => deserializeAttributeValue(schema, item) ); } if (value && typeof value === "object") { const result = {}; for (const [key, innerValue] of Object.entries( value )) { result[key] = deserializeAttributeValue( schema?.query?.[key], innerValue ); } return result; } return value; } function deserializeBlockAttributes(blocks) { return blocks.map((block) => { const { name, innerBlocks, attributes, ...rest } = block; const newAttributes = { ...attributes }; for (const [key, value] of Object.entries(attributes)) { const schema = getBlockAttributeSchema(name, key); if (schema) { newAttributes[key] = deserializeAttributeValue( schema, value ); } } return { ...rest, name, attributes: newAttributes, innerBlocks: deserializeBlockAttributes(innerBlocks ?? []) }; }); } function areBlocksEqual(gblock, yblock) { const yblockAsJson = yblock.toJSON(); const overwrites = { innerBlocks: null, clientId: null }; const res = (0, import_es6.default)( Object.assign({}, gblock, overwrites), Object.assign({}, yblockAsJson, overwrites) ); const inners = gblock.innerBlocks || []; const yinners = yblock.get("innerBlocks"); return res && inners.length === yinners?.length && inners.every( (block, i) => areBlocksEqual(block, yinners.get(i)) ); } function createNewYAttributeMap(blockName, attributes) { return new import_sync.Y.Map( Object.entries(attributes).map( ([attributeName, attributeValue]) => { return [ attributeName, createNewYAttributeValue( blockName, attributeName, attributeValue ) ]; } ) ); } function createNewYAttributeValue(blockName, attributeName, attributeValue) { const schema = getBlockAttributeSchema(blockName, attributeName); return createYValueFromSchema(schema, attributeValue); } function createYValueFromSchema(schema, value) { if (!schema) { return value; } if (schema.type === "rich-text") { return new import_sync.Y.Text(value?.toString() ?? ""); } if (schema.type === "array" && schema.query && Array.isArray(value)) { const query = schema.query; const yArray = new import_sync.Y.Array(); yArray.insert( 0, value.map((item) => createYMapFromQuery(query, item)) ); return yArray; } if (schema.type === "object" && schema.query && isRecord(value)) { return createYMapFromQuery(schema.query, value); } return value; } function isRecord(value) { return !!value && typeof value === "object" && !Array.isArray(value); } function createYMapFromQuery(query, obj) { if (!isRecord(obj)) { return new import_sync.Y.Map(); } const entries = Object.entries(obj).map( ([key, val]) => { const subSchema = query[key]; return [key, createYValueFromSchema(subSchema, val)]; } ); return new import_sync.Y.Map(entries); } function createNewYBlock(block) { return (0, import_crdt_utils.createYMap)( Object.fromEntries( Object.entries(block).map(([key, value]) => { switch (key) { case "attributes": { return [ key, createNewYAttributeMap(block.name, value) ]; } case "innerBlocks": { const innerBlocks = new import_sync.Y.Array(); if (!Array.isArray(value)) { return [key, innerBlocks]; } innerBlocks.insert( 0, value.map( (innerBlock) => createNewYBlock(innerBlock) ) ); return [key, innerBlocks]; } default: return [key, value]; } }) ) ); } function mergeCrdtBlocks(yblocks, incomingBlocks, attributeCursor, options = {}) { if (!serializableBlocksCache.has(incomingBlocks)) { serializableBlocksCache.set( incomingBlocks, makeBlocksSerializable(incomingBlocks) ); } const incomingBlocksToSync = serializableBlocksCache.get(incomingBlocks) ?? []; const numOfCommonEntries = Math.min( incomingBlocksToSync.length ?? 0, yblocks.length ); let left = 0; let right = 0; for (; left < numOfCommonEntries && areBlocksEqual(incomingBlocksToSync[left], yblocks.get(left)); left++) { } for (; right < numOfCommonEntries - left && areBlocksEqual( incomingBlocksToSync[incomingBlocksToSync.length - right - 1], yblocks.get(yblocks.length - right - 1) ); right++) { } const numOfUpdatesNeeded = numOfCommonEntries - left - right; const numOfInsertionsNeeded = Math.max( 0, incomingBlocksToSync.length - yblocks.length ); const numOfDeletionsNeeded = Math.max( 0, yblocks.length - incomingBlocksToSync.length ); for (let i = 0; i < numOfUpdatesNeeded; i++, left++) { const incomingYBlock = incomingBlocksToSync[left]; const localYBlock = yblocks.get(left); Object.entries(incomingYBlock).forEach( ([incomingBlockProperty, incomingBlockPropertyValue]) => { switch (incomingBlockProperty) { case "attributes": { const localAttributes = localYBlock.get( incomingBlockProperty ); const incomingAttributes = incomingBlockPropertyValue; if (!localAttributes) { localYBlock.set( incomingBlockProperty, createNewYAttributeMap( incomingYBlock.name, incomingAttributes ) ); break; } Object.entries(incomingAttributes).forEach( ([ incomingAttributeName, incomingAttributeValue ]) => { const currentAttribute = localAttributes?.get( incomingAttributeName ); const isExpectedType = isExpectedAttributeType( incomingYBlock.name, incomingAttributeName, currentAttribute ); const isYType = currentAttribute instanceof import_sync.Y.AbstractType; const isAttributeChanged = !isExpectedType || isYType || !(0, import_es6.default)( currentAttribute, incomingAttributeValue ); if (isAttributeChanged) { updateYBlockAttribute( incomingYBlock.name, incomingYBlock.clientId, incomingAttributeName, incomingAttributeValue, localAttributes, attributeCursor ); } } ); localAttributes.forEach( (_attrValue, attrName) => { if (!incomingBlockPropertyValue.hasOwnProperty( attrName )) { localAttributes.delete(attrName); } } ); break; } case "innerBlocks": { let yInnerBlocks = localYBlock.get( incomingBlockProperty ); if (!(yInnerBlocks instanceof import_sync.Y.Array)) { yInnerBlocks = new import_sync.Y.Array(); localYBlock.set( incomingBlockProperty, yInnerBlocks ); } mergeCrdtBlocks( yInnerBlocks, incomingBlockPropertyValue ?? [], attributeCursor, options ); break; } case "clientId": { if (options.preserveClientIds) { break; } if (incomingBlockPropertyValue !== localYBlock.get(incomingBlockProperty)) { localYBlock.set( incomingBlockProperty, incomingBlockPropertyValue ); } break; } default: if (!(0, import_es6.default)( incomingYBlock[incomingBlockProperty], localYBlock.get(incomingBlockProperty) )) { localYBlock.set( incomingBlockProperty, incomingBlockPropertyValue ); } } } ); localYBlock.forEach((_v, k) => { if (!incomingYBlock.hasOwnProperty(k)) { localYBlock.delete(k); } }); } yblocks.delete(left, numOfDeletionsNeeded); for (let i = 0; i < numOfInsertionsNeeded; i++, left++) { const newBlock = [createNewYBlock(incomingBlocksToSync[left])]; yblocks.insert(left, newBlock); } const knownClientIds = /* @__PURE__ */ new Set(); for (let j = 0; j < yblocks.length; j++) { const yblock = yblocks.get(j); let clientId = yblock.get("clientId"); if (!clientId) { continue; } if (knownClientIds.has(clientId)) { clientId = (0, import_uuid.v4)(); yblock.set("clientId", clientId); } knownClientIds.add(clientId); } } function areArrayElementsEqual(newElement, yElement) { if (yElement instanceof import_sync.Y.Map && isRecord(newElement)) { return (0, import_es6.default)(newElement, yElement.toJSON()); } return (0, import_es6.default)(newElement, yElement); } function mergeYArray(yArray, newValue, schema, cursorPosition, cursorScope) { if (!schema.query) { return; } const query = schema.query; const numOfCommonEntries = Math.min(newValue.length, yArray.length); let left = 0; let right = 0; for (; left < numOfCommonEntries && areArrayElementsEqual(newValue[left], yArray.get(left)); left++) { } for (; right < numOfCommonEntries - left && areArrayElementsEqual( newValue[newValue.length - right - 1], yArray.get(yArray.length - right - 1) ); right++) { } const numOfUpdatesNeeded = numOfCommonEntries - left - right; for (let i = 0; i < numOfUpdatesNeeded; i++) { const currentElement = yArray.get(left + i); const newElement = newValue[left + i]; if (currentElement instanceof import_sync.Y.Map && isRecord(newElement)) { mergeYMapValues( currentElement, newElement, query, cursorPosition, cursorScope ); } else { yArray.delete(0, yArray.length); yArray.insert( 0, newValue.map((item) => createYMapFromQuery(query, item)) ); return; } } const numOfDeletionsNeeded = Math.max(0, yArray.length - newValue.length); if (numOfDeletionsNeeded > 0) { yArray.delete(left + numOfUpdatesNeeded, numOfDeletionsNeeded); } const numOfInsertionsNeeded = Math.max( 0, newValue.length - yArray.length ); if (numOfInsertionsNeeded > 0) { const insertAt = left + numOfUpdatesNeeded; const itemsToInsert = new Array( numOfInsertionsNeeded ); for (let i = 0; i < numOfInsertionsNeeded; i++) { itemsToInsert[i] = createYMapFromQuery( query, newValue[insertAt + i] ); } yArray.insert(insertAt, itemsToInsert); } } function mergeYValue(schema, newVal, yMap, key, cursorPosition, cursorScope) { const currentVal = yMap.get(key); if (schema?.type === "rich-text" && typeof newVal === "string" && currentVal instanceof import_sync.Y.Text) { mergeRichTextUpdate( currentVal, newVal, resolveRichTextCursorPosition(cursorPosition, cursorScope, newVal) ); } else if (schema?.type === "array" && schema.query && Array.isArray(newVal) && currentVal instanceof import_sync.Y.Array) { mergeYArray(currentVal, newVal, schema, cursorPosition, cursorScope); } else if (schema?.type === "object" && schema.query && isRecord(newVal) && currentVal instanceof import_sync.Y.Map) { mergeYMapValues( currentVal, newVal, schema.query, cursorPosition, cursorScope ); } else { const newYValue = createYValueFromSchema(schema, newVal); if (newYValue !== newVal || !(0, import_es6.default)(currentVal, newVal)) { yMap.set(key, newYValue); } } } function mergeYMapValues(yMap, newObj, query, cursorPosition, cursorScope) { for (const [key, newVal] of Object.entries(newObj)) { mergeYValue( query[key], newVal, yMap, key, cursorPosition, cursorScope ); } for (const key of yMap.keys()) { if (!Object.hasOwn(newObj, key)) { yMap.delete(key); } } } function updateYBlockAttribute(blockName, clientId, attributeName, attributeValue, currentAttributes, newCursorPosition) { const schema = getBlockAttributeSchema(blockName, attributeName); mergeYValue( schema, attributeValue, currentAttributes, attributeName, newCursorPosition, { attributeKey: attributeName, clientId } ); } function resolveRichTextCursorPosition(cursorPosition, cursorScope, updatedValue) { return cursorPosition && cursorPosition.clientId === cursorScope.clientId && cursorPosition.attributeKey === cursorScope.attributeKey && "number" === typeof cursorPosition.offset && Number.isInteger(cursorPosition.offset) ? (0, import_crdt_utils.richTextOffsetToHtmlIndex)( updatedValue, (0, import_crdt_utils.asRichTextOffset)(cursorPosition.offset) ) : null; } var cachedBlockAttributeSchemas; function getBlockAttributeSchema(blockName, attributeName) { if (!cachedBlockAttributeSchemas) { cachedBlockAttributeSchemas = /* @__PURE__ */ new Map(); for (const blockType of (0, import_blocks.getBlockTypes)()) { cachedBlockAttributeSchemas.set( blockType.name, new Map( Object.entries(blockType.attributes ?? {}).map( ([name, definition]) => { const { role, type, query } = definition; return [name, { role, type, query }]; } ) ) ); } } return cachedBlockAttributeSchemas.get(blockName)?.get(attributeName); } function isExpectedAttributeType(blockName, attributeName, attributeValue) { const schema = getBlockAttributeSchema(blockName, attributeName); if (schema?.type === "rich-text") { return attributeValue instanceof import_sync.Y.Text; } if (schema?.type === "string") { return typeof attributeValue === "string"; } if (schema?.type === "array" && schema.query) { return attributeValue instanceof import_sync.Y.Array; } if (schema?.type === "object" && schema.query) { return attributeValue instanceof import_sync.Y.Map; } return true; } function isLocalAttribute(blockName, attributeName) { return "local" === getBlockAttributeSchema(blockName, attributeName)?.role; } var localDoc; function mergeRichTextUpdate(blockYText, updatedValue, htmlCursorIndex = null) { const currentValueAsDelta = new import_sync2.Delta(blockYText.toDelta()); const updatedValueAsDelta = new import_sync2.Delta([{ insert: updatedValue }]); const deltaDiff = currentValueAsDelta.diffWithCursor( updatedValueAsDelta, htmlCursorIndex ); const safeDiff = htmlCursorIndex === null || isDeltaVerificationMatch(blockYText, deltaDiff, updatedValue) ? deltaDiff : currentValueAsDelta.diff(updatedValueAsDelta); blockYText.applyDelta(safeDiff.ops); } function isDeltaVerificationMatch(blockYText, delta, expectedValue) { if (!localDoc) { localDoc = new import_sync.Y.Doc(); } const verificationYText = localDoc.getText("verification-text"); verificationYText.delete(0, verificationYText.length); verificationYText.insert(0, blockYText.toString()); verificationYText.applyDelta(delta.ops); return verificationYText.toString() === expectedValue; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { deserializeBlockAttributes, mergeCrdtBlocks, mergeRichTextUpdate }); //# sourceMappingURL=crdt-blocks.cjs.map