@wordpress/core-data
Version:
Access to and manipulation of core WordPress entities.
562 lines (561 loc) • 17.7 kB
JavaScript
// packages/core-data/src/utils/crdt-blocks.ts
import { v4 as uuidv4 } from "uuid";
import fastDeepEqual from "fast-deep-equal/es6/index.js";
import { getBlockTypes } from "@wordpress/blocks";
import { RichTextData } from "@wordpress/rich-text";
import { Y } from "@wordpress/sync";
import {
asRichTextOffset,
createYMap,
richTextOffsetToHtmlIndex
} from "./crdt-utils.mjs";
import { getCachedRichTextData } from "./crdt-text.mjs";
import { Delta } from "../sync.mjs";
var serializableBlocksCache = /* @__PURE__ */ new WeakMap();
function serializeAttributeValue(value) {
if (value instanceof 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 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 = fastDeepEqual(
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 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 Y.Text(value?.toString() ?? "");
}
if (schema.type === "array" && schema.query && Array.isArray(value)) {
const query = schema.query;
const yArray = new 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 Y.Map();
}
const entries = Object.entries(obj).map(
([key, val]) => {
const subSchema = query[key];
return [key, createYValueFromSchema(subSchema, val)];
}
);
return new Y.Map(entries);
}
function createNewYBlock(block) {
return createYMap(
Object.fromEntries(
Object.entries(block).map(([key, value]) => {
switch (key) {
case "attributes": {
return [
key,
createNewYAttributeMap(block.name, value)
];
}
case "innerBlocks": {
const innerBlocks = new 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 Y.AbstractType;
const isAttributeChanged = !isExpectedType || isYType || !fastDeepEqual(
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 Y.Array)) {
yInnerBlocks = new 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 (!fastDeepEqual(
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 = uuidv4();
yblock.set("clientId", clientId);
}
knownClientIds.add(clientId);
}
}
function areArrayElementsEqual(newElement, yElement) {
if (yElement instanceof Y.Map && isRecord(newElement)) {
return fastDeepEqual(newElement, yElement.toJSON());
}
return fastDeepEqual(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 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 Y.Text) {
mergeRichTextUpdate(
currentVal,
newVal,
resolveRichTextCursorPosition(cursorPosition, cursorScope, newVal)
);
} else if (schema?.type === "array" && schema.query && Array.isArray(newVal) && currentVal instanceof Y.Array) {
mergeYArray(currentVal, newVal, schema, cursorPosition, cursorScope);
} else if (schema?.type === "object" && schema.query && isRecord(newVal) && currentVal instanceof Y.Map) {
mergeYMapValues(
currentVal,
newVal,
schema.query,
cursorPosition,
cursorScope
);
} else {
const newYValue = createYValueFromSchema(schema, newVal);
if (newYValue !== newVal || !fastDeepEqual(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) ? richTextOffsetToHtmlIndex(
updatedValue,
asRichTextOffset(cursorPosition.offset)
) : null;
}
var cachedBlockAttributeSchemas;
function getBlockAttributeSchema(blockName, attributeName) {
if (!cachedBlockAttributeSchemas) {
cachedBlockAttributeSchemas = /* @__PURE__ */ new Map();
for (const blockType of 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 Y.Text;
}
if (schema?.type === "string") {
return typeof attributeValue === "string";
}
if (schema?.type === "array" && schema.query) {
return attributeValue instanceof Y.Array;
}
if (schema?.type === "object" && schema.query) {
return attributeValue instanceof 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 Delta(blockYText.toDelta());
const updatedValueAsDelta = new 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 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;
}
export {
deserializeBlockAttributes,
mergeCrdtBlocks,
mergeRichTextUpdate
};
//# sourceMappingURL=crdt-blocks.mjs.map