diff --git a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts index a76373127c..d37fb05005 100644 --- a/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts @@ -177,6 +177,7 @@ const mergeBlocks = ( } // Save suggestion node content before reconstruction + // drops prev suggestionBefore and next suggestionAfter on merge const savedPrevSuggAfter = currentPrevInfo.suggestionAfter ? currentPrevInfo.suggestionAfter.node.copy( currentPrevInfo.suggestionAfter.node.content, @@ -236,6 +237,7 @@ const mergeBlocks = ( } // Create the new blockContainer with the prev block's ID and attributes + // create() skips validation; bad child order ships silently const newBlockContainer = currentPrevInfo.bnBlock.node.type.create( currentPrevInfo.bnBlock.node.attrs, newChildren, diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts index 24248cfc3c..f4b49be4b8 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts @@ -106,6 +106,7 @@ function updateBlockSelectionFromData( anchorBlockPos + data.headCellOffset, ); } else if (data.type === "node") { + // +1 assumes blockContent is first child; may be a leading suggestion node selection = NodeSelection.create(tr.doc, anchorBlockPos + 1); } else { const headBlockPos = getNodeById(data.headBlockId, tr.doc)?.posBeforeNode; diff --git a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts index c995faeda1..aaaef246d5 100644 --- a/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/nestBlock/nestBlock.ts @@ -39,6 +39,7 @@ function sinkItem( if (nodeBefore.type !== itemType) { return false; } + // lastChild may be a trailing suggestion node, not blockGroup const nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type === groupType; // change 2 const inner = Fragment.from(nestedBefore ? itemType.create() : null); @@ -102,6 +103,7 @@ function liftToOuterList( // There are siblings after the lifted items, which must become // children of the last item const blockBeingLifted = range.parent.child(range.endIndex - 1); + // lastChild may be a trailing suggestion node, not blockGroup const nestedAfter = blockBeingLifted.lastChild && blockBeingLifted.lastChild.type === groupType; // change 2 diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts index 3097851f47..32ed4782e4 100644 --- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts +++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts @@ -18,6 +18,7 @@ export function isEmptyColumn(column: Node) { throw new Error("Invalid column: does not have child node."); } + // firstChild may be a suggestion node; childCount===1 below assumes no suggestions const blockContent = blockContainer.firstChild; if (!blockContent) { throw new Error("Invalid blockContainer: does not have child node."); diff --git a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.ts b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.ts index 785714fd13..a8c4a59f6b 100644 --- a/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/splitBlock/splitBlock.ts @@ -47,6 +47,7 @@ export const splitBlockTr = ( // with the first block and the split happens at the blockContent boundary. let effectivePos = posInBlock; const $pos = tr.doc.resolve(posInBlock); + // dead guard — group is compound "suggestionBlockContent blockContent", never === if ($pos.parent.type.spec.group === "suggestionBlockContent") { effectivePos = info.blockContent.beforePos + 1; } diff --git a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts index a3e2b3b0db..7dbafe2c80 100644 --- a/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +++ b/packages/core/src/api/blockManipulation/commands/updateBlock/updateBlock.ts @@ -302,6 +302,7 @@ function updateChildren< throw new Error("impossible"); } // Inserts a new blockGroup containing the child nodes created earlier. + // inserts blockGroup before trailing suggestion, invalid content order tr.insert( blockInfo.blockContent.afterPos, pmSchema.nodes["blockGroup"].createChecked({}, childNodes), diff --git a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts index c018c907a5..4db6384044 100644 --- a/packages/core/src/api/blockManipulation/getBlock/getBlock.ts +++ b/packages/core/src/api/blockManipulation/getBlock/getBlock.ts @@ -22,6 +22,7 @@ export function getBlock< typeof blockIdentifier === "string" ? blockIdentifier : blockIdentifier.id; const pmSchema = getPmSchema(doc); + // suggested-deleted blocks resolve as live, no deletion flag const posInfo = getNodeById(id, doc); if (!posInfo) { return undefined; diff --git a/packages/core/src/api/blockManipulation/insertContentAt.ts b/packages/core/src/api/blockManipulation/insertContentAt.ts index 3409583a37..2787cf50bf 100644 --- a/packages/core/src/api/blockManipulation/insertContentAt.ts +++ b/packages/core/src/api/blockManipulation/insertContentAt.ts @@ -47,6 +47,7 @@ export function insertContentAt( // replace an empty paragraph by an inserted image // instead of inserting the image below the paragraph if (from === to && isOnlyBlockContent) { + // parent may be an empty suggestion shadow node, not blockContent const { parent } = tr.doc.resolve(from); const isEmptyTextBlock = parent.isTextblock && !parent.type.spec.code && !parent.childCount; diff --git a/packages/core/src/api/blockManipulation/selections/selection.ts b/packages/core/src/api/blockManipulation/selections/selection.ts index e5bd761918..fc23c438d7 100644 --- a/packages/core/src/api/blockManipulation/selections/selection.ts +++ b/packages/core/src/api/blockManipulation/selections/selection.ts @@ -236,6 +236,7 @@ export function getSelectionCutBlocks(tr: Transaction, expandToWords = false) { // the correct information about whether content is cut at the start or end of a block // if the end is at the end of a node (|

) move it forward so we include all closing tags (

|) + // end.parent may be a suggestion shadow node when crossing a block boundary while (end.parentOffset >= end.parent.nodeSize - 2 && end.depth > 0) { end = tr.doc.resolve(end.pos + 1); } @@ -251,6 +252,7 @@ export function getSelectionCutBlocks(tr: Transaction, expandToWords = false) { } // if the start is at the end of a node (|

|) move it forwards so we drop all closing tags (|

) + // start.parent may be a suggestion shadow node when crossing a block boundary while (start.parentOffset >= start.parent.nodeSize - 2 && start.depth > 0) { start = tr.doc.resolve(start.pos + 1); } diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts index e150af1309..7269dcd033 100644 --- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts +++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts @@ -50,6 +50,7 @@ function fragmentToExternalHTML< (child) => child.type.isInGroup("bnBlock") || child.type.name === "blockGroup" || + // === "blockContent" misclassifies suggestion nodes (compound group) child.type.spec.group === "blockContent", ) === undefined; if (isWithinBlockContent) { @@ -118,9 +119,11 @@ export function selectedFragmentToHTML< // selected, e.g. an image block. if ( "node" in view.state.selection && + // === "blockContent" misclassifies suggestion nodes (compound group) (view.state.selection.node as Node).type.spec.group === "blockContent" ) { editor.transact((tr) => + // from-1 block expansion assumes blockContent adjacency; off with leading suggestion node tr.setSelection( new NodeSelection(tr.doc.resolve(view.state.selection.from - 1)), ), @@ -128,6 +131,7 @@ export function selectedFragmentToHTML< } // Uses default ProseMirror clipboard serialization. + // serializeForClipboard emits shadow nodes; paste re-creates them const clipboardHTML: string = view.serializeForClipboard( view.state.selection.content(), ).dom.innerHTML; @@ -268,6 +272,7 @@ export const createCopyToClipboardExtension = < // Expands the selection to the parent `blockContainer` node. editor.transact((tr) => + // from-1 block expansion assumes blockContent adjacency; off with leading suggestion node tr.setSelection( new NodeSelection( tr.doc.resolve(view.state.selection.from - 1), diff --git a/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts b/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts index 7faa154dc6..716ad72650 100644 --- a/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts +++ b/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts @@ -45,6 +45,7 @@ function serializeChildren(node: Node, ctx: SerializeContext): string { } function serializeNode(node: Node, ctx: SerializeContext): string { + // no data-suggestion handling; shadow text duplicated to markdown if (node.nodeType === 3 /* Node.TEXT_NODE */) { return node.textContent || ""; } diff --git a/packages/core/src/api/getBlockInfoFromPos.ts b/packages/core/src/api/getBlockInfoFromPos.ts index 4f340d98a4..c898ba4e91 100644 --- a/packages/core/src/api/getBlockInfoFromPos.ts +++ b/packages/core/src/api/getBlockInfoFromPos.ts @@ -41,6 +41,7 @@ export type BlockInfo = { * Whether bnBlock is a blockContainer node */ isBlockContainer: true; + // (Affects ~58 callsites) consumers use blockContent/bnBlock but never branch on suggestionBefore/After /** * A suggestion node that appears before the blockContent, if present. * Suggestion nodes have group "suggestionBlockContent" and are used for @@ -191,6 +192,7 @@ export function getBlockInfoWithManualOffset( afterPos: suggestionAfterPos, }; + // singular suggestionBefore/After, schema allows suggestionBlockContent* if (!foundBlockContent) { suggestionBefore = info; } else { diff --git a/packages/core/src/api/nodeConversions/blockToNode.ts b/packages/core/src/api/nodeConversions/blockToNode.ts index d970227a49..258ea93e19 100644 --- a/packages/core/src/api/nodeConversions/blockToNode.ts +++ b/packages/core/src/api/nodeConversions/blockToNode.ts @@ -321,6 +321,7 @@ function blockOrInlineContentToContentNode( /** * Converts a BlockNote block to a Prosemirror node. */ +// (Affects ~7 callsites) emits no suggestion nodes, round-trips drop suggestions export function blockToNode( block: PartialBlock, schema: Schema, diff --git a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts index 724b552bda..4fbae84809 100644 --- a/packages/core/src/api/nodeConversions/fragmentToBlocks.ts +++ b/packages/core/src/api/nodeConversions/fragmentToBlocks.ts @@ -22,6 +22,7 @@ export function fragmentToBlocks< fragment.descendants((node) => { const pmSchema = getPmSchema(node); if (node.type.name === "blockContainer") { + // firstChild may be a suggestion node, not blockContent or blockGroup if (node.firstChild?.type.name === "blockGroup") { // selection started within a block group // in this case the fragment starts with: diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts index 1a777ad892..f921b51c69 100644 --- a/packages/core/src/api/nodeConversions/nodeToBlock.ts +++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts @@ -482,6 +482,7 @@ export function nodeToBlock< throw new UnreachableCaseError(blockConfig.content); } + // (Affects ~23 callsites) strips suggestion state, Block API is suggestion-blind const block = { id, type: blockConfig.type, @@ -604,6 +605,7 @@ export function prosemirrorSliceToSlicedBlocks< let firstNonSuggestionChild: Node | undefined; let blockGroupChild: Node | undefined; blockContainer.forEach((child) => { + // dead guard — group is compound "suggestionBlockContent blockContent", never === if (child.type.spec.group === "suggestionBlockContent") { return; // skip suggestion nodes } diff --git a/packages/core/src/api/nodeUtil.ts b/packages/core/src/api/nodeUtil.ts index 3388c95413..6530b60b3e 100644 --- a/packages/core/src/api/nodeUtil.ts +++ b/packages/core/src/api/nodeUtil.ts @@ -17,6 +17,7 @@ export function getNodeById( } // Keeps traversing nodes if block with target ID has not been found. + // suggested-deleted blocks resolve as live, no deletion flag if (!isNodeBlock(node) || node.attrs.id !== 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..a8fac6f305 100644 --- a/packages/core/src/api/parsers/html/parseHTML.ts +++ b/packages/core/src/api/parsers/html/parseHTML.ts @@ -23,6 +23,7 @@ export function HTMLToBlocks< // const doc = pmSchema.nodes["doc"].createAndFill()!; // and context: doc.resolve(3), + // parse may auto-create *--attributed shadow nodes from BlockNote HTML const parentNode = parser.parse(htmlNode, { topNode: pmSchema.nodes["blockGroup"].create(), }); diff --git a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts index eb6b06dc7d..9099d743f8 100644 --- a/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts +++ b/packages/core/src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts @@ -28,6 +28,7 @@ function calculateListItemIndex( tr: Transaction, map: Map, ): { index: number; isFirst: boolean; hasStart: boolean } { + // firstChild may be a suggestion node, not blockContent const hasStart = !!node.firstChild!.attrs["start"]; // Fast path: previous sibling already in cache @@ -93,6 +94,7 @@ function calculateListItemIndex( isFirst = false; } else { // Start of a new list + // firstChild may be a suggestion node, not blockContent index = (lastInChain.node.firstChild!.attrs["start"] || 1) - 1; isFirst = true; } @@ -156,6 +158,7 @@ function getDecorations( if ( node.type.name === "blockContainer" && + // firstChild may be a suggestion node, not blockContent node.firstChild!.type.name === "numberedListItem" ) { const { index, isFirst, hasStart } = calculateListItemIndex( @@ -168,6 +171,7 @@ function getDecorations( // Search only the numberedListItem node range, not the full // blockContainer (which includes nested blockGroups whose // decorations could falsely match). + // pos + 1 assumes blockContent is first child, not a suggestion node const blockNode = tr.doc.nodeAt(pos + 1)!; const existingDecorations = nextDecorationSet.find( pos + 1, diff --git a/packages/core/src/blocks/defaultBlockHelpers.ts b/packages/core/src/blocks/defaultBlockHelpers.ts index ccadf93e11..800359565c 100644 --- a/packages/core/src/blocks/defaultBlockHelpers.ts +++ b/packages/core/src/blocks/defaultBlockHelpers.ts @@ -71,6 +71,7 @@ export const defaultBlockToHTML = < if (node.type.name === "blockContainer") { // for regular blocks, get the toDOM spec from the blockContent node + // firstChild safe here; node built by suggestion-blind blockToNode node = node.firstChild!; } diff --git a/packages/core/src/comments/extension.ts b/packages/core/src/comments/extension.ts index c037e80ddf..5141a13611 100644 --- a/packages/core/src/comments/extension.ts +++ b/packages/core/src/comments/extension.ts @@ -30,6 +30,7 @@ function getUpdatedThreadPositions(doc: Node, markType: string) { const threadPositions = new Map(); // find all thread marks and store their position + create decoration for selected thread + // comment marks inside shadow nodes anchor threads to suggestion content doc.descendants((node, pos) => { node.marks.forEach((mark) => { if (mark.type.name === markType) { @@ -228,6 +229,7 @@ export const CommentsExtension = createExtension( return false; } + // click may land in a suggestion shadow node, resolving its comment marks const node = view.state.doc.nodeAt(pos); if (!node) { diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index afb9222e64..6614b4caa1 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -734,6 +734,7 @@ div[data-type="modification"] { text-decoration: none; } +/* no block-level suggestion-state styling (deletion rule disabled) */ .bn-root del, [DISABLED-data-node-deletion] { color: rgba(100, 90, 75, 0.3); diff --git a/packages/core/src/editor/managers/ExportManager.ts b/packages/core/src/editor/managers/ExportManager.ts index 3fe1ee2f0e..bf6fee875f 100644 --- a/packages/core/src/editor/managers/ExportManager.ts +++ b/packages/core/src/editor/managers/ExportManager.ts @@ -34,6 +34,7 @@ export class ExportManager< * @param blocks An array of blocks that should be serialized into HTML. * @returns The blocks, serialized as an HTML string. */ + // (Affects ~3) exports suggestion-blind Block JSON, drops shadow content public blocksToHTMLLossy( blocks: PartialBlock[] = this.editor.document, ): string { diff --git a/packages/core/src/editor/transformPasted.ts b/packages/core/src/editor/transformPasted.ts index 4f0515df95..e94a150f3f 100644 --- a/packages/core/src/editor/transformPasted.ts +++ b/packages/core/src/editor/transformPasted.ts @@ -145,6 +145,7 @@ export function transformPasted(slice: Slice, view: EditorView) { } for (let i = 0; i < f.childCount; i++) { + // === "blockContent" misclassifies suggestion nodes (compound group) if (f.child(i).type.spec.group === "blockContent") { const content = [f.child(i)]; @@ -154,6 +155,7 @@ export function transformPasted(slice: Slice, view: EditorView) { i + 1 < f.childCount && f.child(i + 1).type.name === "blockGroup" // TODO ) { + // .child(0).child(0) assumes blockContent is first child of blockContainer const nestedChild = f .child(i + 1) .child(0) @@ -227,6 +229,7 @@ function retypeLeadingParagraphForEmptyTarget( const blockGroup = fragment.firstChild; const blockContainer = blockGroup?.firstChild; + // firstChild may be a suggestion node, not blockContent const leading = blockContainer?.firstChild; if ( blockGroup?.type.name !== "blockGroup" || @@ -238,6 +241,7 @@ function retypeLeadingParagraphForEmptyTarget( const retyped = target.type.create(target.attrs, leading.content); const newBlockContainer = blockContainer.copy( + // replaceChild(0,...) assumes index 0 is blockContent, not a suggestion node blockContainer.content.replaceChild(0, retyped), ); const newBlockGroup = blockGroup.copy( diff --git a/packages/core/src/extensions/DropCursor/utils.ts b/packages/core/src/extensions/DropCursor/utils.ts index 25e3985238..8e7d00e170 100644 --- a/packages/core/src/extensions/DropCursor/utils.ts +++ b/packages/core/src/extensions/DropCursor/utils.ts @@ -58,8 +58,10 @@ export function getBlockDropRect( return null; } + // nodeBefore may be a suggestion node, not blockContent const before = $pos.nodeBefore; + // nodeAfter may be a suggestion node, not blockContent const after = $pos.nodeAfter; if (!before && !after) { diff --git a/packages/core/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboard.ts b/packages/core/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboard.ts index 357780f44a..2fe139a5a2 100644 --- a/packages/core/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboard.ts +++ b/packages/core/src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboard.ts @@ -49,6 +49,7 @@ export const NodeSelectionKeyboardExtension = createExtension( const tr = view.state.tr; view.dispatch( tr + // $to.after() may land before a trailing suggestion node .insert( view.state.tr.selection.$to.after(), view.state.schema.nodes["paragraph"].createChecked(), @@ -56,6 +57,7 @@ export const NodeSelectionKeyboardExtension = createExtension( .setSelection( new TextSelection( tr.doc.resolve( + // +1 may resolve inside a trailing suggestion node view.state.tr.selection.$to.after() + 1, ), ), diff --git a/packages/core/src/extensions/Placeholder/Placeholder.ts b/packages/core/src/extensions/Placeholder/Placeholder.ts index b8ff2e14ed..90e39d341b 100644 --- a/packages/core/src/extensions/Placeholder/Placeholder.ts +++ b/packages/core/src/extensions/Placeholder/Placeholder.ts @@ -117,6 +117,7 @@ export const PlaceholderExtension = createExtension( // decoration for when there's only one empty block // positions are hardcoded for now + // hardcoded size===6 and positions 2,4 assume no suggestion nodes in empty doc if (state.doc.content.size === 6) { decs.push( Decoration.node(2, 4, { diff --git a/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts b/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts index 714783ad28..290783ad6e 100644 --- a/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts +++ b/packages/core/src/extensions/PreviousBlockType/PreviousBlockType.ts @@ -104,6 +104,7 @@ export const PreviousBlockTypeExtension = createExtension(() => { for (const node of newNodes) { const oldNode = oldNodesById.get(node.node.attrs.id); + // firstChild may be a suggestion node, not blockContent const oldContentNode = oldNode?.node.firstChild; const newContentNode = node.node.firstChild; diff --git a/packages/core/src/extensions/SideMenu/SideMenu.ts b/packages/core/src/extensions/SideMenu/SideMenu.ts index e98059b585..d36acac8ad 100644 --- a/packages/core/src/extensions/SideMenu/SideMenu.ts +++ b/packages/core/src/extensions/SideMenu/SideMenu.ts @@ -258,6 +258,7 @@ export class SideMenuView< blockContentBoundingBox.width, blockContentBoundingBox.height, ), + // resolves suggested-deleted/modified blocks as if live (no suggestion state) block: this.editor.getBlock( this.hoveredBlock!.getAttribute("data-id")!, )!, diff --git a/packages/core/src/extensions/SideMenu/dragging.ts b/packages/core/src/extensions/SideMenu/dragging.ts index f8ba326538..63bb784ddd 100644 --- a/packages/core/src/extensions/SideMenu/dragging.ts +++ b/packages/core/src/extensions/SideMenu/dragging.ts @@ -39,6 +39,7 @@ function blockPositionsFromSelection(selection: Selection, doc: Node) { // the same blocks again. If this happens, the anchor & head move out of the block content node they were originally // in. If the anchor should update but the head shouldn't and vice versa, it means the user selection is outside a // block content node, which should never happen. + // === "blockContent" misclassifies suggestion nodes (compound group) const selectionStartInBlockContent = doc.resolve(selection.from).node().type.spec.group === "blockContent"; const selectionEndInBlockContent = diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts index 4809607e6e..a52d39f4e8 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionMenu.ts @@ -309,6 +309,7 @@ export const SuggestionMenu = createExtension(({ editor }) => { transaction.getMeta("blur") || transaction.getMeta("pointer") || // Moving the caret before the character which triggered the menu should hide it. + // no cursor-in-suggestion-node guard (same-parent check is suggestion-blind) (prev.triggerCharacter !== undefined && newState.selection.from < prev.queryStartPos()) || // Moving the caret to a new block should hide the menu. @@ -381,6 +382,7 @@ export const SuggestionMenu = createExtension(({ editor }) => { const blockNode = findBlock(state.selection); if (blockNode) { return DecorationSet.create(state.doc, [ + // decoration spans whole container incl suggestion shadow nodes Decoration.node( blockNode.pos, blockNode.pos + blockNode.node.nodeSize, diff --git a/packages/core/src/extensions/TableHandles/TableHandles.ts b/packages/core/src/extensions/TableHandles/TableHandles.ts index d957056f4e..3253a690b0 100644 --- a/packages/core/src/extensions/TableHandles/TableHandles.ts +++ b/packages/core/src/extensions/TableHandles/TableHandles.ts @@ -273,6 +273,7 @@ export class TableHandlesView implements PluginView { ); if (editorHasBlockWithType(this.editor, "table")) { + // posBeforeNode+1 assumes blockContent is first child; off with leading suggestion node this.tablePos = pmNodeInfo.posBeforeNode + 1; tableBlock = block; } diff --git a/packages/core/src/extensions/TrailingNode/TrailingNode.ts b/packages/core/src/extensions/TrailingNode/TrailingNode.ts index 59f95d2bed..921fcd07cf 100644 --- a/packages/core/src/extensions/TrailingNode/TrailingNode.ts +++ b/packages/core/src/extensions/TrailingNode/TrailingNode.ts @@ -17,6 +17,7 @@ function shouldShowTrailingWidget(doc: PMNode, isEditable: boolean): boolean { const rootGroup = doc.lastChild; const lastBlock = rootGroup?.lastChild; + // firstChild may be a suggestion node, not blockContent const lastContent = lastBlock?.firstChild; return !( diff --git a/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts index ee0123bcc7..a36a827986 100644 --- a/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +++ b/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts @@ -271,6 +271,7 @@ export const KeyboardShortcutsExtension = Extension.create<{ bottomNestedPrevBlockInfo.blockContent.node.type.spec .content === "tableRow+" ) { + // cascading -1 arithmetic; off if a leading suggestion node precedes blockContent const tableBlockEndPos = blockInfo.bnBlock.beforePos - 1; const tableBlockContentEndPos = tableBlockEndPos - 1; const lastRowEndPos = tableBlockContentEndPos - 1; @@ -674,6 +675,7 @@ export const KeyboardShortcutsExtension = Extension.create<{ nextBlockInfo.blockContent.node.type.spec.content === "tableRow+" ) { + // hardcoded +1 arithmetic; off if next block has a leading suggestion node const tableBlockStartPos = blockInfo.bnBlock.afterPos + 1; const tableBlockContentStartPos = tableBlockStartPos + 1; const firstRowStartPos = tableBlockContentStartPos + 1; diff --git a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts index 54cb8b7340..a795c6f344 100644 --- a/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts +++ b/packages/core/src/extensions/tiptap-extensions/UniqueID/UniqueID.ts @@ -205,6 +205,7 @@ const UniqueID = Extension.create({ }); return; } + // dedupe may regenerate the wrong id when a suggestion creates a duplicate id // check if the node doesn’t exist in the old state const { deleted } = mapping.invert().mapResult(pos); const newNode = deleted && duplicatedNewIds.includes(id); diff --git a/packages/core/src/pm-nodes/SpecialNode.test.ts b/packages/core/src/pm-nodes/SpecialNode.test.ts index c4f3442f63..f4163b4a5e 100644 --- a/packages/core/src/pm-nodes/SpecialNode.test.ts +++ b/packages/core/src/pm-nodes/SpecialNode.test.ts @@ -78,6 +78,7 @@ describe("SuggestionNode - structural", () => { it("should have suggestion-paragraph type registered in the PM schema", () => { const editor = BlockNoteEditor.create(); const nodeTypes = Object.keys(editor.pmSchema.nodes); + // asserts suggestion-* names, real nodes are *--attributed expect(nodeTypes).toContain("suggestion-paragraph"); expect(nodeTypes).toContain("blockContainer"); expect(nodeTypes).toContain("blockGroup"); diff --git a/packages/core/src/schema/blocks/createSpec.ts b/packages/core/src/schema/blocks/createSpec.ts index 99bd7aad85..324ab4a612 100644 --- a/packages/core/src/schema/blocks/createSpec.ts +++ b/packages/core/src/schema/blocks/createSpec.ts @@ -258,6 +258,7 @@ export function addNodeAndExtensionsToSpec< // 1. isRequired: true prevents ProseMirror's DOMParser from auto-creating // suggestion nodes to satisfy optional content expressions // 2. Rendered as data-suggestion="true" on the wrapper div for HTML parsing + // isRequired sentinel hack blocks DOMParser auto-creating shadow nodes attrs["y-attributed"] = { isRequired: true, parseHTML: (element: HTMLElement) => { @@ -285,6 +286,7 @@ export function addNodeAndExtensionsToSpec< ]; }, renderHTML({ HTMLAttributes }) { + // shadow node gets editable contentDOM, no nodeView selectable fix const div = document.createElement("div"); return wrapInBlockStructure( { diff --git a/packages/core/src/schema/blocks/internal.ts b/packages/core/src/schema/blocks/internal.ts index 6a76d1ca05..15d1078573 100644 --- a/packages/core/src/schema/blocks/internal.ts +++ b/packages/core/src/schema/blocks/internal.ts @@ -101,6 +101,7 @@ export function getBlockFromPos< } // Gets parent blockContainer node const blockContainer = tipTapEditor.state.doc.resolve(pos!).node(); + // dead guard — real nodes are named "*--attributed", never "suggestion-" if (blockContainer.type.name.startsWith("suggestion-")) { // The blockContent is inside a suggestion node, which is inside a blockContainer. // Return a stub block since suggestion nodes are transparent to the Block API. diff --git a/packages/core/src/y/extensions/YSync.ts b/packages/core/src/y/extensions/YSync.ts index 0acb7c8d10..c05fcdc032 100644 --- a/packages/core/src/y/extensions/YSync.ts +++ b/packages/core/src/y/extensions/YSync.ts @@ -122,6 +122,7 @@ export const YSyncExtension = createExtension( nodeName: string, kinds: { delete: boolean; insert: boolean; format: boolean }, ) => { + // attributedNodes gates shadows on blockSpecs and delete only const result = Boolean( editor.schema.blockSpecs[nodeName] && kinds.delete, ); diff --git a/packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.ts b/packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.ts index 0866c3523c..16a7daac5a 100644 --- a/packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.ts +++ b/packages/core/src/yjs/extensions/schemaMigration/migrationRules/moveColorAttributes.ts @@ -73,11 +73,13 @@ export const moveColorAttributes: MigrationRule = (fragment, tr) => { node.type.name === "blockContainer" && targetBlockContainers.has(node.attrs.id) ) { + // nodeAt(pos+1) assumes blockContent is first child (legacy input has no suggestions) const el = tr.doc.nodeAt(pos + 1); if (!el) { throw new Error("No element found"); } + // setNodeMarkup(pos+1) assumes blockContent is first child (legacy input has no suggestions) tr.setNodeMarkup(pos + 1, undefined, { // preserve existing attributes ...el.attrs, diff --git a/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx index fd9ab688bf..d9e8f1d6b6 100644 --- a/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx +++ b/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx @@ -41,6 +41,7 @@ export const EmbedTab = < (event: KeyboardEvent) => { if (event.key === "Enter" && !event.nativeEvent.isComposing) { event.preventDefault(); + // untracked prop edit on a possibly suggested file block editor.updateBlock(block.id, { props: { name: filenameFromURL(currentURL), @@ -53,6 +54,7 @@ export const EmbedTab = < ); const handleURLClick = useCallback(() => { + // untracked prop edit on a possibly suggested file block editor.updateBlock(block.id, { props: { name: filenameFromURL(currentURL), diff --git a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx index 64d5a1c74f..77b0a97bd3 100644 --- a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx +++ b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx @@ -62,6 +62,7 @@ export const UploadTab = < }, }; } + // untracked prop edit on a possibly suggested file block editor.updateBlock(props.blockId, updateData); } catch (e) { setUploadFailed(true); diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx index 470a50dcba..83a7cea12e 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx @@ -19,6 +19,7 @@ export const AddCommentButtonInner = () => { const { store } = useExtension(FormattingToolbarExtension); const onClick = useCallback(() => { + // comment can anchor to a suggested-deleted range comments.startPendingComment(); store.setState(false); }, [comments, store]); diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx index 309cc10e13..0655af2451 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx @@ -68,6 +68,7 @@ export const FileCaptionButton = () => { caption: "string", }) ) { + // untracked prop edit on a possibly suggested file block editor.updateBlock(block.id, { props: { caption: event.currentTarget.value, diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx index 9e2272c594..a46f161a6e 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx @@ -54,6 +54,7 @@ export const FileDeleteButton = () => { const onClick = useCallback(() => { if (block !== undefined) { editor.focus(); + // untracked delete on a possibly suggested file block editor.removeBlocks([block.id]); } }, [block, editor]); diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx index 3902651e30..b1522cd235 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx @@ -60,6 +60,7 @@ export const FilePreviewButton = () => { showPreview: "boolean", }) ) { + // untracked prop edit on a possibly suggested file block editor.updateBlock(block.id, { props: { showPreview: !block.props.showPreview, diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx index 87c30fef22..7b5808996d 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx @@ -68,6 +68,7 @@ export const FileRenameButton = () => { name: "string", }) ) { + // untracked prop edit on a possibly suggested file block editor.updateBlock(block.id, { props: { name: event.currentTarget.value, diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx index a21f1006cb..c80bf1435d 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx @@ -108,6 +108,7 @@ export const TextAlignButton = (props: { textAlignment: TextAlignment }) => { textAlignment: defaultProps.textAlignment, }) ) { + // untracked prop edit; block may be suggested-deleted/modified editor.updateBlock(block, { props: { textAlignment: textAlignment }, }); @@ -135,6 +136,7 @@ export const TextAlignButton = (props: { textAlignment: TextAlignment }) => { newTable[row].cells[col].props.textAlignment = textAlignment; }); + // untracked content edit on a possibly suggested table block editor.updateBlock(block, { type: "table", content: { diff --git a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx index d9bb12a00a..ae2a98e4e3 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultSelects/BlockTypeSelect.tsx @@ -167,6 +167,7 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { return filteredItems.map((item) => { const Icon = item.icon; + // reads suggestion-stripped type; wrong current type for suggested-modify const typesMatch = item.type === firstSelectedBlock.type; const propsMatch = Object.entries(item.props || {}).filter( @@ -181,6 +182,7 @@ export const BlockTypeSelect = (props: { items?: BlockTypeSelectItem[] }) => { editor.focus(); editor.transact(() => { for (const block of selectedBlocks) { + // untracked type change; block may be suggested-deleted/modified editor.updateBlock(block, { type: item.type as any, props: item.props as any, diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx index a10469eab1..4a7f0eabd8 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx +++ b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx @@ -69,6 +69,7 @@ export const FormattingToolbarController = (props: { const placement = useEditorState({ editor, selector: ({ editor }) => { + // resolves active block with no concept of suggestion state const block = editor.getTextCursorPosition().block; if ( diff --git a/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx b/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx index ac1209fad6..d09d80fa94 100644 --- a/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx +++ b/packages/react/src/components/SideMenu/DefaultButtons/AddBlockButton.tsx @@ -27,6 +27,7 @@ export const AddBlockButton = () => { return; } + // untracked insert on a possibly suggested-deleted block const blockContent = block.content; const isBlockEmpty = blockContent !== undefined && diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx index 97694194f7..3ff9536405 100644 --- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx +++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx @@ -55,6 +55,7 @@ export const BlockColorsItem = (props: { children: ReactNode }) => { }) ? { color: block.props.textColor, + // untracked prop edit; block may be suggested-deleted/modified setColor: (color) => editor.updateBlock(block, { type: block.type, diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/RemoveBlockItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/RemoveBlockItem.tsx index 12ade8b4ae..84ae65429e 100644 --- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/RemoveBlockItem.tsx +++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/RemoveBlockItem.tsx @@ -28,6 +28,7 @@ export const RemoveBlockItem = (props: { children: ReactNode }) => { selectedBlocks && selectedBlocks.some((b) => b.id === block.id) ? selectedBlocks : [block]; + // untracked delete; bypasses suggestion system on a possibly suggested block editor.removeBlocks(blocksToRemove); }} > diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx index 51211b9e06..2d0a5e4a23 100644 --- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx +++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/TableHeadersItem.tsx @@ -44,6 +44,7 @@ export const TableRowHeaderItem = (props: { children: ReactNode }) => { className={"bn-menu-item"} checked={isHeaderRow} onClick={() => { + // untracked prop edit on a possibly suggested table block editor.updateBlock(block, { content: { ...block.content, @@ -95,6 +96,7 @@ export const TableColumnHeaderItem = (props: { children: ReactNode }) => { className={"bn-menu-item"} checked={isHeaderColumn} onClick={() => { + // untracked prop edit on a possibly suggested table block editor.updateBlock(block, { content: { ...block.content, diff --git a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx index 9b863bde47..85a1f70952 100644 --- a/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx +++ b/packages/react/src/components/TableHandles/ExtendButton/ExtendButton.tsx @@ -99,6 +99,7 @@ export const ExtendButton = ( return; } + // untracked content edit on a possibly suggested table block editor.updateBlock(block, { type: "table", content: { @@ -159,6 +160,7 @@ export const ExtendButton = ( newNumCells > 0 && newNumCells !== currentNumCells ) { + // untracked content edit on a possibly suggested table block editor.updateBlock(block, { type: "table", content: { diff --git a/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx b/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx index 36e1ecca2f..f20f3983d3 100644 --- a/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx +++ b/packages/react/src/components/TableHandles/TableCellMenu/DefaultButtons/ColorPicker.tsx @@ -33,6 +33,7 @@ export const ColorPickerButton = (props: { children?: ReactNode }) => { return; } + // reads only new content; suggested-modify old content is invisible const newTable = block.content.rows.map((row) => { return { ...row, @@ -46,6 +47,7 @@ export const ColorPickerButton = (props: { children?: ReactNode }) => { newTable[rowIndex].cells[colIndex].props.backgroundColor = color; } + // untracked content edit on a possibly suggested table block editor.updateBlock(block, { type: "table", content: { diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx index 812a44ec35..72399b2b48 100644 --- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx +++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/ColorPicker.tsx @@ -63,6 +63,7 @@ export const ColorPickerButton = < return; } + // reads only new content; suggested-modify old content is invisible const newTable = block.content.rows.map((row) => { return { ...row, @@ -78,6 +79,7 @@ export const ColorPickerButton = < } }); + // untracked content edit on a possibly suggested table block editor.updateBlock(block, { type: "table", content: { diff --git a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx index 285f251b48..8cd08ee3a3 100644 --- a/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx +++ b/packages/react/src/components/TableHandles/TableHandleMenu/DefaultButtons/TableHeaderButton.tsx @@ -55,6 +55,7 @@ export const TableHeaderRowButton = < className={"bn-menu-item"} checked={isHeaderRow} onClick={() => { + // untracked prop edit on a possibly suggested table block editor.updateBlock(block, { ...block, content: { @@ -110,6 +111,7 @@ export const TableHeaderColumnButton = < className={"bn-menu-item"} checked={isHeaderColumn} onClick={() => { + // untracked prop edit on a possibly suggested table block editor.updateBlock(block, { ...block, content: { diff --git a/packages/react/src/schema/ReactBlockSpec.tsx b/packages/react/src/schema/ReactBlockSpec.tsx index da722395b3..8fbbe8692c 100644 --- a/packages/react/src/schema/ReactBlockSpec.tsx +++ b/packages/react/src/schema/ReactBlockSpec.tsx @@ -266,6 +266,7 @@ export function createReactBlockSpec< return output; }, render(block, editor) { + // nodeView wires real node only, shadow stays vanilla editable if (this.renderType === "nodeView") { return ReactNodeViewRenderer( (props: NodeViewProps) => { diff --git a/packages/xl-ai/src/prosemirror/agent.ts b/packages/xl-ai/src/prosemirror/agent.ts index f0c5f0063a..3866831f86 100644 --- a/packages/xl-ai/src/prosemirror/agent.ts +++ b/packages/xl-ai/src/prosemirror/agent.ts @@ -186,6 +186,7 @@ export function getStepsAsAgent(inputTr: Transform) { const stepIndex = tr.steps.length; if (isReplacing) { const $pos = tr.doc.resolve(tr.mapping.map(from)); + // isBlock true for shadow nodes, may mark suggestion content if ($pos.nodeAfter?.isBlock) { // mark the entire node as deleted. This can be needed for inline nodes or table cells tr.addNodeMark($pos.pos, pmSchema.mark("y-attributed-delete", {})); @@ -216,6 +217,7 @@ export function getStepsAsAgent(inputTr: Transform) { ) { return true; } + // isBlock true for shadow nodes, may mark suggestion content if (node.isBlock) { tr.addNodeMark(pos, pmSchema.mark("y-attributed-insert", {})); } diff --git a/packages/xl-ai/src/prosemirror/changeset.ts b/packages/xl-ai/src/prosemirror/changeset.ts index 7f40964016..eab66ce5eb 100644 --- a/packages/xl-ai/src/prosemirror/changeset.ts +++ b/packages/xl-ai/src/prosemirror/changeset.ts @@ -128,6 +128,7 @@ function addMissingChanges( } const createEncoder = (doc: Node, updatedDoc: Node) => { + // encoder ignores --attributed shadow nodes, diffs suggestion content // this encoder makes sure unchanged table cells stay intact, // without this, prosemirror-changeset would too eagerly // return changes across table cells (this is covered in test cases).