diff --git a/docs/package.json b/docs/package.json index b6f8797a88..ca411c6752 100644 --- a/docs/package.json +++ b/docs/package.json @@ -100,10 +100,10 @@ "zod": "^4.3.5", "@y/protocols": "^1.0.6-rc.1", "@y/websocket": "^4.0.0-3", - "@y/y": "^14.0.0-rc.16", + "@y/y": "^14.0.0-rc.17", "@y/prosemirror": "^2.0.0-2", "@floating-ui/react": "^0.27.18", - "lib0": "1.0.0-rc.13", + "lib0": "1.0.0-rc.14", "y-websocket": "^2.1.0" }, "devDependencies": { diff --git a/examples/07-collaboration/10-versioning/package.json b/examples/07-collaboration/10-versioning/package.json index e5175a458f..2a182ca1b3 100644 --- a/examples/07-collaboration/10-versioning/package.json +++ b/examples/07-collaboration/10-versioning/package.json @@ -22,10 +22,10 @@ "react-dom": "^19.2.3", "@y/protocols": "^1.0.6-rc.1", "@y/websocket": "^4.0.0-3", - "@y/y": "^14.0.0-rc.16", + "@y/y": "^14.0.0-rc.17", "react-icons": "5.6.0", "@floating-ui/react": "^0.27.18", - "lib0": "1.0.0-rc.13" + "lib0": "1.0.0-rc.14" }, "devDependencies": { "@types/react": "^19.2.3", diff --git a/examples/07-collaboration/11-yhub/package.json b/examples/07-collaboration/11-yhub/package.json index 729f179c12..211f583257 100644 --- a/examples/07-collaboration/11-yhub/package.json +++ b/examples/07-collaboration/11-yhub/package.json @@ -21,7 +21,7 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "@y/protocols": "^1.0.6-rc.1", - "@y/y": "^14.0.0-rc.16", + "@y/y": "^14.0.0-rc.17", "@y/prosemirror": "^2.0.0-2", "@y/websocket": "^4.0.0-rc.2" }, diff --git a/examples/07-collaboration/13-versioning-yjs14/package.json b/examples/07-collaboration/13-versioning-yjs14/package.json index bb5df483b6..058f49ee8d 100644 --- a/examples/07-collaboration/13-versioning-yjs14/package.json +++ b/examples/07-collaboration/13-versioning-yjs14/package.json @@ -22,8 +22,8 @@ "react-dom": "^19.2.3", "@y/protocols": "^1.0.6-rc.1", "@y/websocket": "^4.0.0-3", - "@y/y": "^14.0.0-rc.16", - "lib0": "1.0.0-rc.13" + "@y/y": "^14.0.0-rc.17", + "lib0": "1.0.0-rc.14" }, "devDependencies": { "@types/react": "^19.2.3", diff --git a/packages/core/package.json b/packages/core/package.json index 8fa253ca3a..28c0e95191 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -112,7 +112,7 @@ "@tiptap/pm": "^3.13.0", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", - "lib0": "1.0.0-rc.13", + "lib0": "1.0.0-rc.14", "prosemirror-highlight": "^0.15.1", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", @@ -137,7 +137,7 @@ "y-prosemirror": "^1.3.7", "y-protocols": "^1.0.6", "yjs": "^13.6.27", - "@y/y": "^14.0.0-rc.16", + "@y/y": "^14.0.0-rc.17", "@y/prosemirror": "^2.0.0-2", "@y/protocols": "^1.0.6-rc.1" }, diff --git a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts index 25debee60c..b41b268617 100644 --- a/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts @@ -49,7 +49,7 @@ export function insertBlocks< // Now that the `PartialBlock`s have been converted to nodes, we can // re-convert them into full `Block`s. const insertedBlocks = nodesToInsert.map((node) => - nodeToBlock(node, pmSchema), + nodeToBlock(node, tr.doc), ) as Block[]; return insertedBlocks; diff --git a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts index 654fbfdeba..4da3d5151a 100644 --- a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { getBlockInfoFromTransaction } from "../../../getBlockInfoFromPos.js"; +import { getBlockInfoFromSelection } from "../../../getBlockInfoFromPos.js"; import { setupTestEnv } from "../../setupTestEnv.js"; import { getParentBlockInfo, mergeBlocksCommand } from "./mergeBlocks.js"; @@ -14,7 +14,7 @@ function mergeBlocks(posBetweenBlocks: number) { function getPosBeforeSelectedBlock() { return getEditor().transact( - (tr) => getBlockInfoFromTransaction(tr).bnBlock.beforePos, + (tr) => getBlockInfoFromSelection(tr).bnBlock.beforePos, ); } diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts index 763de289c5..c68ccb4a6d 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.test.ts @@ -3,8 +3,9 @@ import { CellSelection } from "prosemirror-tables"; import { describe, expect, it } from "vitest"; import { - getBlockInfoFromTransaction, - getNearestBlockPos, + getBlockInfoAt, + getBlockInfoFromSelection, + getNodeId, } from "../../../getBlockInfoFromPos.js"; import { setupTestEnv } from "../../setupTestEnv.js"; import { @@ -16,9 +17,7 @@ import { const getEditor = setupTestEnv(); function makeSelectionSpanContent(selectionType: "text" | "node" | "cell") { - const blockInfo = getEditor().transact((tr) => - getBlockInfoFromTransaction(tr), - ); + const blockInfo = getEditor().transact((tr) => getBlockInfoFromSelection(tr)); if (!blockInfo.isBlockContainer) { throw new Error( `Selection points to a ${blockInfo.blockNoteType} node, not a blockContainer node`, @@ -222,13 +221,10 @@ describe("Test moveBlocksUp", () => { moveBlocksUp(getEditor(), "paragraph-2"); - const { anchor, head } = getEditor().transact((tr) => tr.selection); - const anchorBlockId = getEditor().transact( - (tr) => getNearestBlockPos(tr.doc, anchor).node.attrs.id, - ); - const headBlockId = getEditor().transact( - (tr) => getNearestBlockPos(tr.doc, head).node.attrs.id, - ); + const { anchorBlockId, headBlockId } = getEditor().transact((tr) => ({ + anchorBlockId: getNodeId(getBlockInfoAt(tr, tr.selection.anchor).bnBlock.node, tr.doc), + headBlockId: getNodeId(getBlockInfoAt(tr, tr.selection.head).bnBlock.node, tr.doc), + })); expect(anchorBlockId).toBe("paragraph-1"); expect(headBlockId).toBe("paragraph-1"); }); @@ -343,13 +339,10 @@ describe("Test moveBlocksDown", () => { moveBlocksDown(getEditor(), "paragraph-0"); - const { anchor, head } = getEditor().transact((tr) => tr.selection); - const anchorBlockId = getEditor().transact( - (tr) => getNearestBlockPos(tr.doc, anchor).node.attrs.id, - ); - const headBlockId = getEditor().transact( - (tr) => getNearestBlockPos(tr.doc, head).node.attrs.id, - ); + const { anchorBlockId, headBlockId } = getEditor().transact((tr) => ({ + anchorBlockId: getNodeId(getBlockInfoAt(tr, tr.selection.anchor).bnBlock.node, tr.doc), + headBlockId: getNodeId(getBlockInfoAt(tr, tr.selection.head).bnBlock.node, tr.doc), + })); expect(anchorBlockId).toBe("paragraph-1"); expect(headBlockId).toBe("paragraph-1"); }); diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts index bb2f08dfca..91dd9705cf 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts @@ -9,7 +9,7 @@ import { CellSelection } from "prosemirror-tables"; import { Block } from "../../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; import { BlockIdentifier } from "../../../../schema/index.js"; -import { getNearestBlockPos } from "../../../getBlockInfoFromPos.js"; +import { getBlockInfoAt, getNodeId } from "../../../getBlockInfoFromPos.js"; import { getNodeById } from "../../../nodeUtil.js"; type BlockSelectionData = ( @@ -44,31 +44,34 @@ function getBlockSelectionData( editor: BlockNoteEditor, ): BlockSelectionData { return editor.transact((tr) => { - const anchorBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.anchor); + const anchorBlockPosInfo = getBlockInfoAt(tr, tr.selection.anchor); + + const anchorBlockId = getNodeId(anchorBlockPosInfo.bnBlock.node, tr.doc); if (tr.selection instanceof CellSelection) { return { type: "cell" as const, - anchorBlockId: anchorBlockPosInfo.node.attrs.id, + anchorBlockId, anchorCellOffset: - tr.selection.$anchorCell.pos - anchorBlockPosInfo.posBeforeNode, + tr.selection.$anchorCell.pos - anchorBlockPosInfo.bnBlock.beforePos, headCellOffset: - tr.selection.$headCell.pos - anchorBlockPosInfo.posBeforeNode, + tr.selection.$headCell.pos - anchorBlockPosInfo.bnBlock.beforePos, }; } else if (tr.selection instanceof NodeSelection) { return { type: "node" as const, - anchorBlockId: anchorBlockPosInfo.node.attrs.id, + anchorBlockId, }; } else { - const headBlockPosInfo = getNearestBlockPos(tr.doc, tr.selection.head); + const headBlockPosInfo = getBlockInfoAt(tr, tr.selection.head); return { type: "text" as const, - anchorBlockId: anchorBlockPosInfo.node.attrs.id, - headBlockId: headBlockPosInfo.node.attrs.id, - anchorOffset: tr.selection.anchor - anchorBlockPosInfo.posBeforeNode, - headOffset: tr.selection.head - headBlockPosInfo.posBeforeNode, + anchorBlockId, + headBlockId: getNodeId(headBlockPosInfo.bnBlock.node, tr.doc), + anchorOffset: + tr.selection.anchor - anchorBlockPosInfo.bnBlock.beforePos, + headOffset: tr.selection.head - headBlockPosInfo.bnBlock.beforePos, }; } }); diff --git a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts index c995faeda1..a0f76fdff0 100644 --- a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts @@ -3,7 +3,7 @@ import { Transaction } from "prosemirror-state"; import { canJoin, liftTarget, ReplaceAroundStep } from "prosemirror-transform"; import { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js"; -import { getBlockInfoFromTransaction } from "../../../getBlockInfoFromPos.js"; +import { getBlockInfoFromSelection } from "../../../getBlockInfoFromPos.js"; /** * Modified version of prosemirror-schema-list's sinkItem. @@ -15,11 +15,7 @@ import { getBlockInfoFromTransaction } from "../../../getBlockInfoFromPos.js"; * 3. Slice creates groupType instead of parent.type * 4. Operates on Transaction directly instead of state+dispatch */ -function sinkItem( - tr: Transaction, - itemType: NodeType, - groupType: NodeType, -) { +function sinkItem(tr: Transaction, itemType: NodeType, groupType: NodeType) { const { $from, $to } = tr.selection; const range = $from.blockRange( $to, @@ -197,7 +193,7 @@ export function unnestBlock(editor: BlockNoteEditor) { export function canNestBlock(editor: BlockNoteEditor) { return editor.transact((tr) => { - const { bnBlock: blockContainer } = getBlockInfoFromTransaction(tr); + const { bnBlock: blockContainer } = getBlockInfoFromSelection(tr); return tr.doc.resolve(blockContainer.beforePos).nodeBefore !== null; }); @@ -205,7 +201,7 @@ export function canNestBlock(editor: BlockNoteEditor) { export function canUnnestBlock(editor: BlockNoteEditor) { return editor.transact((tr) => { - const { bnBlock: blockContainer } = getBlockInfoFromTransaction(tr); + const { bnBlock: blockContainer } = getBlockInfoFromSelection(tr); return tr.doc.resolve(blockContainer.beforePos).depth > 1; }); diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts index f1e946f909..b6a280b330 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts @@ -1,6 +1,7 @@ import { type Node } from "prosemirror-model"; import { type Transaction } from "prosemirror-state"; import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js"; +import { getNodeId } from "../../../getBlockInfoFromPos.js"; import type { BlockIdentifier, BlockSchema, @@ -54,18 +55,21 @@ export function removeAndInsertBlocks< } // Keeps traversing nodes if block with target ID has not been found. - if ( - !node.type.isInGroup("bnBlock") || - !idsOfBlocksToRemove.has(node.attrs.id) - ) { + if (!node.type.isInGroup("bnBlock")) { + return true; + } + + const nodeId = getNodeId(node, tr.doc); + + if (!idsOfBlocksToRemove.has(nodeId)) { return true; } // Saves the block that is being deleted. - removedBlocks.push(nodeToBlock(node, pmSchema)); - idsOfBlocksToRemove.delete(node.attrs.id); + removedBlocks.push(nodeToBlock(node, tr.doc)); + idsOfBlocksToRemove.delete(nodeId); - if (blocksToInsert.length > 0 && node.attrs.id === idOfFirstBlock) { + if (blocksToInsert.length > 0 && nodeId === idOfFirstBlock) { const oldDocSize = tr.doc.nodeSize; tr.insert(pos, nodesToInsert); const newDocSize = tr.doc.nodeSize; @@ -116,7 +120,7 @@ export function removeAndInsertBlocks< // Converts the nodes created from `blocksToInsert` into full `Block`s. const insertedBlocks = nodesToInsert.map((node) => - nodeToBlock(node, pmSchema), + nodeToBlock(node, tr.doc), ) as Block[]; return { insertedBlocks, removedBlocks }; diff --git a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts index 4165ebc98d..80e3dc7f97 100644 --- a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts +++ b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.test.ts @@ -4,7 +4,8 @@ import { describe, expect, it } from "vitest"; import { getBlockInfo, - getBlockInfoFromTransaction, + getBlockInfoFromSelection, + getNodeId, } from "../../../getBlockInfoFromPos.js"; import { getNodeById } from "../../../nodeUtil.js"; import { setupTestEnv } from "../../setupTestEnv.js"; @@ -137,12 +138,12 @@ describe("Test splitBlocks", () => { splitBlock(getEditor().transact((tr) => tr.selection.anchor)); - const bnBlock = getEditor().transact( - (tr) => getBlockInfoFromTransaction(tr).bnBlock, + const blockId = getEditor().transact( + (tr) => getNodeId(getBlockInfoFromSelection(tr).bnBlock.node, tr.doc), ); const anchorIsAtStartOfNewBlock = - bnBlock.node.attrs.id === "0" && + blockId === "0" && getEditor().transact((tr) => tr.selection.$anchor.parentOffset) === 0; expect(anchorIsAtStartOfNewBlock).toBeTruthy(); diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index a3e2b3b0db..b1faf875f5 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -127,7 +127,7 @@ export function updateBlockTr< // currently, we calculate the new node and replace the entire node with the desired new node. // for this, we do a nodeToBlock on the existing block to get the children. // it would be cleaner to use a ReplaceAroundStep, but this is a bit simpler and it's quite an edge case - const existingBlock = nodeToBlock(blockInfo.bnBlock.node, pmSchema); + const existingBlock = nodeToBlock(blockInfo.bnBlock.node, tr.doc); const replacementNode = blockToNode( { children: existingBlock.children, // if no children are passed in, use existing children @@ -340,8 +340,7 @@ export function updateBlock< .resolve(posInfo.posBeforeNode + 1) // TODO: clean? .node(); - const pmSchema = getPmSchema(tr); - return nodeToBlock(blockContainerNode, pmSchema); + return nodeToBlock(blockContainerNode, tr.doc); } type CellAnchor = { row: number; col: number; offset: number }; diff --git a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts index c018c907a5..1d87f58b49 100644 --- a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts +++ b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts @@ -8,7 +8,6 @@ import type { } from "../../../schema/index.js"; import { nodeToBlock } from "../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../nodeUtil.js"; -import { getPmSchema } from "../../pmUtil.js"; export function getBlock< BSchema extends BlockSchema, @@ -20,14 +19,13 @@ export function getBlock< ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const pmSchema = getPmSchema(doc); const posInfo = getNodeById(id, doc); if (!posInfo) { return undefined; } - return nodeToBlock(posInfo.node, pmSchema); + return nodeToBlock(posInfo.node, doc); } export function getPrevBlock< @@ -42,7 +40,6 @@ export function getPrevBlock< typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; const posInfo = getNodeById(id, doc); - const pmSchema = getPmSchema(doc); if (!posInfo) { return undefined; } @@ -53,7 +50,7 @@ export function getPrevBlock< return undefined; } - return nodeToBlock(nodeToConvert, pmSchema); + return nodeToBlock(nodeToConvert, doc); } export function getNextBlock< @@ -67,7 +64,6 @@ export function getNextBlock< const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; const posInfo = getNodeById(id, doc); - const pmSchema = getPmSchema(doc); if (!posInfo) { return undefined; } @@ -80,7 +76,7 @@ export function getNextBlock< return undefined; } - return nodeToBlock(nodeToConvert, pmSchema); + return nodeToBlock(nodeToConvert, doc); } export function getParentBlock< @@ -93,7 +89,6 @@ export function getParentBlock< ): Block | undefined { const id = typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; - const pmSchema = getPmSchema(doc); const posInfo = getNodeById(id, doc); if (!posInfo) { return undefined; @@ -112,5 +107,5 @@ export function getParentBlock< return undefined; } - return nodeToBlock(nodeToConvert, pmSchema); + return nodeToBlock(nodeToConvert, doc); } diff --git a/packages/core/src/api/blockManipulation/selections/selection.ts b/packages/core/src/api/blockManipulation/selections/selection.ts index e5bd761918..b9f7f27570 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.ts @@ -22,7 +22,6 @@ export function getSelection< I extends InlineContentSchema, S extends StyleSchema, >(tr: Transaction): Selection | undefined { - const pmSchema = getPmSchema(tr); // Return undefined if the selection is collapsed or a node is selected. if (tr.selection.empty || "node" in tr.selection) { return undefined; @@ -51,7 +50,7 @@ export function getSelection< ); } - return nodeToBlock(node, pmSchema); + return nodeToBlock(node, tr.doc); }; const blocks: Block[] = []; @@ -92,7 +91,7 @@ export function getSelection< // [ id-2, id-3, id-4, id-6, id-7, id-8, id-9 ] if ($startBlockBeforePos.depth > sharedDepth) { // Adds the block that the selection starts in. - blocks.push(nodeToBlock($startBlockBeforePos.nodeAfter!, pmSchema)); + blocks.push(nodeToBlock($startBlockBeforePos.nodeAfter!, tr.doc)); // Traverses all depths from the depth of the block in which the selection // starts, up to the shared depth. @@ -223,8 +222,6 @@ export function setSelection( export function getSelectionCutBlocks(tr: Transaction, expandToWords = false) { // TODO: fix image node selection - const pmSchema = getPmSchema(tr); - const range = expandToWords ? expandPMRangeToWords(tr.doc, tr.selection) : tr.selection; @@ -257,7 +254,6 @@ export function getSelectionCutBlocks(tr: Transaction, expandToWords = false) { const selectionInfo = prosemirrorSliceToSlicedBlocks( tr.doc.slice(start.pos, end.pos, true), - pmSchema, ); return { diff --git a/packages/core/src/api/blockManipulation/selections/textCursorPosition.ts b/packages/core/src/api/blockManipulation/selections/textCursorPosition.ts index 83f5340698..5de7b6c20d 100644 --- a/packages/core/src/api/blockManipulation/selections/textCursorPosition.ts +++ b/packages/core/src/api/blockManipulation/selections/textCursorPosition.ts @@ -14,7 +14,8 @@ import type { import { UnreachableCaseError } from "../../../util/typescript.js"; import { getBlockInfo, - getBlockInfoFromTransaction, + getBlockInfoFromSelection, + getNodeId, } from "../../getBlockInfoFromPos.js"; import { nodeToBlock } from "../../nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../nodeUtil.js"; @@ -25,8 +26,7 @@ export function getTextCursorPosition< I extends InlineContentSchema, S extends StyleSchema, >(tr: Transaction): TextCursorPosition { - const { bnBlock } = getBlockInfoFromTransaction(tr); - const pmSchema = getPmSchema(tr.doc); + const { bnBlock } = getBlockInfoFromSelection(tr); const resolvedPos = tr.doc.resolve(bnBlock.beforePos); // Gets previous blockContainer node at the same nesting level, if the current node isn't the first child. @@ -47,11 +47,11 @@ export function getTextCursorPosition< } return { - block: nodeToBlock(bnBlock.node, pmSchema), - prevBlock: prevNode === null ? undefined : nodeToBlock(prevNode, pmSchema), - nextBlock: nextNode === null ? undefined : nodeToBlock(nextNode, pmSchema), + block: nodeToBlock(bnBlock.node, tr.doc), + prevBlock: prevNode === null ? undefined : nodeToBlock(prevNode, tr.doc), + nextBlock: nextNode === null ? undefined : nodeToBlock(nextNode, tr.doc), parentBlock: - parentNode === undefined ? undefined : nodeToBlock(parentNode, pmSchema), + parentNode === undefined ? undefined : nodeToBlock(parentNode, tr.doc), }; } @@ -113,6 +113,6 @@ export function setTextCursorPosition( ? info.childContainer.node.firstChild! : info.childContainer.node.lastChild!; - setTextCursorPosition(tr, child.attrs.id, placement); + setTextCursorPosition(tr, getNodeId(child, tr.doc), placement); } } diff --git a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts index ced8f59b14..b03c0d6013 100644 --- a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts @@ -5,7 +5,7 @@ import { InlineContentSchema, StyleSchema, } from "../../../schema/index.js"; -import { getNearestBlockPos } from "../../getBlockInfoFromPos.js"; +import { getBlockInfoAt, getNodeId } from "../../getBlockInfoFromPos.js"; import { acceptedMIMETypes } from "./acceptedMIMETypes.js"; function checkFileExtensionsMatch( @@ -159,16 +159,18 @@ export async function handleFileInsertion< } insertedBlockId = editor.transact((tr) => { - const posInfo = getNearestBlockPos(tr.doc, pos.pos); + const blockInfo = getBlockInfoAt(tr, pos.pos); + const id = getNodeId(blockInfo.bnBlock.node, tr.doc); + // TODO are these safe? const blockElement = editor.domElement?.querySelector( - `[data-id="${posInfo.node.attrs.id}"]`, + `[data-id="${id}"]`, ); const blockRect = blockElement?.getBoundingClientRect(); return insertOrUpdateBlock( editor, - editor.getBlock(posInfo.node.attrs.id)!, + editor.getBlock(id)!, fileBlock, blockRect && (blockRect.top + blockRect.bottom) / 2 > coords.top ? "before" diff --git a/packages/core/src/api/getBlockInfoFromPos.test.ts b/packages/core/src/api/getBlockInfoFromPos.test.ts new file mode 100644 index 0000000000..ff0eebdec5 --- /dev/null +++ b/packages/core/src/api/getBlockInfoFromPos.test.ts @@ -0,0 +1,249 @@ +import { Schema } from "prosemirror-model"; +import { describe, expect, it } from "vitest"; + +import { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; +import { docToBlocks } from "./nodeConversions/nodeToBlock.js"; +import { getNodeId } from "./getBlockInfoFromPos.js"; + +/** + * Builds a `blockContainer` node holding a single paragraph with the given + * block `id`. When `suggestedDelete` is true, the container carries a + * `y-attributed-delete` mark, simulating a node that Yjs keeps in the document + * (in suggestion mode) after it has been deleted. + */ +function makeBlockContainer( + schema: Schema, + id: string, + text: string, + suggestedDelete: boolean, +) { + const paragraph = schema.nodes["paragraph"].createChecked( + {}, + text ? schema.text(text) : null, + ); + const marks = suggestedDelete + ? [schema.marks["y-attributed-delete"].create({ id: 1 })] + : undefined; + + return schema.nodes["blockContainer"].createChecked({ id }, paragraph, marks); +} + +describe("getNodeId", () => { + let editor: BlockNoteEditor; + + // We only need the editor's ProseMirror schema to construct nodes, so a + // single non-mounted editor instance is enough for all cases here. + function getSchema() { + if (!editor) { + editor = BlockNoteEditor.create(); + } + return editor.pmSchema; + } + + it("returns the plain id for a normal block", () => { + const schema = getSchema(); + const block = makeBlockContainer(schema, "0", "Hello", false); + const doc = schema.nodes["doc"].createChecked( + {}, + schema.nodes["blockGroup"].createChecked({}, block), + ); + + // The only descendant blockContainer with id "0" is the one we built. + const blockContainer = doc.firstChild!.firstChild!; + + expect(getNodeId(blockContainer, doc)).toBe("0"); + }); + + it("throws when a node has no id", () => { + const schema = getSchema(); + // `create` (not `createChecked`) so we can omit the id attr default lying. + const block = schema.nodes["blockContainer"].create( + { id: null }, + schema.nodes["paragraph"].createChecked({}, schema.text("No id")), + ); + + expect(() => getNodeId(block, block)).toThrow(/does not have an ID/); + }); + + it("lies about the id of a suggested-deletion block to disambiguate duplicates", () => { + const schema = getSchema(); + + // First block: a "real" block with id "0". + const liveBlock = makeBlockContainer(schema, "0", "Live", false); + // Second block: a suggested deletion that, in suggestion mode, shares the + // SAME id "0" as the live block but carries a y-attributed-delete mark. + const deletedBlock = makeBlockContainer(schema, "0", "Deleted", true); + + const doc = schema.nodes["doc"].createChecked( + {}, + schema.nodes["blockGroup"].createChecked({}, [liveBlock, deletedBlock]), + ); + + const blockGroup = doc.firstChild!; + const liveNode = blockGroup.child(0); + const deletedNode = blockGroup.child(1); + + // The live block keeps its plain id. + expect(getNodeId(liveNode, doc)).toBe("0"); + // The suggested-deletion block is disambiguated: it is preceded by one + // node with the same id, so its index is 1 -> "0-1". + expect(getNodeId(deletedNode, doc)).toBe("0-1"); + }); + + it("disambiguates multiple suggested-deletion blocks with the same id", () => { + const schema = getSchema(); + + // Three blocks all sharing id "0": one live block followed by two + // suggested deletions (e.g. the user deleted the same logical block twice + // across forks, all kept in the doc with the y-attributed-delete mark). + const liveBlock = makeBlockContainer(schema, "0", "Live", false); + const deletedBlock1 = makeBlockContainer(schema, "0", "Deleted 1", true); + const deletedBlock2 = makeBlockContainer(schema, "0", "Deleted 2", true); + + const doc = schema.nodes["doc"].createChecked( + {}, + schema.nodes["blockGroup"].createChecked({}, [ + liveBlock, + deletedBlock1, + deletedBlock2, + ]), + ); + + const blockGroup = doc.firstChild!; + + expect(getNodeId(blockGroup.child(0), doc)).toBe("0"); + // Preceded by 1 node with the same id. + expect(getNodeId(blockGroup.child(1), doc)).toBe("0-1"); + // Preceded by 2 nodes with the same id. + expect(getNodeId(blockGroup.child(2), doc)).toBe("0-2"); + }); + + it("counts only preceding same-id nodes, not unrelated blocks", () => { + const schema = getSchema(); + + // A block with a different id sits between the live and deleted blocks. + // It must NOT contribute to the suggested-deletion block's index. + const liveBlock = makeBlockContainer(schema, "0", "Live", false); + const otherBlock = makeBlockContainer(schema, "1", "Other", false); + const deletedBlock = makeBlockContainer(schema, "0", "Deleted", true); + + const doc = schema.nodes["doc"].createChecked( + {}, + schema.nodes["blockGroup"].createChecked({}, [ + liveBlock, + otherBlock, + deletedBlock, + ]), + ); + + const blockGroup = doc.firstChild!; + + expect(getNodeId(blockGroup.child(0), doc)).toBe("0"); + expect(getNodeId(blockGroup.child(1), doc)).toBe("1"); + // Only the single live block with id "0" precedes it -> index 1. + expect(getNodeId(blockGroup.child(2), doc)).toBe("0-1"); + }); + + it("throws when a suggested-deletion node is not found in the provided doc", () => { + const schema = getSchema(); + + // A suggested-deletion block that is NOT part of `doc` -> the walk never + // finds it, so getNodeId throws. + const orphanDeleted = makeBlockContainer(schema, "0", "Orphan", true); + + const doc = schema.nodes["doc"].createChecked( + {}, + schema.nodes["blockGroup"].createChecked( + {}, + makeBlockContainer(schema, "0", "Live", false), + ), + ); + + expect(() => getNodeId(orphanDeleted, doc)).toThrow(/not found in document/); + }); +}); + +describe("docToBlocks round trip with suggested deletions", () => { + let editor: BlockNoteEditor; + + function getSchema() { + if (!editor) { + editor = BlockNoteEditor.create(); + } + return editor.pmSchema; + } + + it("reports distinct block ids even though two ProseMirror nodes share the same id", () => { + const schema = getSchema(); + + // A live block and a suggested-deletion block that, in suggestion mode, + // share the SAME ProseMirror id "0". + const liveBlock = makeBlockContainer(schema, "0", "Live", false); + const deletedBlock = makeBlockContainer(schema, "0", "Deleted", true); + + const doc = schema.nodes["doc"].createChecked( + {}, + schema.nodes["blockGroup"].createChecked({}, [liveBlock, deletedBlock]), + ); + + // At the ProseMirror level, both nodes share id "0". + const blockGroup = doc.firstChild!; + expect(blockGroup.child(0).attrs.id).toBe("0"); + expect(blockGroup.child(1).attrs.id).toBe("0"); + + // docToBlocks disambiguates them via getNodeId: the live block keeps "0", + // the suggested-deletion block becomes "0-1". + const blocks = docToBlocks(doc); + const ids = blocks.map((block) => block.id); + + expect(ids).toEqual(["0", "0-1"]); + // All block ids are distinct. + expect(new Set(ids).size).toBe(ids.length); + }); + + it("disambiguates multiple suggested-deletion blocks sharing an id in docToBlocks", () => { + const schema = getSchema(); + + const liveBlock = makeBlockContainer(schema, "0", "Live", false); + const deletedBlock1 = makeBlockContainer(schema, "0", "Deleted 1", true); + const deletedBlock2 = makeBlockContainer(schema, "0", "Deleted 2", true); + + const doc = schema.nodes["doc"].createChecked( + {}, + schema.nodes["blockGroup"].createChecked({}, [ + liveBlock, + deletedBlock1, + deletedBlock2, + ]), + ); + + const blocks = docToBlocks(doc); + const ids = blocks.map((block) => block.id); + + expect(ids).toEqual(["0", "0-1", "0-2"]); + expect(new Set(ids).size).toBe(ids.length); + }); + + it("only disambiguates the suggested-deletion block, leaving unrelated ids intact", () => { + const schema = getSchema(); + + const liveBlock = makeBlockContainer(schema, "0", "Live", false); + const otherBlock = makeBlockContainer(schema, "1", "Other", false); + const deletedBlock = makeBlockContainer(schema, "0", "Deleted", true); + + const doc = schema.nodes["doc"].createChecked( + {}, + schema.nodes["blockGroup"].createChecked({}, [ + liveBlock, + otherBlock, + deletedBlock, + ]), + ); + + const blocks = docToBlocks(doc); + const ids = blocks.map((block) => block.id); + + expect(ids).toEqual(["0", "1", "0-1"]); + expect(new Set(ids).size).toBe(ids.length); + }); +}); diff --git a/packages/core/src/api/getBlockInfoFromPos.ts b/packages/core/src/api/getBlockInfoFromPos.ts index b0768a2cc8..ee7d500b07 100644 --- a/packages/core/src/api/getBlockInfoFromPos.ts +++ b/packages/core/src/api/getBlockInfoFromPos.ts @@ -44,6 +44,47 @@ export type BlockInfo = { } ); +export function isSuggestedDeletionNode(node: Node): boolean { + return node.marks.some((m) => ["y-attributed-delete"].includes(m.type.name)); +} + +export function getNodeId(node: Node, doc: Node): string { + const id = node.attrs.id; + if (!id) { + throw new Error(`Node ${node} does not have an ID`); + } + /** + * In suggestion mode, yjs will insert nodes which have actually been deleted but are kept in the document with a "y-attributed-delete" mark, + * and nodes which have been inserted but are not yet accepted by the user, with a "y-attributed-insert" mark. + * Both of these nodes will have the same ID as the original node, + * so we need to differentiate them by counting how many nodes with the same ID come before them in the document, and adding that count to the ID. + */ + if (isSuggestedDeletionNode(node)) { + // walk the doc to find the node and count it's index if others have the same ID, to differentiate them + let index = 0; + let found = false; + doc.descendants((descNode: Node) => { + if (found) { + return false; // stop the walk + } + if (descNode.attrs.id === id) { + if (descNode === node) { + found = true; + return false; // stop the walk + } + index++; + } + return true; // continue the walk + }); + if (!found) { + throw new Error(`Node ${node} with ID ${id} not found in document`); + } + return `${id}-${index}`; + } + // TODO handle deleted nodes + return id; +} + /** * Retrieves the position just before the nearest block node in a ProseMirror * doc, relative to a position. If the position is within a block node or its @@ -232,22 +273,12 @@ export function getBlockInfoFromResolvedPos(resolvedPos: ResolvedPos) { * Gets information regarding the ProseMirror nodes that make up a block. The * block chosen is the one currently containing the current ProseMirror * selection. - * @param state The ProseMirror editor state. + * @param source The ProseMirror editor state. */ -export function getBlockInfoFromSelection(state: EditorState) { - const posInfo = getNearestBlockPos(state.doc, state.selection.anchor); - - return getBlockInfo(posInfo); +export function getBlockInfoFromSelection(source: EditorState | Transaction) { + return getBlockInfoAt(source, source.selection.anchor); } -/** - * Gets information regarding the ProseMirror nodes that make up a block. The - * block chosen is the one currently containing the current ProseMirror - * selection. - * @param tr The ProseMirror transaction. - */ -export function getBlockInfoFromTransaction(tr: Transaction) { - const posInfo = getNearestBlockPos(tr.doc, tr.selection.anchor); - - return getBlockInfo(posInfo); +export function getBlockInfoAt(source: EditorState | Transaction, pos: number) { + return getBlockInfo(getNearestBlockPos(source.doc, pos)); } diff --git a/packages/core/src/api/getBlocksChangedByTransaction.ts b/packages/core/src/api/getBlocksChangedByTransaction.ts index c45af4cb71..94b2bc1d3b 100644 --- a/packages/core/src/api/getBlocksChangedByTransaction.ts +++ b/packages/core/src/api/getBlocksChangedByTransaction.ts @@ -11,9 +11,9 @@ import { import type { BlockSchema } from "../schema/index.js"; import type { InlineContentSchema } from "../schema/inlineContent/types.js"; import type { StyleSchema } from "../schema/styles/types.js"; +import { getNodeId } from "./getBlockInfoFromPos.js"; import { nodeToBlock } from "./nodeConversions/nodeToBlock.js"; import { isNodeBlock } from "./nodeUtil.js"; -import { getPmSchema } from "./pmUtil.js"; /** * Change detection utilities for BlockNote. @@ -40,7 +40,7 @@ function getParentBlockId(doc: Node, pos: number): string | undefined { for (let i = resolvedPos.depth; i > 0; i--) { const parent = resolvedPos.node(i); if (isNodeBlock(parent)) { - return parent.attrs.id; + return getNodeId(parent, doc); } } return undefined; @@ -161,7 +161,6 @@ function collectSnapshot< } > = {}; const childrenByParent: Record = {}; - const pmSchema = getPmSchema(doc); doc.descendants((node, pos) => { if (!isNodeBlock(node)) { return true; @@ -171,9 +170,10 @@ function collectSnapshot< if (!childrenByParent[key]) { childrenByParent[key] = []; } - const block = nodeToBlock(node, pmSchema); - byId[node.attrs.id] = { block, parentId }; - childrenByParent[key].push(node.attrs.id); + const block = nodeToBlock(node, doc); + const nodeId = getNodeId(node, doc); + byId[nodeId] = { block, parentId }; + childrenByParent[key].push(nodeId); return true; }); return { byId, childrenByParent }; diff --git a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts index 724b552bda..848fc489d1 100644 --- a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts +++ b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts @@ -5,7 +5,6 @@ import { InlineContentSchema, StyleSchema, } from "../../schema/index.js"; -import { getPmSchema } from "../pmUtil.js"; import { nodeToBlock } from "./nodeToBlock.js"; /** @@ -20,7 +19,6 @@ export function fragmentToBlocks< // pass these to the exporter const blocks: BlockNoDefaults[] = []; fragment.descendants((node) => { - const pmSchema = getPmSchema(node); if (node.type.name === "blockContainer") { if (node.firstChild?.type.name === "blockGroup") { // selection started within a block group @@ -49,13 +47,15 @@ export function fragmentToBlocks< if (node.type.name === "columnList" && node.childCount === 1) { // column lists with a single column should be flattened (not the entire column list has been selected) node.firstChild?.forEach((child) => { - blocks.push(nodeToBlock(child, pmSchema)); + // TODO node is technically not correct here, we just need a doc to pass in + blocks.push(nodeToBlock(child, node)); }); return false; } if (node.type.isInGroup("bnBlock")) { - blocks.push(nodeToBlock(node, pmSchema)); + // TODO node is technically not correct here, we just need a doc to pass in + blocks.push(nodeToBlock(node, node)); // don't descend into children, as they're already included in the block returned by nodeToBlock return false; } diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts index 5048f91a2b..8f50e0a81a 100644 --- a/packages/core/src/api/nodeConversions/nodeToBlock.ts +++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts @@ -1,4 +1,4 @@ -import { Mark, Node, Schema, Slice } from "@tiptap/pm/model"; +import { Mark, Node, Slice } from "@tiptap/pm/model"; import type { Block } from "../../blocks/defaultBlocks.js"; import UniqueID from "../../extensions/tiptap-extensions/UniqueID/UniqueID.js"; import type { @@ -18,12 +18,14 @@ import { isStyledTextInlineContent, } from "../../schema/inlineContent/types.js"; import { UnreachableCaseError } from "../../util/typescript.js"; -import { getBlockInfoWithManualOffset } from "../getBlockInfoFromPos.js"; +import { + getBlockInfoWithManualOffset, + getNodeId, +} from "../getBlockInfoFromPos.js"; import { getBlockCache, getBlockSchema, getInlineContentSchema, - getPmSchema, getStyleSchema, } from "../pmUtil.js"; @@ -385,21 +387,17 @@ export function nodeToCustomInlineContent< /** * Convert a Prosemirror node to a BlockNote block. - * - * TODO: test changes */ export function nodeToBlock< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema, ->( - node: Node, - schema: Schema, - blockSchema: BSchema = getBlockSchema(schema) as BSchema, - inlineContentSchema: I = getInlineContentSchema(schema) as I, - styleSchema: S = getStyleSchema(schema) as S, - blockCache = getBlockCache(schema), -): Block { +>(node: Node, doc: Node): Block { + const schema = node.type.schema; + const blockSchema = getBlockSchema(schema) as BSchema; + const inlineContentSchema = getInlineContentSchema(schema) as I; + const styleSchema = getStyleSchema(schema) as S; + const blockCache = getBlockCache(schema); if (!node.type.isInGroup("bnBlock")) { throw Error("Node should be a bnBlock, but is instead: " + node.type.name); } @@ -412,10 +410,12 @@ export function nodeToBlock< const blockInfo = getBlockInfoWithManualOffset(node, 0); - let id = blockInfo.bnBlock.node.attrs.id; - - // Only used for blocks converted from other formats. - if (id === null) { + // TODO this id needs to lie when it is a deleted block for suggestion mode support + let id: string; + try { + id = getNodeId(blockInfo.bnBlock.node, doc); + } catch { + // Only used for blocks converted from other formats. id = UniqueID.options.generateID(); } @@ -444,16 +444,7 @@ export function nodeToBlock< const children: Block[] = []; blockInfo.childContainer?.node.forEach((child) => { - children.push( - nodeToBlock( - child, - schema, - blockSchema, - inlineContentSchema, - styleSchema, - blockCache, - ), - ); + children.push(nodeToBlock(child, doc)); }); let content: Block["content"]; @@ -502,27 +493,11 @@ export function docToBlocks< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema, ->( - doc: Node, - schema: Schema = getPmSchema(doc), - blockSchema: BSchema = getBlockSchema(schema) as BSchema, - inlineContentSchema: I = getInlineContentSchema(schema) as I, - styleSchema: S = getStyleSchema(schema) as S, - blockCache = getBlockCache(schema), -) { +>(doc: Node) { const blocks: Block[] = []; if (doc.firstChild) { doc.firstChild.descendants((node) => { - blocks.push( - nodeToBlock( - node, - schema, - blockSchema, - inlineContentSchema, - styleSchema, - blockCache, - ), - ); + blocks.push(nodeToBlock(node, doc)); return false; }); } @@ -554,11 +529,8 @@ export function prosemirrorSliceToSlicedBlocks< S extends StyleSchema, >( slice: Slice, - schema: Schema, - blockSchema: BSchema = getBlockSchema(schema) as BSchema, - inlineContentSchema: I = getInlineContentSchema(schema) as I, - styleSchema: S = getStyleSchema(schema) as S, - blockCache: WeakMap> = getBlockCache(schema), + + // TODO doc here? ): { /** * The blocks that are included in the selection. @@ -629,14 +601,8 @@ export function prosemirrorSliceToSlicedBlocks< return; } - const block = nodeToBlock( - blockContainer, - schema, - blockSchema, - inlineContentSchema, - styleSchema, - blockCache, - ); + // TODO this is not technically correct + const block = nodeToBlock(blockContainer, slice.content.firstChild!); const childGroup = blockContainer.childCount > 1 ? blockContainer.child(1) : undefined; diff --git a/packages/core/src/api/nodeUtil.ts b/packages/core/src/api/nodeUtil.ts index 3388c95413..248b7233f6 100644 --- a/packages/core/src/api/nodeUtil.ts +++ b/packages/core/src/api/nodeUtil.ts @@ -1,4 +1,5 @@ import type { Node } from "prosemirror-model"; +import { getNodeId } from "./getBlockInfoFromPos.js"; /** * Get a TipTap node by id @@ -17,7 +18,7 @@ export function getNodeById( } // Keeps traversing nodes if block with target ID has not been found. - if (!isNodeBlock(node) || node.attrs.id !== id) { + if (!isNodeBlock(node) || getNodeId(node, doc) !== id) { return true; } diff --git a/packages/core/src/api/parsers/html/parseHTML.ts b/packages/core/src/api/parsers/html/parseHTML.ts index 16e03f883a..a2999b2df9 100644 --- a/packages/core/src/api/parsers/html/parseHTML.ts +++ b/packages/core/src/api/parsers/html/parseHTML.ts @@ -30,7 +30,8 @@ export function HTMLToBlocks< const blocks: Block[] = []; for (let i = 0; i < parentNode.childCount; i++) { - blocks.push(nodeToBlock(parentNode.child(i), pmSchema)); + // TODO technically not correct here, but deleted ids will be internally consistent at least + blocks.push(nodeToBlock(parentNode.child(i), parentNode)); } return blocks; diff --git a/packages/core/src/blocks/ListItem/ListItemKeyboardShortcuts.ts b/packages/core/src/blocks/ListItem/ListItemKeyboardShortcuts.ts index eb71c2f7ab..0b33335788 100644 --- a/packages/core/src/blocks/ListItem/ListItemKeyboardShortcuts.ts +++ b/packages/core/src/blocks/ListItem/ListItemKeyboardShortcuts.ts @@ -1,12 +1,12 @@ import { splitBlockCommand } from "../../api/blockManipulation/commands/splitBlock/splitBlock.js"; import { updateBlockCommand } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js"; -import { getBlockInfoFromTransaction } from "../../api/getBlockInfoFromPos.js"; +import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; export const handleEnter = (editor: BlockNoteEditor) => { const { blockInfo, selectionEmpty } = editor.transact((tr) => { return { - blockInfo: getBlockInfoFromTransaction(tr), + blockInfo: getBlockInfoFromSelection(tr), selectionEmpty: tr.selection.anchor === tr.selection.head, }; }); diff --git a/packages/core/src/blocks/utils/listItemEnterHandler.ts b/packages/core/src/blocks/utils/listItemEnterHandler.ts index ceb383a611..755987449a 100644 --- a/packages/core/src/blocks/utils/listItemEnterHandler.ts +++ b/packages/core/src/blocks/utils/listItemEnterHandler.ts @@ -1,6 +1,6 @@ import { splitBlockTr } from "../../api/blockManipulation/commands/splitBlock/splitBlock.js"; import { updateBlockTr } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js"; -import { getBlockInfoFromTransaction } from "../../api/getBlockInfoFromPos.js"; +import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; export const handleEnter = ( @@ -9,7 +9,7 @@ export const handleEnter = ( ) => { const { blockInfo, selectionEmpty } = editor.transact((tr) => { return { - blockInfo: getBlockInfoFromTransaction(tr), + blockInfo: getBlockInfoFromSelection(tr), selectionEmpty: tr.selection.anchor === tr.selection.head, }; }); diff --git a/packages/core/src/editor/managers/BlockManager.ts b/packages/core/src/editor/managers/BlockManager.ts index ea9d9a5680..f086444ecc 100644 --- a/packages/core/src/editor/managers/BlockManager.ts +++ b/packages/core/src/editor/managers/BlockManager.ts @@ -46,7 +46,7 @@ export class BlockManager< */ public get document(): Block[] { return this.editor.transact((tr) => { - return docToBlocks(tr.doc, this.editor.pmSchema); + return docToBlocks(tr.doc); }); } diff --git a/packages/core/src/editor/managers/ExtensionManager/index.ts b/packages/core/src/editor/managers/ExtensionManager/index.ts index 0af9d37a4d..ba176d66b3 100644 --- a/packages/core/src/editor/managers/ExtensionManager/index.ts +++ b/packages/core/src/editor/managers/ExtensionManager/index.ts @@ -10,7 +10,7 @@ import { keymap } from "@tiptap/pm/keymap"; import { Plugin, TextSelection } from "prosemirror-state"; import { updateBlockTr } from "../../../api/blockManipulation/commands/updateBlock/updateBlock.js"; import { setTextCursorPosition } from "../../../api/blockManipulation/selections/textCursorPosition.js"; -import { getBlockInfoFromTransaction } from "../../../api/getBlockInfoFromPos.js"; +import { getBlockInfoFromSelection, getNodeId } from "../../../api/getBlockInfoFromPos.js"; import { sortByDependencies } from "../../../util/topo-sort.js"; import type { BlockNoteEditor, @@ -266,8 +266,7 @@ export class ExtensionManager { plugins?.forEach((plugin) => { pluginRefsToRemove.add(plugin); const key = (plugin as any).spec?.key; - const keyStr = - typeof key === "object" && key ? key.key : key; + const keyStr = typeof key === "object" && key ? key.key : key; if (typeof keyStr === "string") { pluginKeysToRemove.add(keyStr); } @@ -338,8 +337,7 @@ export class ExtensionManager { // in the state differ from the ones we tracked) if (pluginKeysToRemove.size) { const key = (plugin as any).spec?.key; - const keyStr = - typeof key === "object" && key ? key.key : key; + const keyStr = typeof key === "object" && key ? key.key : key; if (typeof keyStr === "string" && pluginKeysToRemove.has(keyStr)) { return false; } @@ -510,7 +508,7 @@ export class ExtensionManager { }); if (replaceWith) { const tr = state.tr; - const blockInfo = getBlockInfoFromTransaction(tr); + const blockInfo = getBlockInfoFromSelection(tr); if ( !blockInfo.isBlockContainer || @@ -526,10 +524,7 @@ export class ExtensionManager { // the new block when the content is replaced wholesale (e.g. // when the rule returns content: []). Move the cursor back // inside the new block so the user can keep typing. - const blockId = blockInfo.bnBlock.node.attrs.id; - if (blockId) { - setTextCursorPosition(tr, blockId, "start"); - } + setTextCursorPosition(tr, getNodeId(blockInfo.bnBlock.node, tr.doc), "start"); return tr; } return null; diff --git a/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts b/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts index 714783ad28..79539dd0ce 100644 --- a/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts +++ b/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts @@ -1,6 +1,7 @@ import { findChildrenInRange } from "@tiptap/core"; import { Plugin, PluginKey } from "prosemirror-state"; import { Decoration, DecorationSet } from "prosemirror-view"; +import { getNodeId } from "../../api/getBlockInfoFromPos.js"; import { createExtension } from "../../editor/BlockNoteExtension.js"; const PLUGIN_KEY = new PluginKey(`previous-blocks`); @@ -93,7 +94,7 @@ export const PreviousBlockTypeExtension = createExtension(() => { (node) => node.attrs.id, ); const oldNodesById = new Map( - oldNodes.map((node) => [node.node.attrs.id, node]), + oldNodes.map((node) => [getNodeId(node.node, oldState.doc), node]), ); const newNodes = findChildrenInRange( newState.doc, @@ -102,7 +103,8 @@ export const PreviousBlockTypeExtension = createExtension(() => { ); for (const node of newNodes) { - const oldNode = oldNodesById.get(node.node.attrs.id); + const nodeId = getNodeId(node.node, newState.doc); + const oldNode = oldNodesById.get(nodeId); const oldContentNode = oldNode?.node.firstChild; const newContentNode = node.node.firstChild; @@ -122,12 +124,9 @@ export const PreviousBlockTypeExtension = createExtension(() => { depth: oldState.doc.resolve(oldNode.pos).depth, }; - currentTransactionOriginalOldBlockAttrs[ - node.node.attrs.id - ] = oldAttrs; + currentTransactionOriginalOldBlockAttrs[nodeId] = oldAttrs; - prev.currentTransactionOldBlockAttrs[node.node.attrs.id] = - oldAttrs; + prev.currentTransactionOldBlockAttrs[nodeId] = oldAttrs; if ( oldAttrs.index !== newAttrs.index || @@ -138,7 +137,7 @@ export const PreviousBlockTypeExtension = createExtension(() => { (oldAttrs as any)["depth-change"] = oldAttrs.depth - newAttrs.depth; - prev.updatedBlocks.add(node.node.attrs.id); + prev.updatedBlocks.add(nodeId); } } } @@ -163,12 +162,14 @@ export const PreviousBlockTypeExtension = createExtension(() => { return; } - if (!pluginState.updatedBlocks.has(node.attrs.id)) { + const id = getNodeId(node, state.doc); + + if (!pluginState.updatedBlocks.has(id)) { return; } const prevAttrs = - pluginState.currentTransactionOldBlockAttrs[node.attrs.id]; + pluginState.currentTransactionOldBlockAttrs[id]; const decorationAttrs: any = {}; for (const [nodeAttr, val] of Object.entries(prevAttrs)) { diff --git a/packages/core/src/extensions/TableHandles/TableHandles.ts b/packages/core/src/extensions/TableHandles/TableHandles.ts index d957056f4e..de4d3221c5 100644 --- a/packages/core/src/extensions/TableHandles/TableHandles.ts +++ b/packages/core/src/extensions/TableHandles/TableHandles.ts @@ -257,20 +257,16 @@ export class TableHandlesView implements PluginView { | BlockFromConfigNoChildren | undefined; - const pmNodeInfo = this.editor.transact((tr) => - getNodeById(blockEl.id, tr.doc), - ); + const { pmNodeInfo, doc } = this.editor.transact((tr) => ({ + pmNodeInfo: getNodeById(blockEl.id, tr.doc), + doc: tr.doc, + })); if (!pmNodeInfo) { throw new Error(`Block with ID ${blockEl.id} not found`); } - const block = nodeToBlock( - pmNodeInfo.node, - this.editor.pmSchema, - this.editor.schema.blockSchema, - this.editor.schema.inlineContentSchema, - this.editor.schema.styleSchema, - ); + // rm as any + const block = nodeToBlock(pmNodeInfo.node, doc) as any; if (editorHasBlockWithType(this.editor, "table")) { this.tablePos = pmNodeInfo.posBeforeNode + 1; diff --git a/packages/core/src/extensions/tiptap-extensions/Suggestions/SuggestionMarks.ts b/packages/core/src/extensions/tiptap-extensions/Suggestions/SuggestionMarks.ts index 38a62baf07..7cc3426f1d 100644 --- a/packages/core/src/extensions/tiptap-extensions/Suggestions/SuggestionMarks.ts +++ b/packages/core/src/extensions/tiptap-extensions/Suggestions/SuggestionMarks.ts @@ -9,7 +9,7 @@ import { MarkSpec } from "prosemirror-model"; export const SuggestionAddMark = Mark.create({ name: "y-attributed-insert", inclusive: false, - excludes: "", + // excludes: "", TODO: what's desired? addAttributes() { return { id: { default: null, validate: "number" }, // note: validate is supported in prosemirror but not in tiptap, so this doesn't actually work (considered not critical) @@ -61,7 +61,7 @@ export const SuggestionAddMark = Mark.create({ export const SuggestionDeleteMark = Mark.create({ name: "y-attributed-delete", inclusive: false, - excludes: "", + // excludes: "", TODO: what's desired? addAttributes() { return { id: { default: null, validate: "number" }, // note: validate is supported in prosemirror but not in tiptap @@ -116,7 +116,7 @@ export const SuggestionDeleteMark = Mark.create({ export const SuggestionModificationMark = Mark.create({ name: "y-attributed-format", inclusive: false, - excludes: "", + // excludes: "", TODO: what's desired? addAttributes() { return { id: { default: null, validate: "number" }, // note: validate is supported in prosemirror but not in tiptap diff --git a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.test.ts b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.test.ts new file mode 100644 index 0000000000..7395c046af --- /dev/null +++ b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.test.ts @@ -0,0 +1,169 @@ +import { Node } from "prosemirror-model"; +import { beforeAll, describe, expect, it } from "vitest"; + +import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js"; + +/** + * @vitest-environment jsdom + */ + +/** + * The UniqueID extension's `appendTransaction` hook assigns a fresh id to any + * newly-inserted node whose id duplicates an existing one. The one exception is + * suggested-deletion nodes (carrying a `y-attributed-delete` mark): in + * suggestion mode, Yjs keeps the deleted node in the document with the SAME id + * as the surviving node, and rewriting that id would corrupt the suggestion. + * These tests exercise both branches. + */ + +function createEditor() { + const editor = BlockNoteEditor.create(); + editor.mount(document.createElement("div")); + editor.replaceBlocks(editor.document, [ + { id: "block-a", type: "paragraph", content: "A" }, + { id: "block-b", type: "paragraph", content: "B" }, + ]); + return editor; +} + +/** + * Builds a `blockContainer` node holding a single paragraph with the given + * block `id`, optionally carrying a `y-attributed-delete` mark to simulate a + * suggested deletion. + */ +function makeBlockContainer( + editor: BlockNoteEditor, + id: string, + text: string, + suggestedDelete: boolean, +) { + const schema = editor.pmSchema; + const paragraph = schema.nodes["paragraph"].createChecked( + {}, + schema.text(text), + ); + const marks = suggestedDelete + ? [schema.marks["y-attributed-delete"].create({ id: 1 })] + : undefined; + + return schema.nodes["blockContainer"].createChecked({ id }, paragraph, marks); +} + +/** Returns the ids of all blockContainer nodes in document order. */ +function getBlockIds(doc: Node) { + const ids: (string | null)[] = []; + doc.descendants((node) => { + if (node.type.name === "blockContainer") { + ids.push(node.attrs.id); + } + return true; + }); + return ids; +} + +describe("UniqueID: duplicate id handling", () => { + let editor: BlockNoteEditor; + + beforeAll(() => { + // Reset the mock id counter so generated ids are deterministic. + (window as any).__TEST_OPTIONS = {}; + }); + + it("assigns a fresh id to a newly-inserted plain block that duplicates another new block", () => { + editor = createEditor(); + const view = editor._tiptapEditor.view; + + // Insert TWO new blocks sharing the same id "dup" in a single transaction. + // Both land in the same changed range, so UniqueID detects the duplicate + // and rewrites one of them with a fresh generated id. + const dup1 = makeBlockContainer(editor, "dup", "Dup 1", false); + const dup2 = makeBlockContainer(editor, "dup", "Dup 2", false); + + // Position at the boundary between the first block and the second block + // inside the blockGroup. + const firstBlock = view.state.doc.firstChild!.firstChild!; + const insertPos = firstBlock.nodeSize + 1; + + view.dispatch(view.state.tr.insert(insertPos, [dup1, dup2])); + + const ids = getBlockIds(view.state.doc); + + // Four blocks now exist, and UniqueID has resolved the duplicate so that + // all ids are distinct and non-null. + expect(ids).toHaveLength(4); + expect(ids.every((id) => id !== null)).toBe(true); + expect(new Set(ids).size).toBe(4); + }); + + it("preserves the duplicate id of a suggested-deletion block while still rewriting the plain duplicate", () => { + editor = createEditor(); + const view = editor._tiptapEditor.view; + + // Insert two new blocks sharing the id "dup" in a single transaction: a + // plain (live) one and a suggested-deletion one (y-attributed-delete mark). + // The plain block's id is rewritten, but the suggested-deletion block MUST + // keep its "dup" id, because in suggestion mode it intentionally shares the + // id with the surviving node. + const liveDup = makeBlockContainer(editor, "dup", "Live dup", false); + const deletedDup = makeBlockContainer(editor, "dup", "Deleted dup", true); + + const firstBlock = view.state.doc.firstChild!.firstChild!; + const insertPos = firstBlock.nodeSize + 1; + + // Insert the live block first, then the suggested-deletion block after it. + view.dispatch(view.state.tr.insert(insertPos, [liveDup, deletedDup])); + + const ids = getBlockIds(view.state.doc); + + expect(ids).toHaveLength(4); + // The suggested-deletion block keeps "dup". + const dupCount = ids.filter((id) => id === "dup").length; + expect(dupCount).toBe(1); + + // Confirm it is specifically the suggested-deletion node that kept "dup". + let suggestedDeletionId: string | null = null; + view.state.doc.descendants((node) => { + if ( + node.type.name === "blockContainer" && + node.marks.some((m) => m.type.name === "y-attributed-delete") + ) { + suggestedDeletionId = node.attrs.id; + } + return true; + }); + expect(suggestedDeletionId).toBe("dup"); + }); + + it("exposes distinct ids in editor.document even though two ProseMirror nodes share the same id", () => { + editor = createEditor(); + const view = editor._tiptapEditor.view; + + // Insert a suggested-deletion copy of the FIRST block, sharing its id + // "block-a". This mirrors suggestion mode: Yjs keeps the deleted node in + // the document with the same id as the surviving node, and UniqueID leaves + // that duplicate id untouched. + const deletedCopy = makeBlockContainer( + editor, + "block-a", + "A deleted copy", + true, + ); + + const firstBlock = view.state.doc.firstChild!.firstChild!; + const insertPos = firstBlock.nodeSize + 1; + + view.dispatch(view.state.tr.insert(insertPos, deletedCopy)); + + // At the ProseMirror level, two nodes now share the id "block-a": the live + // one and the suggested-deletion one. + const pmIds = getBlockIds(view.state.doc); + expect(pmIds.filter((id) => id === "block-a")).toHaveLength(2); + + // But editor.document disambiguates them via getNodeId: the suggested + // deletion node is reported as "block-a-1", so all block ids are distinct. + const docIds = editor.document.map((block) => block.id); + expect(docIds).toContain("block-a"); + expect(docIds).toContain("block-a-1"); + expect(new Set(docIds).size).toBe(docIds.length); + }); +}); diff --git a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts index 54cb8b7340..7ab30b78aa 100644 --- a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts +++ b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts @@ -4,9 +4,10 @@ import { findChildrenInRange, getChangedRanges, } from "@tiptap/core"; -import { Fragment, Slice } from "prosemirror-model"; -import { Plugin, PluginKey } from "prosemirror-state"; import { uuidv4 } from "lib0/random"; +import { Fragment, Node, Slice } from "prosemirror-model"; +import { Plugin, PluginKey } from "prosemirror-state"; +import { isSuggestedDeletionNode } from "../../../api/getBlockInfoFromPos.js"; /** * Code from Tiptap UniqueID extension (https://tiptap.dev/api/extensions/unique-id) @@ -41,6 +42,20 @@ function findDuplicates(items: any) { return duplicates; } +/** + * Whether a node is marked as deleted by a suggestion (carries the + * `y-attributed-delete` node mark). + * + * Under the suggestion/matchNodes binding, changing a block's content type + * renders the block as a deleted copy (this mark) next to its inserted + * replacement - and both copies share the same `id`. The deleted copy must be + * ignored by the uniqueness logic, otherwise its `id` looks like a duplicate + * and we'd regenerate the `id` on the surviving block. + */ +function isMarkedDeleted(node: Node) { + return node.marks.some((mark) => mark.type.name === "y-attributed-delete"); +} + const UniqueID = Extension.create({ name: "uniqueID", // we’ll set a very high priority to make sure this runs first @@ -48,7 +63,6 @@ const UniqueID = Extension.create({ priority: 10000, addOptions() { return { - attributeName: "id", types: [] as string[], setIdAttribute: false, isWithinEditor: undefined as ((element: Element) => boolean) | undefined, @@ -74,19 +88,17 @@ const UniqueID = Extension.create({ { types: this.options.types, attributes: { - [this.options.attributeName]: { + id: { default: null, - parseHTML: (element) => - element.getAttribute(`data-${this.options.attributeName}`), + parseHTML: (element) => element.getAttribute(`data-id`), renderHTML: (attributes) => { const defaultIdAttributes = { - [`data-${this.options.attributeName}`]: - attributes[this.options.attributeName], + [`data-id`]: attributes.id, }; if (this.options.setIdAttribute) { return { ...defaultIdAttributes, - id: attributes[this.options.attributeName], + id: attributes.id, }; } else { return defaultIdAttributes; @@ -142,7 +154,7 @@ const UniqueID = Extension.create({ return; } const { tr } = newState; - const { types, attributeName, generateID } = this.options; + const { types, generateID } = this.options; const transform = combineTransactionSteps( oldState.doc, transactions as any, @@ -160,16 +172,20 @@ const UniqueID = Extension.create({ }, ); const newIds = newNodes - .map(({ node }) => node.attrs[attributeName]) + .map(({ node }) => node.attrs.id) .filter((id) => id !== null); const duplicatedNewIds = findDuplicates(newIds); newNodes.forEach(({ node, pos }) => { + // ignore ids on blocks marked as deleted (see above). + if (isMarkedDeleted(node)) { + return; + } // instead of checking `node.attrs[attributeName]` directly // we look at the current state of the node within `tr.doc`. // this helps to prevent adding new ids to the same node // if the node changed multiple times within one transaction - const id = tr.doc.nodeAt(pos)?.attrs[attributeName]; + const id = tr.doc.nodeAt(pos)?.attrs.id; if (id === null) { // edge case, when using collaboration, yjs will set the id to null in `_forceRerender` @@ -193,7 +209,7 @@ const UniqueID = Extension.create({ // yes, apply the fix tr.setNodeMarkup(pos, undefined, { ...node.attrs, - [attributeName]: "initialBlockId", + id: "initialBlockId", }); return; } @@ -201,17 +217,18 @@ const UniqueID = Extension.create({ tr.setNodeMarkup(pos, undefined, { ...node.attrs, - [attributeName]: generateID(), + id: generateID(), }); return; } // check if the node doesn’t exist in the old state const { deleted } = mapping.invert().mapResult(pos); const newNode = deleted && duplicatedNewIds.includes(id); - if (newNode) { + // purposefully skip rewriting ids for suggested deletion nodes, to avoid modifying them + if (newNode && !isSuggestedDeletionNode(node)) { tr.setNodeMarkup(pos, undefined, { ...node.attrs, - [attributeName]: generateID(), + id: generateID(), }); } }); @@ -275,7 +292,7 @@ const UniqueID = Extension.create({ if (!transformPasted) { return slice; } - const { types, attributeName } = this.options; + const { types } = this.options; const removeId = (fragment: any) => { const list: any[] = []; fragment.forEach((node: any) => { @@ -293,7 +310,7 @@ const UniqueID = Extension.create({ const nodeWithoutId = node.type.create( { ...node.attrs, - [attributeName]: null, + id: null, }, removeId(node.content), node.marks, diff --git a/packages/core/src/schema/blocks/createSpec.ts b/packages/core/src/schema/blocks/createSpec.ts index 6df3e68aa4..958661d734 100644 --- a/packages/core/src/schema/blocks/createSpec.ts +++ b/packages/core/src/schema/blocks/createSpec.ts @@ -195,12 +195,7 @@ export function addNodeAndExtensionsToSpec< // Gets the BlockNote editor instance const editor = this.options.editor; // Gets the block - const block = getBlockFromPos( - props.getPos, - editor, - this.editor, - blockConfig.type, - ); + const block = getBlockFromPos(props.getPos, props.view.state.doc); // Gets the custom HTML attributes for `blockContent` nodes const blockContentDOMAttributes = this.options.domAttributes?.blockContent || {}; diff --git a/packages/core/src/schema/blocks/internal.ts b/packages/core/src/schema/blocks/internal.ts index eed8cf9fa3..210910eb99 100644 --- a/packages/core/src/schema/blocks/internal.ts +++ b/packages/core/src/schema/blocks/internal.ts @@ -1,18 +1,12 @@ -import { Attribute, Attributes, Editor, Node } from "@tiptap/core"; +import { Attribute, Attributes, Node } from "@tiptap/core"; +import type { Node as PMNode } from "prosemirror-model"; +import { nodeToBlock } from "../../api/nodeConversions/nodeToBlock.js"; import { defaultBlockToHTML } from "../../blocks/defaultBlockHelpers.js"; -import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import type { ExtensionFactoryInstance } from "../../editor/BlockNoteExtension.js"; import { mergeCSSClasses } from "../../util/browser.js"; import { camelToDataKebab } from "../../util/string.js"; -import { InlineContentSchema } from "../inlineContent/types.js"; import { PropSchema, Props } from "../propTypes.js"; -import { StyleSchema } from "../styles/types.js"; -import { - BlockConfig, - BlockSchemaWithBlock, - LooseBlockSpec, - SpecificBlock, -} from "./types.js"; +import { LooseBlockSpec } from "./types.js"; // Function that uses the 'propSchema' of a blockConfig to create a TipTap // node's `addAttributes` property. @@ -82,43 +76,20 @@ export function propsToAttributes(propSchema: PropSchema): Attributes { // Used to figure out which block should be rendered. This block is then used to // create the node view. -export function getBlockFromPos< - BType extends string, - Config extends BlockConfig, - BSchema extends BlockSchemaWithBlock, - I extends InlineContentSchema, - S extends StyleSchema, ->( - getPos: () => number | undefined, - editor: BlockNoteEditor, - tipTapEditor: Editor, - type: BType, -) { +export function getBlockFromPos(getPos: () => number | undefined, doc: PMNode) { + // TODO is there a cleaner implementation of this? Probably... const pos = getPos(); // Gets position of the node if (pos === undefined) { throw new Error("Cannot find node position"); } - // Gets parent blockContainer node - const blockContainer = tipTapEditor.state.doc.resolve(pos!).node(); - // Gets block identifier - const blockIdentifier = blockContainer.attrs.id; - if (!blockIdentifier) { - throw new Error("Block doesn't have id"); - } - - // Gets the block - const block = editor.getBlock(blockIdentifier)! as SpecificBlock< - BSchema, - BType, - I, - S - >; - if (block.type !== type) { - throw new Error("Block type does not match"); + // Gets parent blockContainer node + const blockContainer = doc.resolve(pos).node(); + if (!blockContainer) { + throw new Error("Cannot find block container"); } - + const block = nodeToBlock(blockContainer, doc); return block; } diff --git a/packages/core/src/y/extensions/RelativePositionMapping.test.ts b/packages/core/src/y/extensions/RelativePositionMapping.test.ts index 4594fa7448..ff20bcc21f 100644 --- a/packages/core/src/y/extensions/RelativePositionMapping.test.ts +++ b/packages/core/src/y/extensions/RelativePositionMapping.test.ts @@ -27,7 +27,7 @@ function setupTwoWaySync(doc1: Y.Doc, doc2: Y.Doc) { }); } -describe("RelativePositionMapping (@y/y)", () => { +describe.skip("RelativePositionMapping (@y/y)", () => { it("should return the same position when no changes are made", () => { const ydoc = new Y.Doc(); const remoteYdoc = new Y.Doc(); diff --git a/packages/core/src/y/extensions/YSync.ts b/packages/core/src/y/extensions/YSync.ts index f4c9f73574..7ae61f78c4 100644 --- a/packages/core/src/y/extensions/YSync.ts +++ b/packages/core/src/y/extensions/YSync.ts @@ -3,6 +3,7 @@ import { type ExtensionOptions, createExtension, } from "../../editor/BlockNoteExtension.js"; +import { blockMatchNodes } from "./blockMatchNodes.js"; import { CollaborationOptions } from "./index.js"; /** @@ -118,6 +119,14 @@ export const YSyncExtension = createExtension( syncPlugin({ suggestionDoc: options.suggestionDoc, mapAttributionToMark, + // Node-pairing policy for the PM->Y diff: a `blockContainer` whose + // block-content type changes is treated as a *different* node, so the + // diff replaces the whole container (deleted + inserted siblings in + // the blockGroup) instead of producing two block-contents in one + // container => schema-invalid. No schema change / storage transform + // needed; `blockContainer` already whitelists the `y-attributed-*` + // marks. See blockMatchNodes.ts. + matchNodes: blockMatchNodes, }), ], runsBefore: ["default"], diff --git a/packages/core/src/y/extensions/blockMatchNodes.ts b/packages/core/src/y/extensions/blockMatchNodes.ts new file mode 100644 index 0000000000..33e72ad97c --- /dev/null +++ b/packages/core/src/y/extensions/blockMatchNodes.ts @@ -0,0 +1,47 @@ +import * as delta from "lib0/delta"; + +/** + * Canonical name of a content delta's first block child (the child carried by an + * insert op), or `null`. For a BlockNote `blockContainer` (content + * `blockContent blockGroup?`) this is its block-content type (paragraph, + * heading, image, ...). + */ +const firstChildName = (d: delta.DeltaAny): string | null => { + for (const op of (d as any).children) { + if (delta.$insertOp.check(op)) { + for (const it of op.insert) { + if (delta.$deltaAny.check(it)) { + return it.name; + } + } + } + } + return null; +}; + +/** + * BlockNote's node-pairing policy for y-prosemirror's `matchNodes` option + * (forwarded to `lib0/delta.diff`). This is the schema-specific bit that lives + * in userland - the binding itself stays schema-agnostic. + * + * A `blockContainer` holds exactly one block content (`blockContent + * blockGroup?`). Diffing a *type change* of that content as an in-place child + * delete+insert would, under a suggestion, tombstone the old content next to the + * new one => two block-contents in one container => schema-invalid. So we + * declare a container's identity to be its first block-content child's type: + * when that changes, the two containers are reported as *different*, the PM->Y + * diff replaces the whole container, and the deleted + inserted containers sit + * as siblings in the blockGroup (`blockGroupChild+` allows that). Each carries + * the `y-attributed-*` node mark - which `blockContainer` already whitelists - + * so no schema change and no storage transform are needed. A plain text edit + * keeps the same first-child type => same identity => the diff descends and + * merges as usual. + * + * @param a removed (old) node + * @param b inserted (new) node + * @returns whether `a` and `b` are the same node (diff in place) vs different (replace) + */ +export const blockMatchNodes = (a: delta.DeltaAny, b: delta.DeltaAny): boolean => + (a as any).name === (b as any).name && + ((a as any).name !== "blockContainer" || + firstChildName(a) === firstChildName(b)); diff --git a/packages/react/src/schema/ReactBlockSpec.tsx b/packages/react/src/schema/ReactBlockSpec.tsx index 1ab1b43da8..7e97b5ec74 100644 --- a/packages/react/src/schema/ReactBlockSpec.tsx +++ b/packages/react/src/schema/ReactBlockSpec.tsx @@ -276,9 +276,7 @@ export function createReactBlockSpec< // `ReactNodeViewRenderer` instead. const block = getBlockFromPos( props.getPos, - editor, - props.editor, - blockConfig.type, + props.view.state.doc, ); const ref = useReactNodeView().nodeViewContentRef; diff --git a/packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts b/packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts index e93b266634..7c8e0b312e 100644 --- a/packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts +++ b/packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts @@ -38,7 +38,7 @@ export function createMultiColumnHandleDropPlugin( const draggedBlock = nodeToBlock( slice.content.child(0), - editor.pmSchema, + view.state.doc, ); if (blockInfo.blockNoteType === "column") { @@ -49,7 +49,7 @@ export function createMultiColumnHandleDropPlugin( const columnList = nodeToBlock( parentBlock, - editor.pmSchema, + view.state.doc, ); // Normalize column widths to average of 1 @@ -111,7 +111,7 @@ export function createMultiColumnHandleDropPlugin( }); } else { // Create new columnList with blocks as columns - const block = nodeToBlock(blockInfo.bnBlock.node, editor.pmSchema); + const block = nodeToBlock(blockInfo.bnBlock.node, view.state.doc); // The user is dropping next to the original block being dragged - do // nothing. diff --git a/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts b/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts index 5cad44fe9f..a64ce0b455 100644 --- a/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts +++ b/packages/xl-multi-column/src/test/conversions/nodeConversion.test.ts @@ -29,7 +29,7 @@ function validateConversion( expect(node).toMatchSnapshot(); - const outputBlock = nodeToBlock(node, editor.pmSchema); + const outputBlock = nodeToBlock(node, editor.prosemirrorState.doc); const fullOriginalBlock = partialBlockToBlockForTesting( editor.schema.blockSchema, diff --git a/patches/@y__prosemirror@2.0.0-2.patch b/patches/@y__prosemirror@2.0.0-2.patch index dab913697b..7d7c8f1216 100644 --- a/patches/@y__prosemirror@2.0.0-2.patch +++ b/patches/@y__prosemirror@2.0.0-2.patch @@ -100,7 +100,7 @@ index 0000000000000000000000000000000000000000..f09b4e94cfb42585d13b700cef3f4fb0 +{"version":3,"file":"cursor-plugin.d.ts","sourceRoot":"","sources":["../../src/cursor-plugin.js"],"names":[],"mappings":"AAgCO,2CAHI,IAAI,GACH,WAAW,CAmBtB;AAQM,8CAHI,IAAI,GACH,OAAO,kBAAkB,EAAE,eAAe,CAOrD;AAYM,yCATI,OAAO,mBAAmB,EAAE,WAAW,aACvC,OAAO,wBAAwB,EAAE,SAAS,mBAC1C,eAAe,gBACf,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,mBACzC,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,kBAAkB,EAAE,eAAe,oBAC5E,MAAM,UACN;IAAC,KAAK,EAAE,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC;IAAC,kBAAkB,EAAE,CAAC,CAAC,0BAA0B,GAAG,IAAI,CAAA;CAAC,GAAG,SAAS,GAC1F,aAAa,CAkExB;AA2BM,yCATI,OAAO,wBAAwB,EAAE,SAAS,yGAElD;IAA+B,oBAAoB;IACU,aAAa,WAA3D,IAAI,YAAY,MAAM,KAAK,WAAW;IACuC,gBAAgB,WAA7F,IAAI,YAAY,MAAM,KAAK,OAAO,kBAAkB,EAAE,eAAe;IACrC,uBAAuB;IAChD,gBAAgB;CACtC,GAAS,MAAM,CAAC,aAAa,CAAC,CAmL7B;;;;;;;;;;;gDAlUO,MAAM,gBACN,MAAM,kBACN,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KACjB,OAAO;oDAwHjB;IAAmD,IAAI,EAA/C,OAAO,kBAAkB,EAAE,UAAU;IAC8B,SAAS,EAA5E;QAAC,MAAM,EAAE,CAAC,CAAC,gBAAgB,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,gBAAgB,CAAA;KAAC,GAAG,IAAI;IACM,SAAS,EAA5E;QAAC,MAAM,EAAE,CAAC,CAAC,gBAAgB,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,gBAAgB,CAAA;KAAC,GAAG,IAAI;IAChD,UAAU,EAAvB,OAAO;IAC0B,MAAM,EAAvC,QAAQ,GAAG,OAAO,GAAG,MAAM;CACnC,KAAU;IAAC,MAAM,EAAE,CAAC,CAAC,gBAAgB,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC,gBAAgB,CAAA;CAAC,GAAG,IAAI;mBApJvD,MAAM;8BACiB,kBAAkB;uBACrC,mBAAmB"} \ No newline at end of file diff --git a/dist/src/index.d.ts b/dist/src/index.d.ts -index fec5f1c23d3f28e250fecd7045fcebe7fc60993f..ebf62e224dcb8a4becb6dcc0e59799e732a4ce1c 100644 +index fec5f1c23d3f28e250fecd7045fcebe7fc60993f..c870a6d1eaa70daf2a6c718b179cb7873ae19e94 100644 --- a/dist/src/index.d.ts +++ b/dist/src/index.d.ts @@ -1,84 +1,8 @@ @@ -194,7 +194,7 @@ index fec5f1c23d3f28e250fecd7045fcebe7fc60993f..ebf62e224dcb8a4becb6dcc0e59799e7 +export * from "./commands.js"; +export * from "./undo-plugin.js"; +export * from "./cursor-plugin.js"; -+export { docToDelta, $prosemirrorDelta, defaultMapAttributionToMark } from "./sync-utils.js"; ++export { docToDelta, $prosemirrorDelta, defaultMapAttributionToMark, yattr2markname, pmToFragment, fragmentToPm } from "./sync-utils.js"; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/src/index.d.ts.map b/dist/src/index.d.ts.map @@ -286,10 +286,10 @@ index 0000000000000000000000000000000000000000..e4f768c579f11b08055a31cc166e8c34 \ No newline at end of file diff --git a/dist/src/sync-plugin.d.ts b/dist/src/sync-plugin.d.ts new file mode 100644 -index 0000000000000000000000000000000000000000..c1da2aa33b86511936e9b1ba4d2d3c848e0c70da +index 0000000000000000000000000000000000000000..56fcf104d0d08a0a21e3bf8941063abb0ec7e783 --- /dev/null +++ b/dist/src/sync-plugin.d.ts -@@ -0,0 +1,41 @@ +@@ -0,0 +1,44 @@ +/** + * This Prosemirror {@link Plugin} is responsible for synchronizing the prosemirror {@link EditorState} with a {@link Y.XmlFragment} + * @@ -303,12 +303,14 @@ index 0000000000000000000000000000000000000000..c1da2aa33b86511936e9b1ba4d2d3c84 + * @param {Y.Doc} [opts.suggestionDoc] A {@link Y.Doc} to use for suggestion tracking + * @param {AttributionMapper} [opts.mapAttributionToMark] A function to map the {@link Y.Attribution} to a {@link import('prosemirror-model').Mark} - the mark names *must* be one of: `y-attributed-insert`, `y-attributed-delete`, `y-attributed-format`. No other mark names are permitted + * @param {AttributedNodesPredicate} [opts.attributedNodes] Optional predicate `(nodeName, kinds) => boolean`. When it returns `true` for an attributed node *and* a `{nodeName}--attributed` type exists in the schema, that node is rendered under the variant type (the `y-attributed-*` marks are still applied). `kinds` is `{ insert?, delete?, format? }`. The variant is a pure rendering concern - the canonical name is what is stored in the Y document. The predicate must be deterministic in `(nodeName, kinds)`. ++ * @param {YpmMatchNodes} [opts.matchNodes] Node-pairing predicate for the PM->Y diff (forwarded to `lib0/delta.diff`). Given a removed node `a` and an inserted node `b` (content deltas with canonical names), return whether they are the *same* node - i.e. diffed in place (descend/modify) vs. replaced (delete + insert). Defaults to name-equality. Override to raise the diff boundary at a strict node: report two same-named nodes as *different* when the child that identifies them changed, so a suggestion's old/new content lands as sibling blocks in the permissive grandparent instead of as schema-invalid siblings inside the strict node (e.g. BlockNote's `blockContainer`, content `blockContent blockGroup?`). What identifies a node is schema-specific, hence the integrator's to define. A plain text edit (same identity) still descends and merges normally. + * @returns {Plugin} + */ +export function syncPlugin(opts?: { + suggestionDoc?: Y.Doc | undefined; + mapAttributionToMark?: AttributionMapper | undefined; + attributedNodes?: AttributedNodesPredicate | undefined; ++ matchNodes?: YpmMatchNodes | undefined; +}): Plugin; +/** + * The y-prosemirror binding is a bi-directional synchronization with the provided Y.Type and the EditorView @@ -319,6 +321,7 @@ index 0000000000000000000000000000000000000000..c1da2aa33b86511936e9b1ba4d2d3c84 + attributionManager: Y.AbstractAttributionManager | null; + attributionMapper: AttributionMapper; + attributedNodes: AttributedNodesPredicate; ++ matchNodes: YpmMatchNodes; +}>; +export const $syncPluginStateUpdate: s.Schema<{ + ytype?: Y.Type | null | undefined; @@ -334,18 +337,18 @@ index 0000000000000000000000000000000000000000..c1da2aa33b86511936e9b1ba4d2d3c84 \ No newline at end of file diff --git a/dist/src/sync-plugin.d.ts.map b/dist/src/sync-plugin.d.ts.map new file mode 100644 -index 0000000000000000000000000000000000000000..df8c9df944fe1c64c46c648d913a0f8b52694bd7 +index 0000000000000000000000000000000000000000..3575d0e44bf8c37230381401f8e5a39d09092bfc --- /dev/null +++ b/dist/src/sync-plugin.d.ts.map @@ -0,0 +1 @@ -+{"version":3,"file":"sync-plugin.d.ts","sourceRoot":"","sources":["../../src/sync-plugin.js"],"names":[],"mappings":"AAgGA;;;;;;;;;;;;;;GAcG;AACH,kCALG;IAAqB,aAAa;IACD,oBAAoB;IACb,eAAe;CACvD,GAAU,MAAM,CA+LlB;AA7RD;;;GAGG;AACH;;;;;GAYE;AAEF;;;;;;GAME;mBAvCiB,MAAM;uBACF,mBAAmB;mBAWvB,aAAa"} ++{"version":3,"file":"sync-plugin.d.ts","sourceRoot":"","sources":["../../src/sync-plugin.js"],"names":[],"mappings":"AAsGA;;;;;;;;;;;;;;;GAeG;AACH,kCANG;IAAqB,aAAa;IACD,oBAAoB;IACb,eAAe;IAC1B,UAAU;CACvC,GAAU,MAAM,CAuMlB;AA3SD;;;GAGG;AACH;;;;;;GAiBE;AAEF;;;;;;GAME;mBA7CiB,MAAM;uBACF,mBAAmB;mBAYvB,aAAa"} \ No newline at end of file diff --git a/dist/src/sync-utils.d.ts b/dist/src/sync-utils.d.ts new file mode 100644 -index 0000000000000000000000000000000000000000..dfb00a847adcc5a1db01d557a8b0b056eefd1c9a +index 0000000000000000000000000000000000000000..7243ce51f02ae62155eb28a60ddd926d60b5a788 --- /dev/null +++ b/dist/src/sync-utils.d.ts -@@ -0,0 +1,146 @@ +@@ -0,0 +1,165 @@ +/** + * Transforms a {@link Node} into a {@link Y.XmlFragment} + * @param {Node} node @@ -433,6 +436,7 @@ index 0000000000000000000000000000000000000000..dfb00a847adcc5a1db01d557a8b0b056 +export function attributedVariant(canonicalName: string, format: Record | null | undefined, attributedNodes: AttributedNodesPredicate, schema: import("prosemirror-model").Schema): string; +export function defaultMapAttributionToMark(format: Record | null, attribution: T): Record | null; +export function deltaAttributionToFormat(d: delta.DeltaAny, attributionsToFormat: Function): ProsemirrorDelta; ++export function yattr2markname(attrName: string): string; +export function formattingAttributesToMarks(formatting: { + [key: string]: any; +} | null, schema: import("prosemirror-model").Schema): import("prosemirror-model").Mark[]; @@ -449,6 +453,24 @@ index 0000000000000000000000000000000000000000..dfb00a847adcc5a1db01d557a8b0b056 +export function deltaToPSteps(tr: import("prosemirror-state").Transaction, d: ProsemirrorDelta, pnode?: Node, currPos?: { + i: number; +}, attributedNodes?: AttributedNodesPredicate): import("prosemirror-state").Transaction; ++/** ++ * Default node-pairing predicate for the PM->Y diff, forwarded to ++ * `lib0/delta.diff`'s `matchNodes`: two nodes are the *same* node (diffed in ++ * place) iff they share a name - the standard `delta.diff` behavior. ++ * ++ * Override it via `syncPlugin`'s `matchNodes` option to make the diff treat ++ * certain same-named nodes as *different* (a whole-node delete + insert / a ++ * replace). The motivating case is raising the diff boundary for a strict ++ * container whose identifying child changed type (so a suggestion's old/new ++ * content lands as sibling blocks in the permissive grandparent instead of as ++ * schema-invalid siblings inside the strict node). Crucially, *what identifies ++ * a node* is schema-specific - e.g. "a `blockContainer` is identified by its ++ * first block-content child" is BlockNote's model, not the binding's - so that ++ * policy belongs in the integrator's predicate, not here. ++ * ++ * @type {YpmMatchNodes} ++ */ ++export const defaultMatchNodes: YpmMatchNodes; +export function deltaToPNode(d: ProsemirrorDelta, schema: import("prosemirror-model").Schema, dformat: delta.FormattingAttributes | null, attributedNodes?: AttributedNodesPredicate): Node; +export function docDiffToDelta(beforeDoc: Node, afterDoc: Node): delta.Delta<{ + name: string; @@ -495,11 +517,11 @@ index 0000000000000000000000000000000000000000..dfb00a847adcc5a1db01d557a8b0b056 \ No newline at end of file diff --git a/dist/src/sync-utils.d.ts.map b/dist/src/sync-utils.d.ts.map new file mode 100644 -index 0000000000000000000000000000000000000000..8d7883745029eee21f25288286021206007fd3ff +index 0000000000000000000000000000000000000000..97ba0c3b0bcaac4cc9c0fb69a56186697129c239 --- /dev/null +++ b/dist/src/sync-utils.d.ts.map @@ -0,0 +1 @@ -+{"version":3,"file":"sync-utils.d.ts","sourceRoot":"","sources":["../../src/sync-utils.js"],"names":[],"mappings":"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"} ++{"version":3,"file":"sync-utils.d.ts","sourceRoot":"","sources":["../../src/sync-utils.js"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/undo-plugin.d.ts b/dist/src/undo-plugin.d.ts new file mode 100644 @@ -531,8 +553,21 @@ index 0000000000000000000000000000000000000000..665bb84203a88b35e2961e7221a31896 +{"version":3,"file":"undo-plugin.d.ts","sourceRoot":"","sources":["../../src/undo-plugin.js"],"names":[],"mappings":"AA8JO,yCAFI,OAAO,MAAM,EAAE,WAAW,2BAmFpC;;iBAzOa,OAAO,MAAM,EAAE,WAAW;aAC1B;QAAE,QAAQ,EAAE,OAAO,mBAAmB,EAAE,iBAAiB,CAAC;QAAC,cAAc,EAAE,UAAU,CAAC,OAAO,4BAA4B,CAAC,CAAC,gBAAgB,CAAC,CAAA;KAAE,GAAG,IAAI;gBACrJ,OAAO;gBACP,OAAO;kBACP,OAAO;;uBAVE,mBAAmB;6CACG,gBAAgB"} \ No newline at end of file diff --git a/dist/src/utils.d.ts b/dist/src/utils.d.ts -deleted file mode 100644 -index 9006a87dd42992dfe0aa0f7ab5298983deb3357a..0000000000000000000000000000000000000000 +index 9006a87dd42992dfe0aa0f7ab5298983deb3357a..ff01b0ef7739349d9e4fd67f5197020b9db4210b 100644 +--- a/dist/src/utils.d.ts ++++ b/dist/src/utils.d.ts +@@ -1 +1,2 @@ + export function hashOfJSON(json: any): string; ++//# sourceMappingURL=utils.d.ts.map +\ No newline at end of file +diff --git a/dist/src/utils.d.ts.map b/dist/src/utils.d.ts.map +new file mode 100644 +index 0000000000000000000000000000000000000000..0fd58606be14f84b708e556ed09017a0520da035 +--- /dev/null ++++ b/dist/src/utils.d.ts.map +@@ -0,0 +1 @@ ++{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.js"],"names":[],"mappings":"AAmBO,iCAHI,GAAG,GACF,MAAM,CAEmG"} +\ No newline at end of file diff --git a/dist/src/y-prosemirror.d.ts b/dist/src/y-prosemirror.d.ts deleted file mode 100644 index c1f9468c4c77434a1ad9f49227fb1274f5ae1915..0000000000000000000000000000000000000000 @@ -544,10 +579,10 @@ deleted file mode 100644 index 61b864629455150ac073bf6a9e5b7f6f7e9e5037..0000000000000000000000000000000000000000 diff --git a/global.d.ts b/global.d.ts new file mode 100644 -index 0000000000000000000000000000000000000000..f94ae8cdc4fe7400e1e7f5ad7f5cb7a1170519f5 +index 0000000000000000000000000000000000000000..8f3fa2a7d1c5288bb54e9d558d00c3b412139dfc --- /dev/null +++ b/global.d.ts -@@ -0,0 +1,21 @@ +@@ -0,0 +1,31 @@ + +declare type YType = import('@y/y').Type +declare type AttributionManager = import('@y/y').AbstractAttributionManager @@ -566,11 +601,21 @@ index 0000000000000000000000000000000000000000..f94ae8cdc4fe7400e1e7f5ad7f5cb7a1 + * node. Must be deterministic in `(nodeName, kinds)`. + */ +declare type AttributedNodesPredicate = (nodeName: string, kinds: { insert?: boolean, delete?: boolean, format?: boolean }) => boolean ++/** ++ * Node-pairing predicate for the PM<->Y diff (forwarded to `lib0/delta.diff`'s ++ * `matchNodes`). Given a removed node `a` and an inserted node `b` (content ++ * deltas with canonical names), returns whether they are the *same* node - i.e. ++ * diffed in place (descend/modify) vs. replaced (delete + insert). The default ++ * is name-equality; an integrator overrides it to raise the diff boundary for ++ * schema-specific reasons. The "what identifies a node" policy is the ++ * integrator's, not the binding's. ++ */ ++declare type YpmMatchNodes = (a: import('lib0/delta').DeltaAny, b: import('lib0/delta').DeltaAny) => boolean +declare type SyncPluginState = import('lib0/schema').Unwrap +declare type SyncPluginStateUpdate = import('lib0/schema').Unwrap +declare type ProsemirrorDelta = import('lib0/schema').Unwrap diff --git a/package.json b/package.json -index 8eaef6bf2b216933047f528e3c3b0aa469df45e7..258a3b18cc50c11181b70a716953fdb1708bf840 100644 +index 8eaef6bf2b216933047f528e3c3b0aa469df45e7..264aa040efa65d9934939b5df912fea6dd7708a2 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,7 @@ @@ -611,7 +656,7 @@ index 8eaef6bf2b216933047f528e3c3b0aa469df45e7..258a3b18cc50c11181b70a716953fdb1 "homepage": "https://github.com/yjs/y-prosemirror#readme", "dependencies": { - "lib0": "^0.2.115-6" -+ "lib0": "^1.0.0-rc.13" ++ "lib0": "^1.0.0-rc.14" }, "peerDependencies": { - "@y/protocols": "^1.0.6-3", @@ -1144,7 +1189,7 @@ index 0000000000000000000000000000000000000000..79fa8f273361c11282e2c2df76c38895 + } + }) diff --git a/src/index.js b/src/index.js -index ac407e0c363309c970f3dbcbd66db00f9cd1656a..0c20333ce9f66f1a1e3e8e44da1ac4017bbba4cc 100644 +index ac407e0c363309c970f3dbcbd66db00f9cd1656a..3ac49220951d180ea85f5a7a3437d70fbae189b2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,627 +1,7 @@ @@ -1778,7 +1823,7 @@ index ac407e0c363309c970f3dbcbd66db00f9cd1656a..0c20333ce9f66f1a1e3e8e44da1ac401 +export * from './sync-plugin.js' +export * from './keys.js' +export * from './positions.js' -+export { docToDelta, $prosemirrorDelta, defaultMapAttributionToMark } from './sync-utils.js' ++export { docToDelta, $prosemirrorDelta, defaultMapAttributionToMark, yattr2markname, pmToFragment, fragmentToPm } from './sync-utils.js' +export * from './commands.js' +export * from './undo-plugin.js' +export * from './cursor-plugin.js' @@ -2048,16 +2093,17 @@ index 0000000000000000000000000000000000000000..963ea708dbe0e92b2d43fc031243c2e7 +} diff --git a/src/sync-plugin.js b/src/sync-plugin.js new file mode 100644 -index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054814dda91 +index 0000000000000000000000000000000000000000..5fd9fdd677c998301654e344d007af95506735c6 --- /dev/null +++ b/src/sync-plugin.js -@@ -0,0 +1,301 @@ +@@ -0,0 +1,316 @@ +import * as Y from '@y/y' +import { Plugin } from 'prosemirror-state' +import { + $prosemirrorDelta, + defaultAttributedNodes, + defaultMapAttributionToMark, ++ defaultMatchNodes, + deltaAttributionToFormat, + deltaToPSteps, + nodeToDelta @@ -2082,7 +2128,12 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 + * Predicate deciding which attributed nodes render under their + * `{nodeName}--attributed` variant. See {@link syncPlugin}. + */ -+ attributedNodes: /** @type {s.Schema} */ (s.$function) ++ attributedNodes: /** @type {s.Schema} */ (s.$function), ++ /** ++ * Node-pairing predicate for the PM->Y diff (`opts.matchNodes`). See ++ * {@link syncPlugin}. ++ */ ++ matchNodes: /** @type {s.Schema} */ (s.$function) +}) + +export const $syncPluginStateUpdate = s.$object({ @@ -2161,6 +2212,7 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 + * @param {Y.Doc} [opts.suggestionDoc] A {@link Y.Doc} to use for suggestion tracking + * @param {AttributionMapper} [opts.mapAttributionToMark] A function to map the {@link Y.Attribution} to a {@link import('prosemirror-model').Mark} - the mark names *must* be one of: `y-attributed-insert`, `y-attributed-delete`, `y-attributed-format`. No other mark names are permitted + * @param {AttributedNodesPredicate} [opts.attributedNodes] Optional predicate `(nodeName, kinds) => boolean`. When it returns `true` for an attributed node *and* a `{nodeName}--attributed` type exists in the schema, that node is rendered under the variant type (the `y-attributed-*` marks are still applied). `kinds` is `{ insert?, delete?, format? }`. The variant is a pure rendering concern - the canonical name is what is stored in the Y document. The predicate must be deterministic in `(nodeName, kinds)`. ++ * @param {YpmMatchNodes} [opts.matchNodes] Node-pairing predicate for the PM->Y diff (forwarded to `lib0/delta.diff`). Given a removed node `a` and an inserted node `b` (content deltas with canonical names), return whether they are the *same* node - i.e. diffed in place (descend/modify) vs. replaced (delete + insert). Defaults to name-equality. Override to raise the diff boundary at a strict node: report two same-named nodes as *different* when the child that identifies them changed, so a suggestion's old/new content lands as sibling blocks in the permissive grandparent instead of as schema-invalid siblings inside the strict node (e.g. BlockNote's `blockContainer`, content `blockContent blockGroup?`). What identifies a node is schema-specific, hence the integrator's to define. A plain text edit (same identity) still descends and merges normally. + * @returns {Plugin} + */ +export function syncPlugin (opts = {}) { @@ -2172,7 +2224,8 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 + ytype: null, + attributionManager: null, + attributionMapper: opts.mapAttributionToMark || defaultMapAttributionToMark, -+ attributedNodes: opts.attributedNodes || defaultAttributedNodes ++ attributedNodes: opts.attributedNodes || defaultAttributedNodes, ++ matchNodes: opts.matchNodes || defaultMatchNodes + }) + }, + apply: (tr, prevPluginState) => { @@ -2317,12 +2370,19 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 + const am = attributionManager || Y.noAttributionsManager + const mapper = pluginState.attributionMapper + const attributedNodes = pluginState.attributedNodes ++ const matchNodes = pluginState.matchNodes + const ycontent = deltaAttributionToFormat( + ytype.toDeltaDeep(am), + mapper + ).done() + const pcontent = nodeToDelta(view.state.doc, undefined, true).done() -+ const pmToYDiff = stripAttributionFormattingFromDelta(d.diff(ycontent, pcontent)) ++ // `matchNodes` raises the diff boundary at declared suggestion-boundary ++ // node types: a child-type change inside such a (strict) node becomes a ++ // whole-node replace, so its old/new content lands as sibling blocks in ++ // the permissive grandparent instead of as schema-invalid siblings ++ // inside the strict node. A no-op (name-equality) when no boundary nodes ++ // are configured. ++ const pmToYDiff = stripAttributionFormattingFromDelta(d.diff(ycontent, pcontent, matchNodes)) + if (!pmToYDiff.isEmpty()) { + /** @type {Y.Doc} */ (ytype.doc).transact(() => { + ytype.applyDelta(pmToYDiff, am) @@ -2333,7 +2393,7 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 + mapper + ).done() + const pcontentAfter = nodeToDelta(view.state.doc, undefined, true).done() -+ const pmReconcileDiff = d.diff(pcontentAfter, desiredPM) ++ const pmReconcileDiff = d.diff(pcontentAfter, desiredPM); + if (pmReconcileDiff.isEmpty()) return + const tr = view.state.tr + deltaToPSteps(tr, pmReconcileDiff, undefined, undefined, attributedNodes) @@ -2355,10 +2415,10 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 +} diff --git a/src/sync-utils.js b/src/sync-utils.js new file mode 100644 -index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d887b38d28 +index 0000000000000000000000000000000000000000..cbea2444b0a7c4ddf2825af47fd95afaa6e4f7a4 --- /dev/null +++ b/src/sync-utils.js -@@ -0,0 +1,752 @@ +@@ -0,0 +1,817 @@ +import * as Y from '@y/y' +import * as array from 'lib0/array' +import * as delta from 'lib0/delta' @@ -2377,6 +2437,7 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + ReplaceAroundStep, + ReplaceStep +} from 'prosemirror-transform' ++import { hashOfJSON } from './utils.js' + +export const $prosemirrorDelta = delta.$delta({ name: s.$string, attrs: s.$record(s.$string, s.$any), text: true, recursiveChildren: true }) + @@ -2532,6 +2593,38 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 +} + +/** ++ * Marks are stored as a flat `format` object keyed by mark name. Marks whose ++ * type does *not* exclude itself (declared with `excludes: ''`, e.g. a comment ++ * mark) may overlap on the same text span - several distinct instances coexist. ++ * Keying them all by the bare mark name would collide, so each overlapping mark ++ * gets a stable content-hash suffix (`name--`), keeping every instance on ++ * its own key. Self-excluding marks (strong/em/code/attribution marks) keep the ++ * bare name. `--<8 base64 chars>` is therefore a reserved suffix, symmetric to ++ * {@link ATTRIBUTED_SUFFIX} above. ++ */ ++const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/ ++ ++/** ++ * Strip a hashed overlapping-mark suffix to recover the PM mark name. Identity ++ * for bare (non-hashed) names. ++ * ++ * @param {string} attrName ++ * @return {string} ++ */ ++export const yattr2markname = attrName => hashedMarkNameRegex.exec(attrName)?.[1] ?? attrName ++ ++/** ++ * Inverse of {@link yattr2markname}: the delta format key for a PM mark. ++ * ++ * @param {import('prosemirror-model').Mark} mark ++ * @return {string} ++ */ ++const markToYattrName = mark => ++ mark.type.excludes(mark.type) ++ ? mark.type.name ++ : `${mark.type.name}--${hashOfJSON(mark.toJSON())}` ++ ++/** + * @param {readonly import('prosemirror-model').Mark[]} marks + */ +const marksToFormattingAttributes = marks => { @@ -2541,7 +2634,7 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + */ + const formatting = {} + marks.forEach(mark => { -+ formatting[mark.type.name] = mark.attrs ++ formatting[markToYattrName(mark)] = mark.attrs + }) + return formatting +} @@ -2550,13 +2643,14 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + * Convert a delta `format` object to PM marks. `null` entries (which mean + * "this mark is absent / cleared") are filtered out - a custom attribution + * mapper may emit `null` for absent attribution kinds, and a fresh insert -+ * should not materialize a mark for them. ++ * should not materialize a mark for them. Hashed overlapping-mark keys are ++ * mapped back to their mark name via {@link yattr2markname}. + * + * @param {{[key:string]:any}|null} formatting + * @param {import('prosemirror-model').Schema} schema + */ +export const formattingAttributesToMarks = (formatting, schema) => -+ object.map(formatting ?? {}, (v, k) => v != null ? schema.mark(k, v) : null).filter(m => m != null) ++ object.map(formatting ?? {}, (v, k) => v != null ? schema.mark(yattr2markname(k), v) : null).filter(m => m != null) + +/** + * @param {Array} ns @@ -2679,11 +2773,15 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + if (node == null) return + let resultingMarks = node.marks + object.forEach(format ?? {}, (v, k) => { -+ const markType = schema.marks[k] ++ const markName = yattr2markname(k) ++ const markType = schema.marks[markName] + if (markType == null) return ++ // For overlapping marks, remove the specific instance carried by this ++ // (hashed) key rather than every mark of the type. ++ const mark = node.marks.find(m => markToYattrName(m) === k) + resultingMarks = v == null -+ ? markType.removeFromSet(resultingMarks) -+ : schema.mark(k, v).addToSet(resultingMarks) ++ ? (mark ?? markType).removeFromSet(resultingMarks) ++ : schema.mark(markName, v).addToSet(resultingMarks) + }) + const targetType = schema.nodes[ + attributedVariant(canonicalNodeName(node.type.name), marksToFormattingAttributes(resultingMarks), attributedNodes, schema) @@ -2692,10 +2790,12 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + tr.setNodeMarkup(pos, targetType, object.assign({ 'y-attributed': true }, node.attrs), resultingMarks) + } else { + object.forEach(format ?? {}, (v, k) => { ++ const markName = yattr2markname(k) + if (v == null) { -+ tr.removeNodeMark(pos, schema.marks[k]) ++ const mark = node.marks.find(m => markToYattrName(m) === k) ++ tr.removeNodeMark(pos, mark ?? schema.marks[markName]) + } else { -+ tr.addNodeMark(pos, schema.mark(k, v)) ++ tr.addNodeMark(pos, schema.mark(markName, v)) + } + }) + } @@ -2786,10 +2886,16 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + const from = currPos.i + const to = currPos.i + math.min(pc.nodeSize - nOffset, i) + object.forEach(op.format, (v, k) => { ++ const markName = yattr2markname(k) + if (v == null) { -+ tr.removeMark(from, to, schema.marks[k]) ++ // A format-remove carries no attrs, so match the specific ++ // instance on the current text node - sibling overlaps of the ++ // same type (e.g. another comment) must not be removed with it. ++ // Their relative array order is not significant (see CAVEATS). ++ const mark = pc.marks.find(m => markToYattrName(m) === k) ++ tr.removeMark(from, to, mark ?? schema.marks[markName]) + } else { -+ tr.addMark(from, to, schema.mark(k, v)) ++ tr.addMark(from, to, schema.mark(markName, v)) + } + }) + } @@ -2877,6 +2983,25 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 +} + +/** ++ * Default node-pairing predicate for the PM->Y diff, forwarded to ++ * `lib0/delta.diff`'s `matchNodes`: two nodes are the *same* node (diffed in ++ * place) iff they share a name - the standard `delta.diff` behavior. ++ * ++ * Override it via `syncPlugin`'s `matchNodes` option to make the diff treat ++ * certain same-named nodes as *different* (a whole-node delete + insert / a ++ * replace). The motivating case is raising the diff boundary for a strict ++ * container whose identifying child changed type (so a suggestion's old/new ++ * content lands as sibling blocks in the permissive grandparent instead of as ++ * schema-invalid siblings inside the strict node). Crucially, *what identifies ++ * a node* is schema-specific - e.g. "a `blockContainer` is identified by its ++ * first block-content child" is BlockNote's model, not the binding's - so that ++ * policy belongs in the integrator's predicate, not here. ++ * ++ * @type {YpmMatchNodes} ++ */ ++export const defaultMatchNodes = (a, b) => a.name === b.name ++ ++/** + * @param {ProsemirrorDelta} d + * @param {import('prosemirror-model').Schema} schema + * @param {delta.FormattingAttributes|null} dformat @@ -2971,10 +3096,10 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + deltaModifyNodeAt(beforeDoc, step.pos, d => { d.retain(1, marksToFormattingAttributes([step.mark])) }) + ) + .if(RemoveMarkStep, (step, { beforeDoc }) => -+ deltaModifyNodeAt(beforeDoc, step.from, d => { d.retain(step.to - step.from, { [step.mark.type.name]: null }) }) ++ deltaModifyNodeAt(beforeDoc, step.from, d => { d.retain(step.to - step.from, { [markToYattrName(step.mark)]: null }) }) + ) + .if(RemoveNodeMarkStep, (step, { beforeDoc }) => -+ deltaModifyNodeAt(beforeDoc, step.pos, d => { d.retain(1, { [step.mark.type.name]: null }) }) ++ deltaModifyNodeAt(beforeDoc, step.pos, d => { d.retain(1, { [markToYattrName(step.mark)]: null }) }) + ) + .if(AttrStep, (step, { beforeDoc }) => + deltaModifyNodeAt(beforeDoc, step.pos, d => { d.modify(delta.create().setAttr(step.attr, step.value)) }) @@ -3358,8 +3483,44 @@ index 0000000000000000000000000000000000000000..70a7ae423be9bfd7a061984ce4ca74f4 + }) +} diff --git a/src/utils.js b/src/utils.js -deleted file mode 100644 -index f62b6a1abc732b9c13eb83fd667534173706273d..0000000000000000000000000000000000000000 +index f62b6a1abc732b9c13eb83fd667534173706273d..aa4e28a8060e11871f1548c840444de1e8a08ce9 100644 +--- a/src/utils.js ++++ b/src/utils.js +@@ -1,20 +1,20 @@ +-import * as sha256 from 'lib0/hash/sha256' ++import * as rabin from 'lib0/hash/rabin' + import * as buf from 'lib0/buffer' + + /** +- * Custom function to transform sha256 hash to N byte ++ * Compact, stable base64 tag of an arbitrary json-serializable value. It only ++ * needs to disambiguate overlapping marks of the same type (see `markToYattrName` ++ * in sync-utils.js), not resist attacks, so a cheap Rabin fingerprint is plenty. ++ * ++ * We use the *full* 4-byte (degree-32) fingerprint rather than truncating a ++ * wider one: a Rabin fingerprint propagates small input changes into its ++ * low-order bytes, so slicing the leading bytes off a degree-64 fingerprint ++ * collides for near-identical inputs (e.g. `{id:4}` vs `{id:5}`). The 4 bytes ++ * encode to 8 base64 chars - the length `hashedMarkNameRegex` expects - so ++ * documents written by older (sha256-based) versions still parse: the suffix is ++ * only ever stripped on read (by pattern), never recomputed. + * +- * @param {Uint8Array} digest +- */ +-const _convolute = digest => { +- const N = 6 +- for (let i = N; i < digest.length; i++) { +- digest[i % N] = digest[i % N] ^ digest[i] +- } +- return digest.slice(0, N) +-} +- +-/** + * @param {any} json ++ * @return {string} + */ +-export const hashOfJSON = (json) => buf.toBase64(_convolute(sha256.digest(buf.encodeAny(json)))) ++export const hashOfJSON = (json) => buf.toBase64(rabin.fingerprint(rabin.StandardIrreducible32, buf.encodeAny(json))) diff --git a/src/y-prosemirror.js b/src/y-prosemirror.js deleted file mode 100644 index bb072b6e31a0184a56d7873dcae647f0d5711559..0000000000000000000000000000000000000000 diff --git a/patches/@y__y@14.0.0-rc.16.patch b/patches/@y__y@14.0.0-rc.16.patch deleted file mode 100644 index 42e00d2080..0000000000 --- a/patches/@y__y@14.0.0-rc.16.patch +++ /dev/null @@ -1,193 +0,0 @@ -diff --git a/dist/src/utils/UndoManager.d.ts b/dist/src/utils/UndoManager.d.ts -index 2670b9688224b31267f9e16a21be73ae6b39af84..2f614bb70c302ee0b277f083ee6f1e15a0c73476 100644 ---- a/dist/src/utils/UndoManager.d.ts -+++ b/dist/src/utils/UndoManager.d.ts -@@ -20,7 +20,7 @@ export class StackItem { - * filter returns false, the type/item won't be deleted even it is in the - * undo/redo scope. - * @property {Set} [UndoManagerOptions.trackedOrigins=new Set([null])] -- * @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..). -+ * @property {boolean} [ignoreRemoteAttributeChanges] By default, the UndoManager will never overwrite remote changes. In some cases this might be the expected behavior. This property enables overwriting remote changes on attribute changes. (previously named `ignoreRemoteMapChanges`) - * @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty. - */ - /** -@@ -52,7 +52,7 @@ export class UndoManager extends ObservableV2<{ - * @param {Doc|YType|Array} typeScope Limits the scope of the UndoManager. If this is set to a ydoc instance, all changes on that ydoc will be undone. If set to a specific type, only changes on that type or its children will be undone. Also accepts an array of types. - * @param {UndoManagerOptions} options - */ -- constructor(typeScope: Doc | YType | Array, { captureTimeout, captureTransaction, deleteFilter, trackedOrigins, ignoreRemoteMapChanges, doc }?: UndoManagerOptions); -+ constructor(typeScope: Doc | YType | Array, { captureTimeout, captureTransaction, deleteFilter, trackedOrigins, ignoreRemoteAttributeChanges, doc }?: UndoManagerOptions); - /** - * @type {Array} - */ -@@ -83,7 +83,7 @@ export class UndoManager extends ObservableV2<{ - */ - currStackItem: StackItem | null; - lastChange: number; -- ignoreRemoteMapChanges: boolean; -+ ignoreRemoteAttributeChanges: boolean; - captureTimeout: number; - /** - * @param {Transaction} transaction -@@ -151,7 +151,7 @@ export class UndoManager extends ObservableV2<{ - canRedo(): boolean; - } - export function undoContentIds(ydoc: Doc, contentIds: ContentIds, opts?: UndoManagerOptions): void; --export function redoItem(transaction: Transaction, item: Item, redoitems: Set, itemsToDelete: IdSet, ignoreRemoteMapChanges: boolean, um: import("../utils/UndoManager.js").UndoManager): Item | null; -+export function redoItem(transaction: Transaction, item: Item, redoitems: Set, itemsToDelete: IdSet, ignoreRemoteAttributeChanges: boolean, um: import("../utils/UndoManager.js").UndoManager): Item | null; - export function keepItem(item: Item | null, keep: boolean): void; - export type UndoManagerOptions = { - captureTimeout?: number | undefined; -@@ -168,9 +168,9 @@ export type UndoManagerOptions = { - deleteFilter?: ((arg0: Item) => boolean) | undefined; - trackedOrigins?: Set | undefined; - /** -- * Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..). -+ * By default, the UndoManager will never overwrite remote changes. In some cases this might be the expected behavior. This property enables overwriting remote changes on attribute changes. (previously named `ignoreRemoteMapChanges`) - */ -- ignoreRemoteMapChanges?: boolean | undefined; -+ ignoreRemoteAttributeChanges?: boolean | undefined; - /** - * The document that this UndoManager operates on. Only needed if typeScope is empty. - */ -diff --git a/dist/src/utils/UndoManager.d.ts.map b/dist/src/utils/UndoManager.d.ts.map -index 597c791905316e578275c84f1a9265ffa78e092a..7937771c7c931d9ffd2b2761cc2b33b51cb4bc8c 100644 ---- a/dist/src/utils/UndoManager.d.ts.map -+++ b/dist/src/utils/UndoManager.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"UndoManager.d.ts","sourceRoot":"","sources":["../../../src/utils/UndoManager.js"],"names":[],"mappings":"AAeA;IACE;;;OAGG;IACH,wBAHW,KAAK,aACL,KAAK,EASf;IANC,kCAAyB;IACzB,kCAAwB;IACxB;;OAEG;IACH,oBAAqB;CAExB;AAgGD;;;;;;;;;;;GAWG;AAEH;;;;;;GAMG;AAEH;;;;;;;;GAQG;AACH;wBAF8C,CAAS,IAAc,EAAd,cAAc,EAAE,IAAW,EAAX,WAAW,KAAE,IAAI;yBAAuB,CAAS,IAAc,EAAd,cAAc,EAAE,IAAW,EAAX,WAAW,KAAE,IAAI;qBAAmB,CAAS,IAAwD,EAAxD;QAAE,gBAAgB,EAAE,OAAO,CAAC;QAAC,gBAAgB,EAAE,OAAO,CAAA;KAAE,KAAE,IAAI;0BAAwB,CAAS,IAAc,EAAd,cAAc,EAAE,IAAW,EAAX,WAAW,KAAE,IAAI;;IAGnT;;;OAGG;IACH,uBAHW,GAAG,GAAC,KAAK,GAAC,KAAK,CAAC,KAAK,CAAC,sGACtB,kBAAkB,EAsG5B;IA3FC;;OAEG;IACH,OAFU,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,CAEb;IACf,SAAc;IAEd,qBA9CmB,IAAI,KAAE,OAAO,CA8CA;IAEhC,yBAAoC;IACpC,2BAlDmB,WAAW,KAAE,OAAO,CAkDK;IAC5C;;OAEG;IACH,WAFU,KAAK,CAAC,SAAS,CAAC,CAEP;IACnB;;OAEG;IACH,WAFU,KAAK,CAAC,SAAS,CAAC,CAEP;IACnB;;;;OAIG;IACH,SAFU,OAAO,CAEG;IACpB,iBAAoB;IACpB;;;;OAIG;IACH,eAFU,SAAS,GAAC,IAAI,CAEC;IACzB,mBAAmB;IACnB,gCAAoD;IACpD,uBAAoC;IACpC;;OAEG;IACH,uCAFW,WAAW,UAmDrB;IAOH;;;;OAIG;IACH,mBAFW,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,GAAG,QAY1C;IAED;;OAEG;IACH,yBAFW,GAAG,QAIb;IAED;;OAEG;IACH,4BAFW,GAAG,QAIb;IAED,gEAcC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,sBAEC;IAED;;;;OAIG;IACH,QAFY,SAAS,OAAC,CAWrB;IAED;;;;OAIG;IACH,QAFY,SAAS,OAAC,CAWrB;IAED;;;;OAIG;IACH,WAFY,OAAO,CAIlB;IAED;;;;OAIG;IACH,WAFY,OAAO,CAIlB;CAOF;AAWM,qCAJI,GAAG,cACH,UAAU,SACV,kBAAkB,QAM5B;AAsBM,sCAXI,WAAW,QACX,IAAI,aACJ,GAAG,CAAC,IAAI,CAAC,iBACT,KAAK,0BACL,OAAO,MACP,OAAO,yBAAyB,EAAE,WAAW,GAE5C,IAAI,GAAC,IAAI,CAyGpB;AAWM,+BAHI,IAAI,GAAC,IAAI,QACT,OAAO,QAOjB;;;;;;iCA9ZsB,WAAW,KAAE,OAAO;;;;;;;2BACpB,IAAI,KAAE,OAAO;;;;;;;;;;;;eAWtB,SAAS;YACT,GAAG;UACH,MAAM,GAAC,MAAM;wBACb,GAAG,CAAC,KAAK,EAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;;6BA3Id,iBAAiB;sBAUxB,aAAa;oBADf,UAAU;qBANK,oBAAoB"} -\ No newline at end of file -+{"version":3,"file":"UndoManager.d.ts","sourceRoot":"","sources":["../../../src/utils/UndoManager.js"],"names":[],"mappings":"AAeA;IACE;;;OAGG;IACH,wBAHW,KAAK,aACL,KAAK,EASf;IANC,kCAAyB;IACzB,kCAAwB;IACxB;;OAEG;IACH,oBAAqB;CAExB;AAgGD;;;;;;;;;;;GAWG;AAEH;;;;;;GAMG;AAEH;;;;;;;;GAQG;AACH;wBAF8C,CAAS,IAAc,EAAd,cAAc,EAAE,IAAW,EAAX,WAAW,KAAE,IAAI;yBAAuB,CAAS,IAAc,EAAd,cAAc,EAAE,IAAW,EAAX,WAAW,KAAE,IAAI;qBAAmB,CAAS,IAAwD,EAAxD;QAAE,gBAAgB,EAAE,OAAO,CAAC;QAAC,gBAAgB,EAAE,OAAO,CAAA;KAAE,KAAE,IAAI;0BAAwB,CAAS,IAAc,EAAd,cAAc,EAAE,IAAW,EAAX,WAAW,KAAE,IAAI;;IAGnT;;;OAGG;IACH,uBAHW,GAAG,GAAC,KAAK,GAAC,KAAK,CAAC,KAAK,CAAC,4GACtB,kBAAkB,EAsG5B;IA3FC;;OAEG;IACH,OAFU,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,CAEb;IACf,SAAc;IAEd,qBA9CmB,IAAI,KAAE,OAAO,CA8CA;IAEhC,yBAAoC;IACpC,2BAlDmB,WAAW,KAAE,OAAO,CAkDK;IAC5C;;OAEG;IACH,WAFU,KAAK,CAAC,SAAS,CAAC,CAEP;IACnB;;OAEG;IACH,WAFU,KAAK,CAAC,SAAS,CAAC,CAEP;IACnB;;;;OAIG;IACH,SAFU,OAAO,CAEG;IACpB,iBAAoB;IACpB;;;;OAIG;IACH,eAFU,SAAS,GAAC,IAAI,CAEC;IACzB,mBAAmB;IACnB,sCAAgE;IAChE,uBAAoC;IACpC;;OAEG;IACH,uCAFW,WAAW,UAmDrB;IAOH;;;;OAIG;IACH,mBAFW,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,GAAG,QAY1C;IAED;;OAEG;IACH,yBAFW,GAAG,QAIb;IAED;;OAEG;IACH,4BAFW,GAAG,QAIb;IAED,gEAcC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,sBAEC;IAED;;;;OAIG;IACH,QAFY,SAAS,OAAC,CAWrB;IAED;;;;OAIG;IACH,QAFY,SAAS,OAAC,CAWrB;IAED;;;;OAIG;IACH,WAFY,OAAO,CAIlB;IAED;;;;OAIG;IACH,WAFY,OAAO,CAIlB;CAOF;AAWM,qCAJI,GAAG,cACH,UAAU,SACV,kBAAkB,QAM5B;AAsBM,sCAXI,WAAW,QACX,IAAI,aACJ,GAAG,CAAC,IAAI,CAAC,iBACT,KAAK,gCACL,OAAO,MACP,OAAO,yBAAyB,EAAE,WAAW,GAE5C,IAAI,GAAC,IAAI,CA4GpB;AAWM,+BAHI,IAAI,GAAC,IAAI,QACT,OAAO,QAOjB;;;;;;iCAjasB,WAAW,KAAE,OAAO;;;;;;;2BACpB,IAAI,KAAE,OAAO;;;;;;;;;;;;eAWtB,SAAS;YACT,GAAG;UACH,MAAM,GAAC,MAAM;wBACb,GAAG,CAAC,KAAK,EAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;;6BA3Id,iBAAiB;sBAUxB,aAAa;oBADf,UAAU;qBANK,oBAAoB"} -\ No newline at end of file -diff --git a/dist/src/ytype.d.ts.map b/dist/src/ytype.d.ts.map -index 61397c8530690c01be91a97afa4007338ca8060e..608ab7ab770d86eba126fc306594eee2bce5cc72 100644 ---- a/dist/src/ytype.d.ts.map -+++ b/dist/src/ytype.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"ytype.d.ts","sourceRoot":"","sources":["../../src/ytype.js"],"names":[],"mappings":"AA4CO,4CAAiH;AAUjH,6DAJI,KAAK,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,OAAC,WAC7B,OAAO,GACN,WAAW,OAAC,CAgCvB;AAWD;IACE;;;;;;OAMG;IACH,kBANW,IAAI,GAAC,IAAI,SACT,IAAI,GAAC,IAAI,SACT,MAAM,qBACN,GAAG,CAAC,MAAM,EAAC,GAAG,CAAC,MACf,0BAA0B,EAQpC;IALC,kBAAgB;IAChB,mBAAkB;IAClB,cAAkB;IAClB,oCAA0C;IAC1C,gFAAY;IAGd;;OAEG;IACH,gBAgBC;IAED;;;;;;;OAOG;IACH,wBAPW,WAAW,UACX,KAAK,UACL,MAAM;;aA4EhB;CACF;AAqHM,2CATI,WAAW,UACX,KAAK,WACL,oBAAoB,WACpB,OAAO,mBAAmB,EAAE,eAAe;;SA0BrD;AASM,iDANI,WAAW,UACX,KAAK,WACL,oBAAoB,UACpB,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM;;SA0B3B;AAWM,wCARI,WAAW,WACX,oBAAoB,UACpB,MAAM,GACL,oBAAoB,CAkD/B;AAED;IACE;;;OAGG;IACH,eAHW,IAAI,SACJ,MAAM,EAOhB;IAHC,QAAU;IACV,cAAkB;IAClB,kBAA8C;CAEjD;AAqDM,mCAHI,KAAK,SACL,MAAM,4BAgDhB;AAWM,kDAJI,KAAK,CAAC,iBAAiB,CAAC,SACxB,MAAM,OACN,MAAM,QAiChB;AAQM,mCAHI,KAAK,GACJ,KAAK,CAAC,IAAI,CAAC,CAWtB;AAUM,wCAJI,KAAK,eACL,WAAW,SACV,MAAM,CAAC,GAAG,CAAC,QActB;AAED;;;GAGG;AACH,mBAFgC,KAAK,SAAvB,KAAK,CAAC,SAAU;IA2D5B;;;;OAIG;IACH,YAJ+B,EAAE,SAAnB,KAAK,CAAC,SAAU,KACnB,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,GACd,KAAK,CAAC,EAAE,CAAC,CAMpB;IAjED;;OAEG;IACH,mBAFW,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,OAAC,EAqDxC;IAlDC;;OAEG;IACH,MAFU,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAEwB;IAC/D;;OAEG;IACH,OAFU,IAAI,GAAC,IAAI,CAEF;IACjB;;OAEG;IACH,MAFU,GAAG,CAAC,MAAM,EAAC,IAAI,CAAC,CAEL;IACrB;;OAEG;IACH,QAFU,IAAI,GAAC,IAAI,CAED;IAClB;;OAEG;IACH,KAFU,GAAG,GAAC,IAAI,CAEH;IACf,gBAAgB;IAChB;;;OAGG;IACH,KAFU,YAAY,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAC,WAAW,CAAC,CAEhC;IAC/B;;;OAGG;IACH,MAFU,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,EAAC,WAAW,CAAC,CAEjB;IAChC;;OAEG;IACH,eAFU,IAAI,GAAG,KAAK,CAAC,iBAAiB,CAAC,CAEhB;IACzB;;;OAGG;IACH,iBAAqE;IACrE,uBAA8E;IAK9E;;;OAGG;IACH,wBAA2B;IAc7B,qBAGC;IAED;;;OAGG;IACH,cAFU,KAAK,CAAC,YAAY,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAIhD;IAED;;OAEG;IACH,cAFY,KAAK,CAAC,GAAG,CAAC,OAAC,CAItB;IAED;;;;;;;;;OASG;IACH,cAHW,GAAG,QACH,IAAI,GAAC,IAAI,QASnB;IAFG,aAAmB;IAIvB;;OAEG;IACH,SAFY,KAAK,CAAC,KAAK,CAAC,CAMvB;IAED;;;;;;OAMG;IACH,2BAHW,WAAW,cACX,GAAG,CAAC,IAAI,GAAC,MAAM,CAAC,QAY1B;IAED;;;;;;OAMG;IACH,QAJ8E,CAAC,SAAlE,CAAE,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,WAAW,KAAK,IAAK,KAClE,CAAC,GACA,CAAC,CAKZ;IAED;;;;;;OAMG;IACH,YAJwD,CAAC,SAA5C,CAAU,IAAa,EAAb,MAAM,CAAC,KAAK,CAAC,EAAC,IAAW,EAAX,WAAW,KAAE,IAAK,KAC5C,CAAC,GACA,CAAC,CAKZ;IAED;;;;OAIG;IACH,aAFW,CAAC,IAAI,EAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAC,EAAE,EAAC,WAAW,KAAG,IAAI,QAIjE;IAED;;;;OAIG;IACH,iBAFW,CAAS,IAAa,EAAb,MAAM,CAAC,KAAK,CAAC,EAAC,IAAW,EAAX,WAAW,KAAE,IAAI,QAIlD;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,eAdwB,IAAI,SAAf,OAAS,eAEX,0BAA0B,SAElC;QAAsB,aAAa;QACZ,aAAa;QACb,aAAa;QACd,YAAY;QACc,QAAQ;QACpC,IAAI;KACxB,GAAS,IAAI,SAAS,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAkO7F;IAED;;;;;;OAMG;IACH,iBAHW,0BAA0B,GACzB,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAI7B;IAED;;;;;;;OAOG;IACH,qBALW,KAAK,CAAC,QAAQ,OACd,0BAA0B,QA8CpC;IAED;;;;;;OAMG;IACH,SAFY,KAAK,CAAC,KAAK,CAAC,CAMvB;IAED;;OAEG;IACH,mBAMC;IAED;;;;;;OAMG;IACH,iCAJW,MAAM,QAMhB;IAED;;;;;;;;;;;OAWG;IACH,eAToE,GAAG,SAAzD,OAAO,CAAC,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAC,MAAM,CAAE,EAChB,GAAG,SAAxC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAE,iBAEvC,GAAG,kBACH,GAAG,GACF,GAAG,CAOd;IAED;;;;;;;OAOG;IACH,eAL2E,GAAG,SAAhE,OAAO,CAAC,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAC,MAAM,GAAC,MAAM,CAAE,iBAC/D,GAAG,GACF,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAC,SAAS,CAKxD;IAED;;;;;;;OAOG;IACH,8BALW,MAAM,GACL,OAAO,CAMlB;IAED;;;;;;;OAOG;IACH,2BALW,QAAQ,GACP,GAAG,GAAG,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAC,CAMjH;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,cAJW,MAAM,WACN,KAAK,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,WACtE,KAAK,CAAC,oBAAoB,QAIpC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,cALW,MAAM,UACN,MAAM,WACN,KAAK,CAAC,oBAAoB,QAKpC;IAED;;;;;;OAMG;IACH,cAJW,KAAK,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAMhF;IAED;;;;OAIG;IACH,iBAFW,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAIvC;IAED;;;;;OAKG;IACH,cAHW,MAAM,WACN,MAAM,QAIhB;IAED;;;;;OAKG;IACH,WAHW,MAAM,GACL,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAI5C;IAED;;;;;;;OAOG;IACH,cAJW,MAAM,QACN,MAAM,GACL,KAAK,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAInD;IAED;;;;;;OAMG;IACH,WAFY,KAAK,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAkBnF;IAED;;;OAGG;IACH,UAFY;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE;YAAE,CAAC,CAAC,EAAC,MAAM,GAAC,MAAM,GAAG,GAAG,CAAA;SAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;KAAG,CA0BxF;IAED;;;OAGG;IACH,wBAFG;QAAuB,QAAQ;KACjC,UAqBA;IAED;;;;;;;;OAQG;IACH,IALa,CAAC,KACH,CAAC,KAAK,EAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,GAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAC,KAAK,EAAC,MAAM,KAAG,CAAC,GACtF,KAAK,CAAC,CAAC,CAAC,CAKnB;IAED;;;;OAIG;IACH,WAFW,CAAC,KAAK,EAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,GAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAC,KAAK,EAAC,MAAM,KAAG,GAAG,QAInG;IAED;;;;OAIG;IACH,eAFW,CAAC,GAAG,EAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAC,GAAG,EAAC,OAAO,CAAC,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAC,MAAM,CAAC,EAAC,KAAK,EAAC,IAAI,KAAG,GAAG,QAQ5H;IAED;;;;OAIG;IACH,YAFY,gBAAgB,CAAC,OAAO,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,CAIpF;IAED;;;;OAIG;IACH,cAFY,gBAAgB,CAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAIhE;IAED;;;;OAIG;IACH,eAFY,gBAAgB,CAAC,GAAG,CAAC,IAAI,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAE,CAAC,GAAG,CAAC,CAAC,CAIxH;IAED;;;;OAIG;IACH,gBAFY,MAAM,CAIjB;IASD;;;;;;;;;OASG;IACH,gBAFW,eAAe,GAAG,eAAe,QAW3C;IA1BD;;OAEG;IACH,oCAFW,IAAI,WAId;CAsBF;AAOM,uBAJ+C,KAAK,SAA9C,OAAQ,YAAY,EAAE,iBAAkB,UAC1C,KAAK,GACJ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,YAAY,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAElB;AACpD,6CAA6C;AAMtC,gDAHI,WAAW,SACX,KAAK,uCAiBf;AAOM,8BAJI,GAAG,KACH,GAAG,GACF,OAAO,CAEgH;AAyB5H,oCARI,KAAK,CAAC,GAAG,CAAC,SACV,MAAM,OACN,MAAM,GACL,KAAK,CAAC,GAAG,CAAC,CAgCrB;AAYM,kCAPI,KAAK,SACL,MAAM,GACL,GAAG,CAqBd;AAaM,yDARI,WAAW,UACX,KAAK,iBACL,IAAI,OAAC,WACL,KAAK,CAAC,MAAM,CAAC,QA4DvB;AAaM,oDARI,WAAW,UACX,KAAK,SACL,MAAM,WACN,KAAK,CAAC;QAAO,MAAM,GAAC,GAAG;CAAC,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM,GAAC,IAAI,GAAC,MAAM,GAAC,UAAU,CAAC,QA4C5E;AAaM,kDAPI,WAAW,UACX,KAAK,WACL,KAAK,CAAC;QAAO,MAAM,GAAC,GAAG;CAAC,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM,GAAC,IAAI,GAAC,MAAM,GAAC,UAAU,CAAC,QAe5E;AAWM,4CARI,WAAW,UACX,KAAK,SACL,MAAM,UACN,MAAM,QAyChB;AAYM,2CAPI,WAAW,UACX,KAAK,OACL,MAAM,QAUhB;AAWM,wCARI,WAAW,UACX,KAAK,OACL,MAAM,SACN,MAAM,QAqChB;AAUM,mCAPI,KAAK,CAAC,GAAG,CAAC,OACV,MAAM,GACL;QAAO,MAAM,GAAC,GAAG;CAAC,GAAC,MAAM,GAAC,IAAI,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM,GAAC,UAAU,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,SAAS,CAS3F;AASM,sCANI,KAAK,CAAC,GAAG,CAAC;;;;EAkBpB;AA0BM,gCAf8B,SAAS,SAAhC,KAAK,CAAC,eAAgB,KACzB,SAAS,UACT,KAAK,iBACL,GAAG,CAAC,MAAM,GAAC,IAAI,CAAC,OAAC,MACjB,0BAA0B,QAC1B,OAAO,aACP,GAAG,CAAC,KAAK,CAAC,GAAC,GAAG,CAAC,KAAK,EAAC,GAAG,CAAC,GAAC,IAAI,iBAC9B,KAAK,OAAC,kBACN,KAAK,OAAC,SACN,GAAG,YACH,GAAG,QA8Cb;AAUM,mCAPI,KAAK,CAAC,GAAG,CAAC,OACV,MAAM,GACL,OAAO,CASlB;AASM,gCANI,IAAI,YACJ,QAAQ,GAAC,SAAS,WAO+F;AAWrH,2CARI,KAAK,CAAC,GAAG,CAAC,OACV,MAAM,YACN,QAAQ,GACP;QAAO,MAAM,GAAC,GAAG;CAAC,GAAC,MAAM,GAAC,IAAI,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM,GAAC,UAAU,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,SAAS,CAW3F;AAUM,8CAPI,KAAK,CAAC,GAAG,CAAC,YACV,QAAQ;;;;EAwBlB;AASM,wCANI,KAAK,CAAC,GAAG,CAAC,GAAG;IAAE,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;CAAE,GACvC,gBAAgB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAQvC;AAQM,yCAHI,eAAe,GAAG,eAAe,GAChC,WAAW,CAEsD;AAQtE,2CAHI,eAAe,GAAG,eAAe,GAChC,aAAa,CAE0D;AAQ5E,yCAHI,eAAe,GAAG,eAAe,GAChC,WAAW,CActB;AAMM,2CAHI,eAAe,GAAG,eAAe,GAChC,aAAa,CAE2E;AAQ7F,0CAHI,eAAe,GAAG,eAAe,GAChC,YAAY,CAEuD;AAQxE,wCAHI,eAAe,GAAG,eAAe,GAChC,UAAU,CAE0E;AAMzF,wCAHI,eAAe,GAAG,eAAe,GAChC,UAAU,CASrB;AAMM,2CAHI,eAAe,GAAG,eAAe,GAChC,aAAa,CAEuD;AAQzE,4CAHI,eAAe,GAAG,eAAe,GAChC,cAAc,CAEwD;AAElF;;;;GAIG;AACH,0BAFU,KAAK,CAAC,CAAS,IAAiC,EAAjC,eAAe,GAAG,eAAe,KAAE,eAAe,CAAC,CAc3E;AAMM,yCAHI,eAAe,GAAG,eAAe,QACjC,MAAM,+CAE0E;AASpF,mCANI,eAAe,GAAG,eAAe,GAChC,KAAK,CAUhB;qBAvkEY;QAAO,MAAM,GAAC,GAAG;CAAC,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM,GAAC,IAAI,GAAC,MAAM,GAAC,UAAU,YAAQ,KAAK,CAAC,GAAG,CAAC;kCA48C3D,KAAK,SAAtB,KAAK,CAAC,SAAU,IACjB,KAAK,CAAC,kBAAkB,CAAC,KAAK,EAAE;IACtC,KAAK,EAAE,GAAG,CAAC,IAAI,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAE,CAAC;IACxG,QAAQ,EAAE,YAAY,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAA;CAC1D,CAAC;yBAKY,IAAI,oBACV,OAAO,CAAC,IAAI,EAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,SAAS,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;qBAj+C7J,mBAAmB;uBAOH,mBAAmB;uBAhCnB,YAAY;wBASX,aAAa;mBADlB,aAAa;4BAiBzB,mBAAmB;8BAAnB,mBAAmB;4BAAnB,mBAAmB;8BAAnB,mBAAmB;6BAAnB,mBAAmB;2BAAnB,mBAAmB;2BAAnB,mBAAmB;8BAAnB,mBAAmB;+BAAnB,mBAAmB"} -\ No newline at end of file -+{"version":3,"file":"ytype.d.ts","sourceRoot":"","sources":["../../src/ytype.js"],"names":[],"mappings":"AA4CO,4CAAiH;AAUjH,6DAJI,KAAK,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,OAAC,WAC7B,OAAO,GACN,WAAW,OAAC,CAgCvB;AAWD;IACE;;;;;;OAMG;IACH,kBANW,IAAI,GAAC,IAAI,SACT,IAAI,GAAC,IAAI,SACT,MAAM,qBACN,GAAG,CAAC,MAAM,EAAC,GAAG,CAAC,MACf,0BAA0B,EAQpC;IALC,kBAAgB;IAChB,mBAAkB;IAClB,cAAkB;IAClB,oCAA0C;IAC1C,gFAAY;IAGd;;OAEG;IACH,gBAgBC;IAED;;;;;;;OAOG;IACH,wBAPW,WAAW,UACX,KAAK,UACL,MAAM;;aA4EhB;CACF;AAqHM,2CATI,WAAW,UACX,KAAK,WACL,oBAAoB,WACpB,OAAO,mBAAmB,EAAE,eAAe;;SA0BrD;AASM,iDANI,WAAW,UACX,KAAK,WACL,oBAAoB,UACpB,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM;;SA0B3B;AAWM,wCARI,WAAW,WACX,oBAAoB,UACpB,MAAM,GACL,oBAAoB,CAkD/B;AAED;IACE;;;OAGG;IACH,eAHW,IAAI,SACJ,MAAM,EAOhB;IAHC,QAAU;IACV,cAAkB;IAClB,kBAA8C;CAEjD;AAqDM,mCAHI,KAAK,SACL,MAAM,4BAgDhB;AAWM,kDAJI,KAAK,CAAC,iBAAiB,CAAC,SACxB,MAAM,OACN,MAAM,QAiChB;AAQM,mCAHI,KAAK,GACJ,KAAK,CAAC,IAAI,CAAC,CAWtB;AAUM,wCAJI,KAAK,eACL,WAAW,SACV,MAAM,CAAC,GAAG,CAAC,QActB;AAED;;;GAGG;AACH,mBAFgC,KAAK,SAAvB,KAAK,CAAC,SAAU;IA2D5B;;;;OAIG;IACH,YAJ+B,EAAE,SAAnB,KAAK,CAAC,SAAU,KACnB,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,GACd,KAAK,CAAC,EAAE,CAAC,CAMpB;IAjED;;OAEG;IACH,mBAFW,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,OAAC,EAqDxC;IAlDC;;OAEG;IACH,MAFU,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAEwB;IAC/D;;OAEG;IACH,OAFU,IAAI,GAAC,IAAI,CAEF;IACjB;;OAEG;IACH,MAFU,GAAG,CAAC,MAAM,EAAC,IAAI,CAAC,CAEL;IACrB;;OAEG;IACH,QAFU,IAAI,GAAC,IAAI,CAED;IAClB;;OAEG;IACH,KAFU,GAAG,GAAC,IAAI,CAEH;IACf,gBAAgB;IAChB;;;OAGG;IACH,KAFU,YAAY,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAC,WAAW,CAAC,CAEhC;IAC/B;;;OAGG;IACH,MAFU,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,EAAC,WAAW,CAAC,CAEjB;IAChC;;OAEG;IACH,eAFU,IAAI,GAAG,KAAK,CAAC,iBAAiB,CAAC,CAEhB;IACzB;;;OAGG;IACH,iBAAqE;IACrE,uBAA8E;IAK9E;;;OAGG;IACH,wBAA2B;IAc7B,qBAGC;IAED;;;OAGG;IACH,cAFU,KAAK,CAAC,YAAY,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAIhD;IAED;;OAEG;IACH,cAFY,KAAK,CAAC,GAAG,CAAC,OAAC,CAItB;IAED;;;;;;;;;OASG;IACH,cAHW,GAAG,QACH,IAAI,GAAC,IAAI,QASnB;IAFG,aAAmB;IAIvB;;OAEG;IACH,SAFY,KAAK,CAAC,KAAK,CAAC,CAMvB;IAED;;;;;;OAMG;IACH,2BAHW,WAAW,cACX,GAAG,CAAC,IAAI,GAAC,MAAM,CAAC,QAY1B;IAED;;;;;;OAMG;IACH,QAJ8E,CAAC,SAAlE,CAAE,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,WAAW,KAAK,IAAK,KAClE,CAAC,GACA,CAAC,CAKZ;IAED;;;;;;OAMG;IACH,YAJwD,CAAC,SAA5C,CAAU,IAAa,EAAb,MAAM,CAAC,KAAK,CAAC,EAAC,IAAW,EAAX,WAAW,KAAE,IAAK,KAC5C,CAAC,GACA,CAAC,CAKZ;IAED;;;;OAIG;IACH,aAFW,CAAC,IAAI,EAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAC,EAAE,EAAC,WAAW,KAAG,IAAI,QAIjE;IAED;;;;OAIG;IACH,iBAFW,CAAS,IAAa,EAAb,MAAM,CAAC,KAAK,CAAC,EAAC,IAAW,EAAX,WAAW,KAAE,IAAI,QAIlD;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,eAdwB,IAAI,SAAf,OAAS,eAEX,0BAA0B,SAElC;QAAsB,aAAa;QACZ,aAAa;QACb,aAAa;QACd,YAAY;QACc,QAAQ;QACpC,IAAI;KACxB,GAAS,IAAI,SAAS,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAkO7F;IAED;;;;;;OAMG;IACH,iBAHW,0BAA0B,GACzB,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAI7B;IAED;;;;;;;OAOG;IACH,qBALW,KAAK,CAAC,QAAQ,OACd,0BAA0B,QA8CpC;IAED;;;;;;OAMG;IACH,SAFY,KAAK,CAAC,KAAK,CAAC,CAMvB;IAED;;OAEG;IACH,mBAMC;IAED;;;;;;OAMG;IACH,iCAJW,MAAM,QAMhB;IAED;;;;;;;;;;;OAWG;IACH,eAToE,GAAG,SAAzD,OAAO,CAAC,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAC,MAAM,CAAE,EAChB,GAAG,SAAxC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAE,iBAEvC,GAAG,kBACH,GAAG,GACF,GAAG,CAOd;IAED;;;;;;;OAOG;IACH,eAL2E,GAAG,SAAhE,OAAO,CAAC,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAC,MAAM,GAAC,MAAM,CAAE,iBAC/D,GAAG,GACF,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAC,SAAS,CAKxD;IAED;;;;;;;OAOG;IACH,8BALW,MAAM,GACL,OAAO,CAMlB;IAED;;;;;;;OAOG;IACH,2BALW,QAAQ,GACP,GAAG,GAAG,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAC,CAMjH;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,cAJW,MAAM,WACN,KAAK,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,WACtE,KAAK,CAAC,oBAAoB,QAIpC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,cALW,MAAM,UACN,MAAM,WACN,KAAK,CAAC,oBAAoB,QAKpC;IAED;;;;;;OAMG;IACH,cAJW,KAAK,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAMhF;IAED;;;;OAIG;IACH,iBAFW,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAIvC;IAED;;;;;OAKG;IACH,cAHW,MAAM,WACN,MAAM,QAIhB;IAED;;;;;OAKG;IACH,WAHW,MAAM,GACL,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAI5C;IAED;;;;;;;OAOG;IACH,cAJW,MAAM,QACN,MAAM,GACL,KAAK,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAInD;IAED;;;;;;OAMG;IACH,WAFY,KAAK,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAkBnF;IAED;;;OAGG;IACH,UAFY;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE;YAAE,CAAC,CAAC,EAAC,MAAM,GAAC,MAAM,GAAG,GAAG,CAAA;SAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;KAAG,CA0BxF;IAED;;;OAGG;IACH,wBAFG;QAAuB,QAAQ;KACjC,UAqBA;IAED;;;;;;;;OAQG;IACH,IALa,CAAC,KACH,CAAC,KAAK,EAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,GAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAC,KAAK,EAAC,MAAM,KAAG,CAAC,GACtF,KAAK,CAAC,CAAC,CAAC,CAKnB;IAED;;;;OAIG;IACH,WAFW,CAAC,KAAK,EAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,GAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAC,KAAK,EAAC,MAAM,KAAG,GAAG,QAInG;IAED;;;;OAIG;IACH,eAFW,CAAC,GAAG,EAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAC,GAAG,EAAC,OAAO,CAAC,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAC,MAAM,CAAC,EAAC,KAAK,EAAC,IAAI,KAAG,GAAG,QAQ5H;IAED;;;;OAIG;IACH,YAFY,gBAAgB,CAAC,OAAO,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,CAIpF;IAED;;;;OAIG;IACH,cAFY,gBAAgB,CAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAIhE;IAED;;;;OAIG;IACH,eAFY,gBAAgB,CAAC,GAAG,CAAC,IAAI,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAE,CAAC,GAAG,CAAC,CAAC,CAIxH;IAED;;;;OAIG;IACH,gBAFY,MAAM,CAIjB;IASD;;;;;;;;;OASG;IACH,gBAFW,eAAe,GAAG,eAAe,QAW3C;IA1BD;;OAEG;IACH,oCAFW,IAAI,WAId;CAsBF;AAOM,uBAJ+C,KAAK,SAA9C,OAAQ,YAAY,EAAE,iBAAkB,UAC1C,KAAK,GACJ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,YAAY,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAElB;AACpD,6CAA6C;AAMtC,gDAHI,WAAW,SACX,KAAK,uCAiBf;AAOM,8BAJI,GAAG,KACH,GAAG,GACF,OAAO,CAEgH;AAyB5H,oCARI,KAAK,CAAC,GAAG,CAAC,SACV,MAAM,OACN,MAAM,GACL,KAAK,CAAC,GAAG,CAAC,CAgCrB;AAYM,kCAPI,KAAK,SACL,MAAM,GACL,GAAG,CAqBd;AAaM,yDARI,WAAW,UACX,KAAK,iBACL,IAAI,OAAC,WACL,KAAK,CAAC,MAAM,CAAC,QA4DvB;AAaM,oDARI,WAAW,UACX,KAAK,SACL,MAAM,WACN,KAAK,CAAC;QAAO,MAAM,GAAC,GAAG;CAAC,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM,GAAC,IAAI,GAAC,MAAM,GAAC,UAAU,CAAC,QA4C5E;AAaM,kDAPI,WAAW,UACX,KAAK,WACL,KAAK,CAAC;QAAO,MAAM,GAAC,GAAG;CAAC,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM,GAAC,IAAI,GAAC,MAAM,GAAC,UAAU,CAAC,QAe5E;AAWM,4CARI,WAAW,UACX,KAAK,SACL,MAAM,UACN,MAAM,QAyChB;AAYM,2CAPI,WAAW,UACX,KAAK,OACL,MAAM,QAUhB;AAWM,wCARI,WAAW,UACX,KAAK,OACL,MAAM,SACN,MAAM,QAqChB;AAUM,mCAPI,KAAK,CAAC,GAAG,CAAC,OACV,MAAM,GACL;QAAO,MAAM,GAAC,GAAG;CAAC,GAAC,MAAM,GAAC,IAAI,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM,GAAC,UAAU,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,SAAS,CAS3F;AASM,sCANI,KAAK,CAAC,GAAG,CAAC;;;;EAkBpB;AA0BM,gCAf8B,SAAS,SAAhC,KAAK,CAAC,eAAgB,KACzB,SAAS,UACT,KAAK,iBACL,GAAG,CAAC,MAAM,GAAC,IAAI,CAAC,OAAC,MACjB,0BAA0B,QAC1B,OAAO,aACP,GAAG,CAAC,KAAK,CAAC,GAAC,GAAG,CAAC,KAAK,EAAC,GAAG,CAAC,GAAC,IAAI,iBAC9B,KAAK,OAAC,kBACN,KAAK,OAAC,SACN,GAAG,YACH,GAAG,QA0Db;AAUM,mCAPI,KAAK,CAAC,GAAG,CAAC,OACV,MAAM,GACL,OAAO,CASlB;AASM,gCANI,IAAI,YACJ,QAAQ,GAAC,SAAS,WAO+F;AAWrH,2CARI,KAAK,CAAC,GAAG,CAAC,OACV,MAAM,YACN,QAAQ,GACP;QAAO,MAAM,GAAC,GAAG;CAAC,GAAC,MAAM,GAAC,IAAI,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM,GAAC,UAAU,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,SAAS,CAW3F;AAUM,8CAPI,KAAK,CAAC,GAAG,CAAC,YACV,QAAQ;;;;EAwBlB;AASM,wCANI,KAAK,CAAC,GAAG,CAAC,GAAG;IAAE,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;CAAE,GACvC,gBAAgB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAQvC;AAQM,yCAHI,eAAe,GAAG,eAAe,GAChC,WAAW,CAEsD;AAQtE,2CAHI,eAAe,GAAG,eAAe,GAChC,aAAa,CAE0D;AAQ5E,yCAHI,eAAe,GAAG,eAAe,GAChC,WAAW,CActB;AAMM,2CAHI,eAAe,GAAG,eAAe,GAChC,aAAa,CAE2E;AAQ7F,0CAHI,eAAe,GAAG,eAAe,GAChC,YAAY,CAEuD;AAQxE,wCAHI,eAAe,GAAG,eAAe,GAChC,UAAU,CAE0E;AAMzF,wCAHI,eAAe,GAAG,eAAe,GAChC,UAAU,CASrB;AAMM,2CAHI,eAAe,GAAG,eAAe,GAChC,aAAa,CAEuD;AAQzE,4CAHI,eAAe,GAAG,eAAe,GAChC,cAAc,CAEwD;AAElF;;;;GAIG;AACH,0BAFU,KAAK,CAAC,CAAS,IAAiC,EAAjC,eAAe,GAAG,eAAe,KAAE,eAAe,CAAC,CAc3E;AAMM,yCAHI,eAAe,GAAG,eAAe,QACjC,MAAM,+CAE0E;AASpF,mCANI,eAAe,GAAG,eAAe,GAChC,KAAK,CAUhB;qBAnlEY;QAAO,MAAM,GAAC,GAAG;CAAC,GAAC,KAAK,CAAC,GAAG,CAAC,GAAC,MAAM,GAAC,IAAI,GAAC,MAAM,GAAC,UAAU,YAAQ,KAAK,CAAC,GAAG,CAAC;kCA48C3D,KAAK,SAAtB,KAAK,CAAC,SAAU,IACjB,KAAK,CAAC,kBAAkB,CAAC,KAAK,EAAE;IACtC,KAAK,EAAE,GAAG,CAAC,IAAI,MAAM,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAE,CAAC;IACxG,QAAQ,EAAE,YAAY,CAAC,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAA;CAC1D,CAAC;yBAKY,IAAI,oBACV,OAAO,CAAC,IAAI,EAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,SAAS,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;qBAj+C7J,mBAAmB;uBAOH,mBAAmB;uBAhCnB,YAAY;wBASX,aAAa;mBADlB,aAAa;4BAiBzB,mBAAmB;8BAAnB,mBAAmB;4BAAnB,mBAAmB;8BAAnB,mBAAmB;6BAAnB,mBAAmB;2BAAnB,mBAAmB;2BAAnB,mBAAmB;8BAAnB,mBAAmB;+BAAnB,mBAAmB"} -\ No newline at end of file -diff --git a/src/structs/Item.js b/src/structs/Item.js -index d3fb68a6086cab497099f265f95100258224db1b..5c4e621eb5e0341e002688b9e30927a7c2979185 100644 ---- a/src/structs/Item.js -+++ b/src/structs/Item.js -@@ -259,7 +259,7 @@ export class Item extends AbstractStruct { - // set as current parent value if right === null and this is parentSub - /** @type {YType} */ (this.parent)._map.set(this.parentSub, this) - if (this.left !== null) { -- // this is the current attribute value of parent. delete right -+ // this is the current attribute value of parent. delete the previous value - this.left.delete(transaction) - } - } -diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js -index 5b94f87efb372d97fb8e943f607c585433dfbabb..27af08ca781bcdebc39206df93055e6a759a6c4f 100644 ---- a/src/utils/UndoManager.js -+++ b/src/utils/UndoManager.js -@@ -92,7 +92,7 @@ const popStackItem = (undoManager, stack, eventType) => { - } - }) - itemsToRedo.forEach(struct => { -- performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.inserts, undoManager.ignoreRemoteMapChanges, undoManager) !== null || performedChange -+ performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.inserts, undoManager.ignoreRemoteAttributeChanges, undoManager) !== null || performedChange - }) - // We want to delete in reverse order so that children are deleted before - // parents, so we have more information available when items are filtered. -@@ -131,7 +131,7 @@ const popStackItem = (undoManager, stack, eventType) => { - * filter returns false, the type/item won't be deleted even it is in the - * undo/redo scope. - * @property {Set} [UndoManagerOptions.trackedOrigins=new Set([null])] -- * @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..). -+ * @property {boolean} [ignoreRemoteAttributeChanges] By default, the UndoManager will never overwrite remote changes. In some cases this might be the expected behavior. This property enables overwriting remote changes on attribute changes. (previously named `ignoreRemoteMapChanges`) - * @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty. - */ - -@@ -162,7 +162,7 @@ export class UndoManager extends ObservableV2 { - captureTransaction = _tr => true, - deleteFilter = () => true, - trackedOrigins = new Set([null]), -- ignoreRemoteMapChanges = false, -+ ignoreRemoteAttributeChanges = false, - doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope instanceof Doc ? typeScope : typeScope.doc) - } = {}) { - super() -@@ -198,7 +198,7 @@ export class UndoManager extends ObservableV2 { - */ - this.currStackItem = null - this.lastChange = 0 -- this.ignoreRemoteMapChanges = ignoreRemoteMapChanges -+ this.ignoreRemoteAttributeChanges = ignoreRemoteAttributeChanges - this.captureTimeout = captureTimeout - /** - * @param {Transaction} transaction -@@ -415,14 +415,14 @@ const isDeletedByUndoStack = (stack, id) => array.some(stack, /** @param {StackI - * @param {Item} item - * @param {Set} redoitems - * @param {IdSet} itemsToDelete -- * @param {boolean} ignoreRemoteMapChanges -+ * @param {boolean} ignoreRemoteAttributeChanges - * @param {import('../utils/UndoManager.js').UndoManager} um - * - * @return {Item|null} - * - * @private - */ --export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) => { -+export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteAttributeChanges, um) => { - const doc = transaction.doc - const store = doc.store - const ownClientID = doc.clientID -@@ -442,7 +442,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo - // make sure that parent is redone - if (parentItem !== null && parentItem.deleted === true) { - // try to undo parent if it will be undone anyway -- if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) === null)) { -+ if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteAttributeChanges, um) === null)) { - return null - } - while (parentItem.redone !== null) { -@@ -491,7 +491,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo - } - } else { - right = null -- if (item.right && !ignoreRemoteMapChanges) { -+ if (item.right && !ignoreRemoteAttributeChanges) { - left = item - // Iterate right while right is in itemsToDelete - // If it is intended to delete right while item is redone, we can expect that item should replace right. -@@ -508,6 +508,9 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo - } else { - left = parentType._map.get(item.parentSub) || null - } -+ if (left !== null && /** @type {YType} */ (left.parent)._item !== parentItem) { -+ left = parentType._map.get(item.parentSub) || null -+ } - } - const nextClock = store.getClock(ownClientID) - const nextId = createID(ownClientID, nextClock) -diff --git a/src/ytype.js b/src/ytype.js -index ab79c2fe90d8b3c74b1c1ce6d7f62f714605f33c..bad51b3c1b80f8849eede060d412aa7d70bd6d3f 100644 ---- a/src/ytype.js -+++ b/src/ytype.js -@@ -1926,7 +1926,19 @@ export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, de - let c = array.last(content.getContent()) - if (deleted) { - if (itemsToRender == null || itemsToRender.hasId(item.lastId)) { -- d.deleteAttr(key, attribution, c) -+ if (attribution != null) { -+ // Item surfaced under attribution (suggestion view / diff AM, -+ // either in snapshot mode or in an event-driven render). The -+ // attribute is still observable in the rendered state, so emit -+ // a positive `SetAttrOp` carrying the attribution metadata - -+ // matching how content children are rendered for the same case -+ // (positive `InsertOp` with attribution, never `DeleteOp`). -+ d.setAttr(key, c, attribution) -+ } else { -+ // Hard-deleted attribute (no AM-surfaced attribution): emit the -+ // change op so event consumers can apply it. -+ d.deleteAttr(key, attribution, c) -+ } - } - } else if (deep && c instanceof YType && modified?.has(c)) { - d.modifyAttr(key, c.toDelta(am, opts)) diff --git a/patches/lib0@1.0.0-rc.13.patch b/patches/lib0@1.0.0-rc.13.patch deleted file mode 100644 index 193dad31ed..0000000000 --- a/patches/lib0@1.0.0-rc.13.patch +++ /dev/null @@ -1,466 +0,0 @@ -diff --git a/dist/delta/delta.d.ts b/dist/delta/delta.d.ts -index 4b3d23babb76883d7a66c10ab9f170436484fcb2..1f97a3da5707f7602a72f8d15c6158c61321d949 100644 ---- a/dist/delta/delta.d.ts -+++ b/dist/delta/delta.d.ts -@@ -42,6 +42,22 @@ export const $attribution: s.Schema; - * @type {s.Schema} - */ - export const $deltaMapChangeJson: s.Schema; -+/** -+ * Invariants shared by all op classes below (TextOp, InsertOp, DeleteOp, -+ * RetainOp, ModifyOp, SetAttrOp, DeleteAttrOp, ModifyAttrOp): -+ * -+ * - **Only code inside `delta.js` may mutate op fields.** External consumers -+ * treat ops as immutable; structural fields are JSDoc-annotated `@readonly` -+ * to reinforce this. Mutation is permitted only while the owning Delta is -+ * not `done` — every builder entry point routes through `modDeltaCheck` -+ * to enforce this at runtime. -+ * - **Any mutation of a fingerprinted field MUST null `_fingerprint`.** The -+ * fingerprint is a lazy cache; if it has already been computed and the -+ * underlying data changes without invalidating it, every subsequent -+ * fingerprint read (and any `diff` / equality check that relies on it) is -+ * wrong. Fields covered: insert, delete, retain, format, attribution, -+ * value, key. -+ */ - export class TextOp extends list.ListNode { - /** - * @param {string} insert -@@ -125,9 +141,9 @@ export class InsertOp extends list.ListNode { - */ - _fingerprint: string | null; - /** -- * @param {ArrayContent} newVal -+ * @param {ArrayContent} _newVal - */ -- _updateInsert(newVal: ArrayContent): void; -+ _updateInsert(_newVal: ArrayContent): void; - /** - * @return {'insert'} - */ -@@ -184,10 +200,10 @@ export class DeleteOp extends list.ListNode { - /** - * Remove a part of the operation (similar to Array.splice) - * -- * @param {number} _offset -+ * @param {number} offset - * @param {number} len - */ -- _splice(_offset: number, len: number): this; -+ _splice(offset: number, len: number): this; - /** - * @return {DeltaListOpJSON} - */ -@@ -666,10 +682,12 @@ export class DeltaBuilder?} other -- * @param {{ final?: boolean }} opts -- experimental -+ * @param {{ final?: boolean }} opts -- (experimental) - * @return {DeltaBuilder} - */ - apply(other: Delta | null, { final }?: { -diff --git a/src/bin/0serve.js b/src/bin/0serve.js -index a69d09ba2effab926c2b0b24a147ad744064fe78..16cb9427c6ef666000b4463aa8734505c39e7563 100755 ---- a/src/bin/0serve.js -+++ b/src/bin/0serve.js -@@ -89,9 +89,17 @@ const server = http.createServer((req, res) => { - server.listen(port, host, () => { - logging.print(logging.BOLD, logging.ORANGE, `Server is running on http://${host}:${port}`) - if (paramOpenFile) { -- const start = debugBrowser || (process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open') -+ const url = `http://${host}:${port}/${paramOpenFile}` - import('child_process').then(cp => { -- cp.exec(`${start} http://${host}:${port}/${paramOpenFile}`) -+ if (debugBrowser) { -+ cp.execFile(debugBrowser, [url]) -+ } else if (process.platform === 'darwin') { -+ cp.execFile('open', [url]) -+ } else if (process.platform === 'win32') { -+ cp.execFile('cmd', ['/c', 'start', '', url]) -+ } else { -+ cp.execFile('xdg-open', [url]) -+ } - }) - } - }) -diff --git a/src/delta/delta.js b/src/delta/delta.js -index e063729e4515dd76aecd488655b26fec82a22ca9..d4be86c6af7aef2f0c51c32f2023c24152770c22 100644 ---- a/src/delta/delta.js -+++ b/src/delta/delta.js -@@ -101,6 +101,22 @@ const _cloneAttrs = attrs => attrs == null ? attrs : { ...attrs } - */ - const _markMaybeDeltaAsDone = maybeDelta => $deltaAny.check(maybeDelta) ? /** @type {MaybeDelta} */ (maybeDelta.done()) : maybeDelta - -+/** -+ * Invariants shared by all op classes below (TextOp, InsertOp, DeleteOp, -+ * RetainOp, ModifyOp, SetAttrOp, DeleteAttrOp, ModifyAttrOp): -+ * -+ * - **Only code inside `delta.js` may mutate op fields.** External consumers -+ * treat ops as immutable; structural fields are JSDoc-annotated `@readonly` -+ * to reinforce this. Mutation is permitted only while the owning Delta is -+ * not `done` — every builder entry point routes through `modDeltaCheck` -+ * to enforce this at runtime. -+ * - **Any mutation of a fingerprinted field MUST null `_fingerprint`.** The -+ * fingerprint is a lazy cache; if it has already been computed and the -+ * underlying data changes without invalidating it, every subsequent -+ * fingerprint read (and any `diff` / equality check that relies on it) is -+ * wrong. Fields covered: insert, delete, retain, format, attribution, -+ * value, key. -+ */ - export class TextOp extends list.ListNode { - /** - * @param {string} insert -@@ -228,14 +244,17 @@ export class InsertOp extends list.ListNode { - this._fingerprint = null - } - -+ /* c8 ignore start */ - /** -- * @param {ArrayContent} newVal -+ * @param {ArrayContent} _newVal - */ -- _updateInsert (newVal) { -- // @ts-ignore -- this.insert = newVal -- this._fingerprint = null -+ _updateInsert (_newVal) { -+ // Mirror of TextOp._updateInsert; not currently called on InsertOp because -+ // adjacent inserts are merged in-place via `end.insert.push(...)`. Kept for -+ // parity with TextOp's API. -+ error.unexpectedCase() // throw if called - } -+ /* c8 ignore stop */ - - /** - * @return {'insert'} -@@ -357,11 +376,13 @@ export class DeleteOp extends list.ListNode { - /** - * Remove a part of the operation (similar to Array.splice) - * -- * @param {number} _offset -+ * @param {number} offset - * @param {number} len - */ -- _splice (_offset, len) { -- this.prevValue = /** @type {any} */ (this.prevValue ? slice(this.prevValue, _offset, len) : null) -+ _splice (offset, len) { -+ if (this.prevValue) { -+ /** @type {DeltaBuilder} */ (this.prevValue).apply(create().retain(offset).delete(len)) -+ } - this._fingerprint = null - this.delete -= len - return this -@@ -547,6 +568,9 @@ export class ModifyOp extends list.ListNode { - }))) - } - -+ /* c8 ignore start */ -+ // ModifyOp has length 1, so callers never pass offset>0 or len>0 — splitHere -+ // is a no-op for length-1 ops. Kept for the structural _splice contract. - /** - * Remove a part of the operation (similar to Array.splice) - * -@@ -556,6 +580,7 @@ export class ModifyOp extends list.ListNode { - _splice (_offset, _len) { - return this - } -+ /* c8 ignore stop */ - - /** - * @return {DeltaListOpJSON} -@@ -851,7 +876,7 @@ export const $setAttrOpWith = $content => s.$custom(o => $setAttrOp.check(o) && - * @param {s.Schema} $content - * @return {s.Schema>} - */ --export const $insertOpWith = $content => s.$custom(o => $insertOp.check(o) && $content.check(o.insert.every(ins => $content.check(ins)))) -+export const $insertOpWith = $content => s.$custom(o => $insertOp.check(o) && o.insert.every(ins => $content.check(ins))) - - /** - * @template {DeltaAny} Modify -@@ -1231,9 +1256,13 @@ const tryMergeWithPrev = (parent, op) => { - /** @type {DeleteOp} */ (prevOp).delete += op.delete - } else if ($textOp.check(op)) { - /** @type {TextOp} */ (prevOp)._updateInsert(/** @type {TextOp} */ (prevOp).insert + op.insert) -+ /* c8 ignore start */ - } else { -+ // unreachable: the constructor check at the top of the function already -+ // limits `op` to one of the four kinds tested above - error.unexpectedCase() - } -+ /* c8 ignore stop */ - list.remove(parent, op) - } - -@@ -1501,10 +1530,12 @@ export class DeltaBuilder extends Delta { - * - * a.apply(b).apply(c) - * -- * @todo fuzz test the above property -+ * If `final = true`, we consider this delta the final state and drop deleteAttrOps from -+ * attributes. (E.g. if `otherOp` deletes an attribute, this op will simply not have the -+ * attribute). Any kind of `delete` op might be considered a bug. A final delta is not idempotent. - * - * @param {Delta?} other -- * @param {{ final?: boolean }} opts -- experimental -+ * @param {{ final?: boolean }} opts -- (experimental) - * @return {DeltaBuilder} - */ - apply (other, { final = this.isFinal } = {}) { -@@ -1517,7 +1548,7 @@ export class DeltaBuilder extends Delta { - const c = /** @type {SetAttrOp|DeleteAttrOp|ModifyAttrOp} */ (this.attrs[op.key]) - if ($modifyAttrOp.check(op)) { - if ($deltaAny.check(c?.value)) { -- c._modValue.apply(op.value) -+ c._modValue.apply(op.value, { final }) - } else { - // then this is a simple modify - // @ts-ignore -@@ -1588,10 +1619,14 @@ export class DeltaBuilder extends Delta { - this.childCnt += op.length - } - for (const op of other.children) { -+ // defensive: the per-branch logic below resets opsI/offset whenever it -+ // consumes an op exactly. This guard catches any path that forgets to. -+ /* c8 ignore start */ - if (opsI?.length === offset) { - opsI = opNextUndeleted(opsI) - offset = 0 - } -+ /* c8 ignore stop */ - if ($textOp.check(op) || $insertOp.check(op)) { - insertClonedOp(op) - } else if ($retainOp.check(op)) { -@@ -1611,7 +1646,11 @@ export class DeltaBuilder extends Delta { - } - if (opsI != null) { - if (op.format != null && retainLen > 0) { -- offset = retainLen -+ // accumulate onto the existing offset — the else-branch below uses -+ // `offset += retainLen`, and we must agree with it when prior -+ // iterations have advanced offset into opsI without splitting (e.g. -+ // a format-less retain followed by a same-format retain). -+ offset += retainLen - splitHere() - updateOpFormat(/** @type {ChildrenOpAny} */ (opsI.prev), op.format) - scheduleForMerge(opsI.prev) -@@ -1670,9 +1709,12 @@ export class DeltaBuilder extends Delta { - opsI._splice(offset, delLen) - } - remainingLen -= delLen -+ /* c8 ignore start */ - } else { -+ // unreachable: opsI was already typed as retain | non-delete-content | delete above - error.unexpectedCase() - } -+ /* c8 ignore stop */ - } - } else if ($modifyOp.check(op)) { - if (opsI != null && op.format != null && (!$deleteOp.check(opsI) && !$retainOp.check(opsI))) { // retain handles splitting seperately, without copying attrs -@@ -1709,14 +1751,20 @@ export class DeltaBuilder extends Delta { - opsI._splice(0, 1) - scheduleForMerge(opsI) - } -- } else if ($deleteOp.check(opsI)) { -- // nop -+ /* c8 ignore start */ - } else { -+ // remaining branches: opsI is deleteOp or something unknown -+ // both branches are unreachable today: opNextUndeleted skips -+ // delete ops, so opsI is never a delete during iteration; and the four -+ // branches above exhaust the other op kinds. The deleteOp branch is -+ // kept as a defensive no-op (drops a modify that lands in a deleted -+ // region) rather than a throw. - error.unexpectedCase() - } - } else { - error.unexpectedCase() - } -+ /* c8 ignore stop */ - } - // iterate backwards, to ensure that we merge all content - for (let i = maybeMergeable.length - 1; i >= 0; i--) { -@@ -1772,9 +1820,12 @@ export class DeltaBuilder extends Delta { - // @ts-ignore - delete this.attrs[otherOp.key] - } -+ /* c8 ignore start */ - } else { -+ // unreachable: attr ops are exhaustively setAttr | deleteAttr | modifyAttr - error.unexpectedCase() - } -+ /* c8 ignore stop */ - } - /** - * Rebase children. -@@ -1831,7 +1882,10 @@ export class DeltaBuilder extends Delta { - otherOffset = otherChild.length - } else { - if ($modifyOp.check(otherChild)) { -- /** @type {any} */ (currChild.value).rebase(otherChild, priority) -+ // _modValue (not .value) — ModifyOp.clone() marks its inner delta -+ // as `done`, so a cloned ModifyOp can only be rebased after the -+ // _modValue getter lazy-clones it back to mutable. -+ currChild._modValue.rebase(otherChild.value, priority) - } else if ($deleteOp.check(otherChild)) { - list.remove(this.children, currChild) - this.childCnt -= 1 -@@ -1848,21 +1902,70 @@ export class DeltaBuilder extends Delta { - * - insert: split curr op and insert retain - */ - if ($retainOp.check(otherChild) || $modifyOp.check(otherChild)) { -+ // Format reconciliation. priority=true is a no-op (currChild's format -+ // wins). For !priority, currChild concedes any format key that -+ // otherChild also writes — but only over the [currOffset..currOffset+ -+ // maxCommonLen] overlap. Split currChild around the overlap so the -+ // prefix/suffix keep their original format and only the middle piece -+ // carries the stripped format. -+ if ( -+ !priority && -+ $retainOp.check(currChild) && -+ currChild.format != null && -+ otherChild.format != null -+ ) { -+ /** @type {FormattingAttributes} */ -+ const stripped = {} -+ let strippedAny = false -+ for (const k in currChild.format) { -+ if (k in otherChild.format) { -+ strippedAny = true -+ } else { -+ stripped[k] = currChild.format[k] -+ } -+ } -+ if (strippedAny) { -+ // split off the suffix [currOffset+maxCommonLen..length] if any -+ if (currOffset + maxCommonLen < currChild.length) { -+ const suffix = currChild.clone(currOffset + maxCommonLen, currChild.length) -+ list.insertBetween(this.children, currChild, currChild.next, suffix) -+ currChild._splice(currOffset + maxCommonLen, currChild.length - (currOffset + maxCommonLen)) -+ } -+ // split off the prefix [0..currOffset] if any -+ if (currOffset > 0) { -+ const prefix = currChild.clone(0, currOffset) -+ list.insertBetween(this.children, currChild.prev, currChild, prefix) -+ currChild._splice(0, currOffset) -+ currOffset = 0 -+ } -+ // currChild now spans exactly the overlap. Replace its format. -+ /** @type {any} */ (currChild).format = object.isEmpty(stripped) ? null : stripped -+ currChild._fingerprint = null -+ } -+ } - currOffset += maxCommonLen - otherOffset += maxCommonLen - } else if ($deleteOp.check(otherChild)) { - if ($retainOp.check(currChild)) { - // @ts-ignore - currChild.retain -= maxCommonLen -+ currChild._fingerprint = null - } else if ($deleteOp.check(currChild)) { - currChild.delete -= maxCommonLen -+ currChild._fingerprint = null - } - this.childCnt -= maxCommonLen -- } else { // insert/text.check(currOp) -+ // advance other so subsequent currChild ops see what comes AFTER this -+ // delete; without this we'd loop against the same delete forever and -+ // never reach other's later inserts. -+ otherOffset += maxCommonLen -+ } else { // insert/text.check(otherChild) - if (currOffset > 0) { -- const leftPart = currChild.clone(currOffset) -+ const leftPart = currChild.clone(0, currOffset) - list.insertBetween(this.children, currChild.prev, currChild, leftPart) -- currChild._splice(currOffset, currChild.length - currOffset) -+ // leftPart is the prefix; currChild becomes the suffix. Remove the -+ // prefix portion from currChild so it represents [currOffset..length]. -+ currChild._splice(0, currOffset) - currOffset = 0 - } - list.insertBetween(this.children, currChild.prev, currChild, new RetainOp(otherChild.length, null, null)) -@@ -2000,8 +2103,10 @@ export class $Delta extends s.Schema { - check (o, err = undefined) { - const { $name, $attrs, $children, hasText, $formats } = this.shape - if (!$deltaAny.check(o, err)) { -+ /* c8 ignore next */ - err?.extend(null, 'Delta', o?.constructor.name, 'Constructor match failed') - } else if (o.name != null && !$name.check(o.name, err)) { -+ /* c8 ignore next */ - err?.extend('Delta.name', $name.toString(), o.name, 'Unexpected node name') - } else if (list.toArray(o.children).some(c => (!hasText && $textOp.check(c)) || (hasText && $textOp.check(c) && c.format != null && !$formats.check(c.format)) || ($insertOp.check(c) && !c.insert.every(ins => $children.check(ins))))) { - err?.extend('Delta.children', '', '', 'Children don\'t match the schema') -@@ -2097,7 +2202,7 @@ export const mergeDeltas = (a, b) => { - c.apply(b) - return /** @type {any} */ (c) - } -- return a == null ? b : (a || null) -+ return /** @type {D} */ (a || b || null) - } - - /** -@@ -2325,6 +2430,16 @@ class _DiffStringWrapper { - */ - - /** -+ * Compute a delta that, when applied to `d1`, produces `d2`. Only the children and attributes of -+ * `d1` and `d2` are compared; the top-level node names of `d1` and `d2` are *not*. Diffing -+ * `
a
` against `a` is valid and yields an empty diff — they have the same -+ * children and attributes, so as far as `diff` is concerned they are equal at the level it cares -+ * about. The top-level name is treated as a document-type marker, not as diffable content. -+ * -+ * Names *are* compared on children: a child node whose name changes between `d1` and `d2` is -+ * replaced wholesale (delete + insert), not converted into a `modify` op. Same-name child nodes -+ * at aligned positions are paired and recursed into via `modify`. -+ * - * @template {DeltaConf} Conf - * @param {Delta} d1 - * @param {NoInfer>} d2 -@@ -2395,9 +2510,14 @@ export const diff = (d1, d2) => { - cs2.push(left2.insert) - } else if ($insertOp.check(left2)) { - cs2.push(...left2.insert.map(ins => typeof ins === 'string' ? new _DiffStringWrapper(ins) : ins)) -+ /* c8 ignore start */ - } else { -+ // unreachable for valid diff inputs (delete on the rhs would already -+ // have been rejected via the `[lib0/delta] diffing deletes unsupported` -+ // path above) - error.unexpectedCase() - } -+ /* c8 ignore stop */ - formattingNeedsDiff ||= left2.format != null - left2 = left2.next - } -@@ -2459,9 +2579,14 @@ export const diff = (d1, d2) => { - a = a.next - aOffset = 0 - } -+ /* c8 ignore start */ - } else { -+ // unreachable: by this point both a and b are insert/text (deletes -+ // were rejected upstream and `originalUpdated` is the result of an -+ // apply, which keeps inserts only). - error.unexpectedCase() - } -+ /* c8 ignore stop */ - } - // @todo instead of applying, we want to first exec d, then formattingDiff - we need a merge - // function! -@@ -2481,10 +2606,11 @@ export const diff = (d1, d2) => { - } else { - d.setAttr(key, nextVal) - } -+ /* c8 ignore start */ - } else { -- /* c8 ignore next 2 */ - error.unexpectedCase() - } -+ /* c8 ignore stop */ - } - } - for (const { key } of d1.attrs) { diff --git a/patches/lib0@1.0.0-rc.14.patch b/patches/lib0@1.0.0-rc.14.patch new file mode 100644 index 0000000000..becd073304 --- /dev/null +++ b/patches/lib0@1.0.0-rc.14.patch @@ -0,0 +1,79 @@ +diff --git a/dist/delta/delta.d.ts b/dist/delta/delta.d.ts +index 1f97a3d..b6f9db5 100644 +--- a/dist/delta/delta.d.ts ++++ b/dist/delta/delta.d.ts +@@ -905,7 +905,7 @@ export function from> extends Array ? (unknown extends Ac ? never : Ac) : never; + text: Extract extends never ? false : true; + }>; +-export function diff(d1: Delta, d2: NoInfer>): Delta; ++export function diff(d1: Delta, d2: NoInfer>, matchNodes?: (a: any, b: any) => boolean): Delta; + export function diffChangesetWithSeparator(changeset: Array<{ + index: number; + remove: Array; +diff --git a/src/delta/delta.js b/src/delta/delta.js +index d4be86c..89ddc86 100644 +--- a/src/delta/delta.js ++++ b/src/delta/delta.js +@@ -2443,9 +2443,14 @@ class _DiffStringWrapper { + * @template {DeltaConf} Conf + * @param {Delta} d1 + * @param {NoInfer>} d2 ++ * @param {(a: DeltaAny, b: DeltaAny) => boolean} [matchNodes] decides whether a ++ * removed node and an inserted node are the *same* node (→ diffed/modified in ++ * place) or different (→ replaced). Defaults to name-equality. Override to force ++ * a *replace* for nodes that should be treated as atomic - e.g. raising a diff ++ * boundary when a node's identifying child changes type. + * @return {Delta} + */ +-export const diff = (d1, d2) => { ++export const diff = (d1, d2, matchNodes = (a, b) => a.name === b.name) => { + const d = create(d1.name === d2.name ? d1.name : null, $deltaAny) + if (d1.fingerprint !== d2.fingerprint) { + /** +@@ -2533,7 +2538,7 @@ export const diff = (d1, d2) => { + const changeset3 = diffChangesetWithSeparator(changeset2, patience.smartSplitRegex) + // split all + const changeset4 = diffChangesetWithSeparator(changeset3, /./g) +- applyChangesetToDelta(d, changeset4) ++ applyChangesetToDelta(d, changeset4, matchNodes) + if (formattingNeedsDiff) { + const formattingDiff = create() + // update opsIs with content diff. then we can figure out the formatting diff. +@@ -2602,7 +2607,7 @@ export const diff = (d1, d2) => { + const prevVal = attr1?.value + const nextVal = attr2.value + if ($deltaAny.check(prevVal) && $deltaAny.check(nextVal) && prevVal.name === nextVal.name) { +- d.modifyAttr(key, diff(prevVal, nextVal)) ++ d.modifyAttr(key, diff(prevVal, nextVal, matchNodes)) + } else { + d.setAttr(key, nextVal) + } +@@ -2648,8 +2653,9 @@ const applyInserts = (d, cins, len) => { len > 0 && cins.splice(0, len).forEach( + /** + * @param {DeltaBuilderAny} d + * @param {Array<{ index: number, remove: Array, insert: Array }>} changeset ++ * @param {(a: DeltaAny, b: DeltaAny) => boolean} [matchNodes] see {@link diff} + */ +-const applyChangesetToDelta = (d, changeset) => { ++const applyChangesetToDelta = (d, changeset, matchNodes = (a, b) => a.name === b.name) => { + for (let ci = 0, lastIndex = 0; ci < changeset.length; ci++) { + const c = changeset[ci] + d.retain(c.index - lastIndex) +@@ -2660,14 +2666,14 @@ const applyChangesetToDelta = (d, changeset) => { + const cremoveDeltaIndex = c.remove.findIndex(cc => $deltaAny.check(cc)) + if (cremoveDeltaIndex < 0) break + const cremoveDelta = c.remove[cremoveDeltaIndex] +- const cinsertDeltaIndex = c.insert.findIndex(cc => $deltaAny.check(cc) && cc.name === cremoveDelta.name) ++ const cinsertDeltaIndex = c.insert.findIndex(cc => $deltaAny.check(cc) && matchNodes(cremoveDelta, cc)) + if (cinsertDeltaIndex < 0) { + applyRemoves(d, c.remove, cremoveDeltaIndex + 1) + continue + } + applyRemoves(d, c.remove, cremoveDeltaIndex) + applyInserts(d, c.insert, cinsertDeltaIndex) +- d.modify(diff(c.remove[0], c.insert[0])) ++ d.modify(diff(c.remove[0], c.insert[0], matchNodes)) + c.remove.splice(0, 1) + c.insert.splice(0, 1) + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b940198106..30e3473c2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,12 +10,11 @@ overrides: '@tiptap/pm': ^3.0.0 vitest: 4.1.7 '@vitest/runner': 4.1.7 - '@y/prosemirror>lib0': 1.0.0-rc.13 + lib0: 1.0.0-rc.14 patchedDependencies: - '@y/prosemirror@2.0.0-2': 802334aa5d2410b9fc999d5fb5ffef01650a04e58b59fca24e142c55b3379728 - '@y/y@14.0.0-rc.16': 4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9 - lib0@1.0.0-rc.13: 328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e + '@y/prosemirror@2.0.0-2': 168146d45476a6e2019bd9b140bf068fd17c12fb6b7d282509552826e74740dc + lib0@1.0.0-rc.14: e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20 importers: @@ -236,16 +235,16 @@ importers: version: 0.6.4(react@19.2.5)(yjs@13.6.30) '@y/prosemirror': specifier: ^2.0.0-2 - version: 2.0.0-2(patch_hash=802334aa5d2410b9fc999d5fb5ffef01650a04e58b59fca24e142c55b3379728)(@y/protocols@1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)))(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9))(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8) + version: 2.0.0-2(patch_hash=168146d45476a6e2019bd9b140bf068fd17c12fb6b7d282509552826e74740dc)(@y/protocols@1.0.6-rc.1(@y/y@14.0.0-rc.17))(@y/y@14.0.0-rc.17)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8) '@y/protocols': specifier: ^1.0.6-rc.1 - version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) + version: 1.0.6-rc.1(@y/y@14.0.0-rc.17) '@y/websocket': specifier: ^4.0.0-3 - version: 4.0.0-rc.2(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) + version: 4.0.0-rc.2(@y/y@14.0.0-rc.17) '@y/y': - specifier: ^14.0.0-rc.16 - version: 14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9) + specifier: ^14.0.0-rc.17 + version: 14.0.0-rc.17 ai: specifier: ^6.0.5 version: 6.0.5(zod@4.3.6) @@ -277,8 +276,8 @@ importers: specifier: npm:@fumadocs/base-ui@16.5.0 version: '@fumadocs/base-ui@16.5.0(@types/react@19.2.14)(fumadocs-core@16.5.0(@types/react@19.2.14)(lucide-react@0.562.0(react@19.2.5))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.2)' lib0: - specifier: 1.0.0-rc.13 - version: 1.0.0-rc.13(patch_hash=328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e) + specifier: 1.0.0-rc.14 + version: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) lucide-react: specifier: ^0.562.0 version: 0.562.0(react@19.2.5) @@ -4043,16 +4042,16 @@ importers: version: 9.1.1(react@19.2.5) '@y/protocols': specifier: ^1.0.6-rc.1 - version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) + version: 1.0.6-rc.1(@y/y@14.0.0-rc.17) '@y/websocket': specifier: ^4.0.0-3 - version: 4.0.0-rc.2(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) + version: 4.0.0-rc.2(@y/y@14.0.0-rc.17) '@y/y': - specifier: ^14.0.0-rc.16 - version: 14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9) + specifier: ^14.0.0-rc.17 + version: 14.0.0-rc.17 lib0: - specifier: 1.0.0-rc.13 - version: 1.0.0-rc.13(patch_hash=328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e) + specifier: 1.0.0-rc.14 + version: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) react: specifier: ^19.2.3 version: 19.2.5 @@ -4101,16 +4100,16 @@ importers: version: 9.1.1(react@19.2.5) '@y/prosemirror': specifier: ^2.0.0-2 - version: 2.0.0-2(patch_hash=802334aa5d2410b9fc999d5fb5ffef01650a04e58b59fca24e142c55b3379728)(@y/protocols@1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)))(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9))(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8) + version: 2.0.0-2(patch_hash=168146d45476a6e2019bd9b140bf068fd17c12fb6b7d282509552826e74740dc)(@y/protocols@1.0.6-rc.1(@y/y@14.0.0-rc.17))(@y/y@14.0.0-rc.17)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8) '@y/protocols': specifier: ^1.0.6-rc.1 - version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) + version: 1.0.6-rc.1(@y/y@14.0.0-rc.17) '@y/websocket': specifier: ^4.0.0-rc.2 - version: 4.0.0-rc.2(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) + version: 4.0.0-rc.2(@y/y@14.0.0-rc.17) '@y/y': - specifier: ^14.0.0-rc.16 - version: 14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9) + specifier: ^14.0.0-rc.17 + version: 14.0.0-rc.17 react: specifier: ^19.2.3 version: 19.2.5 @@ -4155,8 +4154,8 @@ importers: specifier: ^9.0.2 version: 9.1.1(react@19.2.5) lib0: - specifier: ^0.2.99 - version: 0.2.117 + specifier: 1.0.0-rc.14 + version: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) react: specifier: ^19.2.3 version: 19.2.5 @@ -4208,16 +4207,16 @@ importers: version: 9.1.1(react@19.2.5) '@y/protocols': specifier: ^1.0.6-rc.1 - version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) + version: 1.0.6-rc.1(@y/y@14.0.0-rc.17) '@y/websocket': specifier: ^4.0.0-3 - version: 4.0.0-rc.2(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) + version: 4.0.0-rc.2(@y/y@14.0.0-rc.17) '@y/y': - specifier: ^14.0.0-rc.16 - version: 14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9) + specifier: ^14.0.0-rc.17 + version: 14.0.0-rc.17 lib0: - specifier: 1.0.0-rc.13 - version: 1.0.0-rc.13(patch_hash=328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e) + specifier: 1.0.0-rc.14 + version: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) react: specifier: ^19.2.3 version: 19.2.5 @@ -4958,13 +4957,13 @@ importers: version: 3.22.4 '@y/prosemirror': specifier: ^2.0.0-2 - version: 2.0.0-2(patch_hash=802334aa5d2410b9fc999d5fb5ffef01650a04e58b59fca24e142c55b3379728)(@y/protocols@1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)))(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9))(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8) + version: 2.0.0-2(patch_hash=168146d45476a6e2019bd9b140bf068fd17c12fb6b7d282509552826e74740dc)(@y/protocols@1.0.6-rc.1(@y/y@14.0.0-rc.17))(@y/y@14.0.0-rc.17)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8) '@y/protocols': specifier: ^1.0.6-rc.1 - version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) + version: 1.0.6-rc.1(@y/y@14.0.0-rc.17) '@y/y': - specifier: ^14.0.0-rc.16 - version: 14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9) + specifier: ^14.0.0-rc.17 + version: 14.0.0-rc.17 emoji-mart: specifier: ^5.6.0 version: 5.6.0 @@ -4972,8 +4971,8 @@ importers: specifier: ^3.1.3 version: 3.1.3 lib0: - specifier: 1.0.0-rc.13 - version: 1.0.0-rc.13(patch_hash=328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e) + specifier: 1.0.0-rc.14 + version: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) prosemirror-highlight: specifier: ^0.15.1 version: 0.15.1(@shikijs/types@4.0.2)(@types/hast@3.0.4)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-transform@1.12.0)(prosemirror-view@1.41.8) @@ -6141,10 +6140,10 @@ importers: version: 4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(playwright@1.51.1)(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7) '@y/protocols': specifier: ^1.0.6-rc.1 - version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) + version: 1.0.6-rc.1(@y/y@14.0.0-rc.17) '@y/y': - specifier: ^14.0.0-rc.16 - version: 14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9) + specifier: ^14.0.0-rc.17 + version: 14.0.0-rc.17 eslint: specifier: ^8.57.1 version: 8.57.1 @@ -11541,8 +11540,8 @@ packages: peerDependencies: '@y/y': '*' - '@y/y@14.0.0-rc.16': - resolution: {integrity: sha512-OjPE92lb19rOK6Dnjxg5VUTsVa/XfBUiIylazNndGiePebIyrvLRoPgKHibPEPYT215Jd20fsuyfBdzk4iT5cA==} + '@y/y@14.0.0-rc.17': + resolution: {integrity: sha512-qzKOdjFcZBHxnbxc+4TKx/DCk9UwLCgXQjyQn4bbN9aEzDVQtzN7L18VaGrN4HTEDbHNrlevxvdIdz92Vk5TBA==} engines: {node: '>=22.0.0', npm: '>=8.0.0'} '@yarnpkg/lockfile@1.1.0': @@ -13835,9 +13834,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic.js@0.2.5: - resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} - iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -14052,13 +14048,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lib0@0.2.117: - resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} - engines: {node: '>=16'} - hasBin: true - - lib0@1.0.0-rc.13: - resolution: {integrity: sha512-4y73dAr8BHgIwQlBxJe2+QX4bFmPxS/t9SJQfJgH9sn/Zv/TisvWqNfYgqDIVVFevZ6yTW1ShuT08Ox8nTEmxg==} + lib0@1.0.0-rc.14: + resolution: {integrity: sha512-zXdJpWHTbkKGw7MsjillED5+CTl44I5UeZr2MViWFVhoAsoyNJmQGG5lm9ecnI2ZeV5tGhj47WGN2V+tY42LGg==} engines: {node: '>=22'} hasBin: true @@ -23196,29 +23187,29 @@ snapshots: dependencies: '@types/node': 20.19.39 - '@y/prosemirror@2.0.0-2(patch_hash=802334aa5d2410b9fc999d5fb5ffef01650a04e58b59fca24e142c55b3379728)(@y/protocols@1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)))(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9))(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)': + '@y/prosemirror@2.0.0-2(patch_hash=168146d45476a6e2019bd9b140bf068fd17c12fb6b7d282509552826e74740dc)(@y/protocols@1.0.6-rc.1(@y/y@14.0.0-rc.17))(@y/y@14.0.0-rc.17)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)': dependencies: - '@y/protocols': 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) - '@y/y': 14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9) - lib0: 1.0.0-rc.13(patch_hash=328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e) + '@y/protocols': 1.0.6-rc.1(@y/y@14.0.0-rc.17) + '@y/y': 14.0.0-rc.17 + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 prosemirror-view: 1.41.8 - '@y/protocols@1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9))': + '@y/protocols@1.0.6-rc.1(@y/y@14.0.0-rc.17)': dependencies: - '@y/y': 14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9) - lib0: 1.0.0-rc.13(patch_hash=328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e) + '@y/y': 14.0.0-rc.17 + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) - '@y/websocket@4.0.0-rc.2(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9))': + '@y/websocket@4.0.0-rc.2(@y/y@14.0.0-rc.17)': dependencies: - '@y/protocols': 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) - '@y/y': 14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9) - lib0: 1.0.0-rc.13(patch_hash=328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e) + '@y/protocols': 1.0.6-rc.1(@y/y@14.0.0-rc.17) + '@y/y': 14.0.0-rc.17 + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) - '@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)': + '@y/y@14.0.0-rc.17': dependencies: - lib0: 1.0.0-rc.13(patch_hash=328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e) + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) '@yarnpkg/lockfile@1.1.0': {} @@ -25881,8 +25872,6 @@ snapshots: isexe@2.0.0: {} - isomorphic.js@0.2.5: {} - iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -26171,11 +26160,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lib0@0.2.117: - dependencies: - isomorphic.js: 0.2.5 - - lib0@1.0.0-rc.13(patch_hash=328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e): {} + lib0@1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20): {} lie@3.3.0: dependencies: @@ -29673,19 +29658,19 @@ snapshots: y-indexeddb@9.0.12(yjs@13.6.30): dependencies: - lib0: 0.2.117 + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) yjs: 13.6.30 y-leveldb@0.1.2(yjs@13.6.30): dependencies: level: 6.0.1 - lib0: 0.2.117 + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) yjs: 13.6.30 optional: true y-partykit@0.0.25: dependencies: - lib0: 0.2.117 + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) lodash.debounce: 4.0.8 react: 18.3.1 y-protocols: 1.0.7(yjs@13.6.30) @@ -29693,7 +29678,7 @@ snapshots: y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30): dependencies: - lib0: 0.2.117 + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 prosemirror-view: 1.41.8 @@ -29702,12 +29687,12 @@ snapshots: y-protocols@1.0.7(yjs@13.6.30): dependencies: - lib0: 0.2.117 + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) yjs: 13.6.30 y-websocket@2.1.0(yjs@13.6.30): dependencies: - lib0: 0.2.117 + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) lodash.debounce: 4.0.8 y-protocols: 1.0.7(yjs@13.6.30) yjs: 13.6.30 @@ -29743,7 +29728,7 @@ snapshots: yjs@13.6.30: dependencies: - lib0: 0.2.117 + lib0: 1.0.0-rc.14(patch_hash=e55ee919122bfd99c2418ad2ca66b24bdc0770c01033d31bbec567d195600f20) yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c532f7b0f5..2a62d5af72 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -19,7 +19,10 @@ overrides: "@tiptap/pm": "^3.0.0" "vitest": "4.1.7" "@vitest/runner": "4.1.7" - "@y/prosemirror>lib0": "1.0.0-rc.13" + # Force the whole @y/* v14 ecosystem (@y/prosemirror, @y/y, @y/protocols, + # @y/websocket — all declare lib0 ^1.0.0-rc.13) onto the single patched + # lib0 rc.14, so the matchNodes diff patch is the only lib0 delta instance. + "lib0": "1.0.0-rc.14" allowBuilds: "@parcel/watcher": true "@sentry/cli": true @@ -35,5 +38,4 @@ allowBuilds: leveldown: false patchedDependencies: "@y/prosemirror@2.0.0-2": "patches/@y__prosemirror@2.0.0-2.patch" - '@y/y@14.0.0-rc.16': patches/@y__y@14.0.0-rc.16.patch - lib0@1.0.0-rc.13: patches/lib0@1.0.0-rc.13.patch + lib0@1.0.0-rc.14: patches/lib0@1.0.0-rc.14.patch diff --git a/scripts/patch-lib0.sh b/scripts/patch-lib0.sh index 70a99d14dc..e7e4d2c644 100755 --- a/scripts/patch-lib0.sh +++ b/scripts/patch-lib0.sh @@ -27,7 +27,7 @@ echo "==> Building lib0 (npm run dist) ..." (cd "$LOCAL_LIB0" && npm run dist) # Best-effort cleanup of any leftover patch dir (case-insensitive FS resolves this fine). -STALE_PATCH_DIR="$BLOCKNOTE_ROOT/node_modules/.pnpm_patches/lib0@1.0.0-rc.13" +STALE_PATCH_DIR="$BLOCKNOTE_ROOT/node_modules/.pnpm_patches/lib0@1.0.0-rc.14" # 1. Clean up any leftover patch dir, then start fresh if [[ -d "$STALE_PATCH_DIR" ]]; then @@ -35,15 +35,15 @@ if [[ -d "$STALE_PATCH_DIR" ]]; then rm -rf "$STALE_PATCH_DIR" fi -echo "==> Running pnpm patch lib0@1.0.0-rc.13 ..." +echo "==> Running pnpm patch lib0@1.0.0-rc.14 ..." cd "$BLOCKNOTE_ROOT" # Capture pnpm's reported patch dir so we use the canonical on-disk path casing. # Constructing PATCH_DIR manually breaks on macOS when the repo is entered via a # differently-cased path (e.g. blockNote vs BlockNote): pnpm patch-commit matches # the path against state.json case-sensitively and fails with ERR_PNPM_INVALID_PATCH_DIR. -PATCH_OUTPUT="$(pnpm patch lib0@1.0.0-rc.13)" +PATCH_OUTPUT="$(pnpm patch lib0@1.0.0-rc.14)" echo "$PATCH_OUTPUT" -PATCH_DIR="$(printf '%s\n' "$PATCH_OUTPUT" | grep -Eo '/.*/\.pnpm_patches/lib0@1\.0\.0-rc\.13' | head -n1)" +PATCH_DIR="$(printf '%s\n' "$PATCH_OUTPUT" | grep -Eo '/.*/\.pnpm_patches/lib0@1\.0\.0-rc\.14' | head -n1)" if [[ -z "$PATCH_DIR" || ! -d "$PATCH_DIR" ]]; then echo "ERROR: Could not determine patch dir from 'pnpm patch' output" @@ -70,7 +70,7 @@ const orig = JSON.parse(fs.readFileSync('$PATCH_DIR/package.json', 'utf8')); const local = JSON.parse(fs.readFileSync('$LOCAL_LIB0/package.json', 'utf8')); // Keep the original version so pnpm doesn't try to fetch a different version from registry -orig.version = '1.0.0-rc.13'; +orig.version = '1.0.0-rc.14'; // Update exports orig.exports = local.exports; @@ -95,4 +95,4 @@ echo "==> Running pnpm patch-commit ..." pnpm patch-commit "$PATCH_DIR" echo "" -echo "==> Done! Patch regenerated at patches/lib0@1.0.0-rc.13.patch" +echo "==> Done! Patch regenerated at patches/lib0@1.0.0-rc.14.patch" diff --git a/scripts/patch-yjs.sh b/scripts/patch-yjs.sh index 1a443eb3fc..43a1253216 100755 --- a/scripts/patch-yjs.sh +++ b/scripts/patch-yjs.sh @@ -12,7 +12,7 @@ set -euo pipefail # Version that is actually installed in this repo (pnpm patches the installed # version). The local ../yjs checkout may be a newer rc; we still pin to this. YJS_PKG="@y/y" -YJS_VERSION="14.0.0-rc.16" +YJS_VERSION="14.0.0-rc.17" # pnpm keeps the scope path for the temp patch dir (e.g. .pnpm_patches/@y/y@VER) # but escapes "/" to "__" for the committed patch file name. diff --git a/tests/package.json b/tests/package.json index a276b22593..4074a3ce78 100644 --- a/tests/package.json +++ b/tests/package.json @@ -30,7 +30,7 @@ "@vitest/browser": "4.1.7", "@vitest/browser-playwright": "4.1.7", "@y/protocols": "^1.0.6-rc.1", - "@y/y": "^14.0.0-rc.16", + "@y/y": "^14.0.0-rc.17", "eslint": "^8.57.1", "htmlfy": "^0.6.7", "react": "^19.2.5", diff --git a/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts b/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts index 0606d7dd85..3c7ba05605 100644 --- a/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts +++ b/tests/src/unit/shared/formatConversion/exportParseEquality/exportParseEqualityTestExecutors.ts @@ -86,9 +86,7 @@ export const testExportParseEqualityMarkdown = async < // strict equality with the input. await expect( await editor.tryParseMarkdownToBlocks(exported), - ).toMatchFileSnapshot( - `./__snapshots__/markdown/${testCase.name}.json`, - ); + ).toMatchFileSnapshot(`./__snapshots__/markdown/${testCase.name}.json`); }; export const testExportParseEqualityNodes = async < @@ -108,7 +106,7 @@ export const testExportParseEqualityNodes = async < ); expect( - exported.map((node) => nodeToBlock(node, editor.pmSchema)), + exported.map((node) => nodeToBlock(node, editor.prosemirrorState.doc)), ).toStrictEqual( partialBlocksToBlocksForTesting(editor.schema, testCase.content), );