@wordpress/core-data
Version:
Access to and manipulation of core WordPress entities.
257 lines (256 loc) • 7.8 kB
JavaScript
// packages/core-data/src/utils/crdt-blocks.ts
import { v4 as uuidv4 } from "uuid";
import fastDeepEqual from "fast-deep-equal/es6";
import { RichTextData } from "@wordpress/rich-text";
import { Y } from "@wordpress/sync";
import { getBlockTypes } from "@wordpress/blocks";
var serializableBlocksCache = /* @__PURE__ */ new WeakMap();
function makeBlockAttributesSerializable(attributes) {
const newAttributes = { ...attributes };
for (const [key, value] of Object.entries(attributes)) {
if (value instanceof RichTextData) {
newAttributes[key] = value.valueOf();
}
}
return newAttributes;
}
function makeBlocksSerializable(blocks) {
return blocks.map((block) => {
const blockAsJson = block instanceof Y.Map ? block.toJSON() : block;
const { name, innerBlocks, attributes, ...rest } = blockAsJson;
delete rest.validationIssues;
delete rest.originalContent;
return {
...rest,
name,
attributes: makeBlockAttributesSerializable(attributes),
innerBlocks: makeBlocksSerializable(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 isRichText = isRichTextAttribute(blockName, attributeName);
if (isRichText) {
return new Y.Text(attributeValue?.toString() ?? "");
}
return attributeValue;
}
function createNewYBlock(block) {
return new Y.Map(
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, lastSelection) {
if (!serializableBlocksCache.has(incomingBlocks)) {
serializableBlocksCache.set(
incomingBlocks,
makeBlocksSerializable(incomingBlocks)
);
}
const allBlocks = serializableBlocksCache.get(incomingBlocks) ?? [];
const blocksToSync = allBlocks.filter(
(block) => shouldBlockBeSynced(block)
);
const numOfCommonEntries = Math.min(
blocksToSync.length ?? 0,
yblocks.length
);
let left = 0;
let right = 0;
for (; left < numOfCommonEntries && areBlocksEqual(blocksToSync[left], yblocks.get(left)); left++) {
}
for (; right < numOfCommonEntries - left && areBlocksEqual(
blocksToSync[blocksToSync.length - right - 1],
yblocks.get(yblocks.length - right - 1)
); right++) {
}
const numOfUpdatesNeeded = numOfCommonEntries - left - right;
const numOfInsertionsNeeded = Math.max(
0,
blocksToSync.length - yblocks.length
);
const numOfDeletionsNeeded = Math.max(
0,
yblocks.length - blocksToSync.length
);
for (let i = 0; i < numOfUpdatesNeeded; i++, left++) {
const block = blocksToSync[left];
const yblock = yblocks.get(left);
Object.entries(block).forEach(([key, value]) => {
switch (key) {
case "attributes": {
const currentAttributes = yblock.get(
key
);
if (!currentAttributes) {
yblock.set(
key,
createNewYAttributeMap(block.name, value)
);
break;
}
Object.entries(value).forEach(
([attributeName, attributeValue]) => {
if (fastDeepEqual(
currentAttributes?.get(attributeName),
attributeValue
)) {
return;
}
const isRichText = isRichTextAttribute(
block.name,
attributeName
);
if (isRichText && "string" === typeof attributeValue) {
const blockYText = currentAttributes.get(
attributeName
);
mergeRichTextUpdate(
blockYText,
attributeValue,
lastSelection
);
} else {
currentAttributes.set(
attributeName,
createNewYAttributeValue(
block.name,
attributeName,
attributeValue
)
);
}
}
);
currentAttributes.forEach(
(_attrValue, attrName) => {
if (!value.hasOwnProperty(attrName)) {
currentAttributes.delete(attrName);
}
}
);
break;
}
case "innerBlocks": {
const yInnerBlocks = yblock.get(key);
mergeCrdtBlocks(yInnerBlocks, value ?? [], lastSelection);
break;
}
default:
if (!fastDeepEqual(block[key], yblock.get(key))) {
yblock.set(key, value);
}
}
});
yblock.forEach((_v, k) => {
if (!block.hasOwnProperty(k)) {
yblock.delete(k);
}
});
}
yblocks.delete(left, numOfDeletionsNeeded);
for (let i = 0; i < numOfInsertionsNeeded; i++, left++) {
const newBlock = [createNewYBlock(blocksToSync[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 (knownClientIds.has(clientId)) {
clientId = uuidv4();
yblock.set("clientId", clientId);
}
knownClientIds.add(clientId);
}
}
function shouldBlockBeSynced(block) {
if ("core/gallery" === block.name) {
return !block.innerBlocks.some(
(innerBlock) => innerBlock.attributes && innerBlock.attributes.blob
);
}
return true;
}
var cachedRichTextAttributes;
function isRichTextAttribute(blockName, attributeName) {
if (!cachedRichTextAttributes) {
cachedRichTextAttributes = /* @__PURE__ */ new Map();
for (const blockType of getBlockTypes()) {
const richTextAttributeMap = /* @__PURE__ */ new Map();
for (const [name, definition] of Object.entries(
blockType.attributes ?? {}
)) {
if ("rich-text" === definition.type) {
richTextAttributeMap.set(name, true);
}
}
cachedRichTextAttributes.set(
blockType.name,
richTextAttributeMap
);
}
}
return cachedRichTextAttributes.get(blockName)?.has(attributeName) ?? false;
}
function mergeRichTextUpdate(blockYText, updatedValue, lastSelection) {
blockYText.delete(0, blockYText.toString().length);
blockYText.insert(0, updatedValue);
}
export {
mergeCrdtBlocks
};
//# sourceMappingURL=crdt-blocks.js.map