From 7399a46f50495baa0690e30d604315c442c819cd Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 8 Jun 2026 14:09:40 +0200 Subject: [PATCH 1/6] fix: base nodeviews on current doc --- packages/core/src/schema/blocks/createSpec.ts | 3 +- packages/core/src/schema/blocks/internal.ts | 38 ++++++------------- .../RelativePositionMapping.test.ts | 2 +- packages/react/src/schema/ReactBlockSpec.tsx | 3 +- 4 files changed, 15 insertions(+), 31 deletions(-) diff --git a/packages/core/src/schema/blocks/createSpec.ts b/packages/core/src/schema/blocks/createSpec.ts index 6df3e68aa4..577999f736 100644 --- a/packages/core/src/schema/blocks/createSpec.ts +++ b/packages/core/src/schema/blocks/createSpec.ts @@ -197,8 +197,7 @@ export function addNodeAndExtensionsToSpec< // Gets the block const block = getBlockFromPos( props.getPos, - editor, - this.editor, + props.view.state.doc, blockConfig.type, ); // Gets the custom HTML attributes for `blockContent` nodes diff --git a/packages/core/src/schema/blocks/internal.ts b/packages/core/src/schema/blocks/internal.ts index eed8cf9fa3..584badc819 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 { getBlock } from "../../api/blockManipulation/getBlock/getBlock.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 { BlockConfig, BlockFromConfig, LooseBlockSpec } from "./types.js"; // Function that uses the 'propSchema' of a blockConfig to create a TipTap // node's `addAttributes` property. @@ -85,22 +79,16 @@ export function propsToAttributes(propSchema: PropSchema): Attributes { 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, -) { +>(getPos: () => number | undefined, doc: PMNode, type: BType) { + // 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(); + const blockContainer = doc.resolve(pos).node(); // Gets block identifier const blockIdentifier = blockContainer.attrs.id; @@ -109,16 +97,14 @@ export function getBlockFromPos< } // Gets the block - const block = editor.getBlock(blockIdentifier)! as SpecificBlock< - BSchema, - BType, - I, - S + const block = getBlock(doc, blockIdentifier) as BlockFromConfig< + Config, + any, + any >; if (block.type !== type) { throw new Error("Block type does not match"); } - 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/react/src/schema/ReactBlockSpec.tsx b/packages/react/src/schema/ReactBlockSpec.tsx index 1ab1b43da8..a7f650a092 100644 --- a/packages/react/src/schema/ReactBlockSpec.tsx +++ b/packages/react/src/schema/ReactBlockSpec.tsx @@ -276,8 +276,7 @@ export function createReactBlockSpec< // `ReactNodeViewRenderer` instead. const block = getBlockFromPos( props.getPos, - editor, - props.editor, + props.view.state.doc, blockConfig.type, ); From f69633407a5de3a9d050e19b7ec789d0a30f3a73 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 8 Jun 2026 15:58:13 +0200 Subject: [PATCH 2/6] feat: decorations approach for y-prosemirror --- examples/07-collaboration/11-yhub/src/App.tsx | 36 +- packages/core/src/editor/Block.css | 60 +- .../src/y/extensions/YSuggestions.test.ts | 121 ++ .../core/src/y/extensions/YSuggestions.ts | 403 ++++ packages/core/src/y/extensions/YSync.ts | 89 - packages/core/src/y/extensions/index.ts | 2 + patches/@y__prosemirror@2.0.0-2.patch | 1750 +++++++++++------ pnpm-lock.yaml | 10 +- 8 files changed, 1805 insertions(+), 666 deletions(-) create mode 100644 packages/core/src/y/extensions/YSuggestions.test.ts create mode 100644 packages/core/src/y/extensions/YSuggestions.ts diff --git a/examples/07-collaboration/11-yhub/src/App.tsx b/examples/07-collaboration/11-yhub/src/App.tsx index 2008ff54f3..d87977cb2e 100644 --- a/examples/07-collaboration/11-yhub/src/App.tsx +++ b/examples/07-collaboration/11-yhub/src/App.tsx @@ -3,7 +3,11 @@ import "@blocknote/core/fonts/inter.css"; import "@blocknote/mantine/style.css"; import { BlockNoteView } from "@blocknote/mantine"; import { useCreateBlockNote } from "@blocknote/react"; -import { Awareness } from "@y/protocols/awareness"; +import { + Awareness, + encodeAwarenessUpdate, + applyAwarenessUpdate, +} from "@y/protocols/awareness"; import { withCollaboration } from "@blocknote/core/y"; import * as Y from "@y/y"; @@ -80,6 +84,36 @@ function setupTwoWaySync(doc1: Y.Doc, doc2: Y.Doc) { setupTwoWaySync(doc, doc2); setupTwoWaySync(suggestingDoc, suggestionModeDoc); +// Sync awareness states so cursors show up across editors +function setupAwarenessSync(a1: Awareness, a2: Awareness) { + // Initial sync + applyAwarenessUpdate( + a2, + encodeAwarenessUpdate(a1, [a1.clientID]), + "sync", + ); + applyAwarenessUpdate( + a1, + encodeAwarenessUpdate(a2, [a2.clientID]), + "sync", + ); + + a1.on("update", ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }) => { + const update = encodeAwarenessUpdate(a1, added.concat(updated).concat(removed)); + applyAwarenessUpdate(a2, update, "sync"); + }); + + a2.on("update", ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }) => { + const update = encodeAwarenessUpdate(a2, added.concat(updated).concat(removed)); + applyAwarenessUpdate(a1, update, "sync"); + }); +} + +setupAwarenessSync(provider.awareness, provider2.awareness); +setupAwarenessSync(suggestingProvider.awareness, suggestionModeProvider.awareness); +setupAwarenessSync(provider.awareness, suggestingProvider.awareness); +setupAwarenessSync(provider.awareness, suggestionModeProvider.awareness); + function Editor({ fragment, provider, diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index afb9222e64..10b096bcf2 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -33,8 +33,8 @@ BASIC STYLES transition: all 0.2s; /* Workaround for selection issue on Chrome, see #1588 and also here: https://discuss.prosemirror.net/t/mouse-down-selection-behaviour-different-on-chrome/8426 - The :before element causes the selection to be set in the wrong place vs - other browsers. Setting no height fixes this, while list item indicators are + The :before element causes the selection to be set in the wrong place vs + other browsers. Setting no height fixes this, while list item indicators are still displayed fine as overflow is not hidden. */ height: 0; overflow: visible; @@ -740,3 +740,59 @@ div[data-type="modification"] { text-decoration: line-through; text-decoration-thickness: 1px; } + +/* Suggestion decoration styling (data-diff-type drives everything) */ +[data-diff-type="inline-insert"], +[data-diff-type="block-insert"] { + background-color: color-mix( + in srgb, + var(--author-color, #28a745) 22%, + transparent + ); + text-decoration: underline; + text-decoration-color: var(--author-color, #28a745); + text-decoration-thickness: 2px; + border-radius: 2px; +} + +[data-diff-type="inline-delete"], +[data-diff-type="block-delete"] { + background-color: color-mix( + in srgb, + var(--author-color, #dc3545) 14%, + transparent + ); + text-decoration: line-through; + text-decoration-color: var(--author-color, #dc3545); + color: #555; + border-radius: 2px; +} +[data-diff-type="block-delete"] { + display: block; + padding: 2px 4px; + margin: 2px 0; +} +[data-diff-type="block-delete"] * { + text-decoration: line-through; + text-decoration-color: var(--author-color, #dc3545); + color: #555; +} +[data-diff-type="inline-delete"] { + padding: 0 1px; +} + +[data-diff-type="inline-update"], +[data-diff-type="block-update"] { + outline: 1.5px dashed var(--author-color, #ffc107); + outline-offset: 1px; + border-radius: 2px; +} + +[data-diff-type="block-delete"] strong, +[data-diff-type="inline-delete"] strong { + font-weight: normal; +} +[data-diff-type="block-delete"] em, +[data-diff-type="inline-delete"] em { + font-style: normal; +} diff --git a/packages/core/src/y/extensions/YSuggestions.test.ts b/packages/core/src/y/extensions/YSuggestions.test.ts new file mode 100644 index 0000000000..a66e249f81 --- /dev/null +++ b/packages/core/src/y/extensions/YSuggestions.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it } from "vitest"; +import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; +import { Fragment } from "prosemirror-model"; +import { findWrappingPath, wrapFragmentInDoc } from "./YSuggestions.js"; + +/** + * @vitest-environment jsdom + */ + +describe("findWrappingPath", () => { + it("finds path [doc, blockGroup, blockContainer] for a checkListItem node", () => { + const editor = BlockNoteEditor.create(); + const schema = editor.pmSchema; + + const checkListItem = schema.nodes.checkListItem.create( + { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + checked: false, + }, + [schema.text("ds")], + ); + + const path = findWrappingPath(schema, checkListItem); + + expect(path).not.toBeNull(); + expect(path!.map((t) => t.name)).toEqual([ + "doc", + "blockGroup", + "blockContainer", + ]); + }); + + it("finds path [doc, blockGroup, blockContainer] for a paragraph node", () => { + const editor = BlockNoteEditor.create(); + const schema = editor.pmSchema; + + const paragraph = schema.nodes.paragraph.create(null, [ + schema.text("hello"), + ]); + + const path = findWrappingPath(schema, paragraph); + + expect(path).not.toBeNull(); + expect(path!.map((t) => t.name)).toEqual([ + "doc", + "blockGroup", + "blockContainer", + ]); + }); +}); + +describe("wrapFragmentInDoc", () => { + it("wraps a fragment with a single checkListItem", () => { + const editor = BlockNoteEditor.create(); + const schema = editor.pmSchema; + + const checkListItem = schema.nodes.checkListItem.create( + { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + checked: false, + }, + [schema.text("ds")], + ); + const fragment = Fragment.from(checkListItem); + + const result = wrapFragmentInDoc(fragment, schema); + + expect(result).not.toBeNull(); + expect(result!.type.name).toBe("doc"); + // Walk down to verify the content + const blockGroup = result!.firstChild!; + expect(blockGroup.type.name).toBe("blockGroup"); + expect(blockGroup.childCount).toBe(1); + const bc = blockGroup.firstChild!; + expect(bc.type.name).toBe("blockContainer"); + expect(bc.firstChild!.type.name).toBe("checkListItem"); + expect(bc.firstChild!.textContent).toBe("ds"); + }); + + it("wraps a fragment with multiple block content nodes", () => { + const editor = BlockNoteEditor.create(); + const schema = editor.pmSchema; + + const item1 = schema.nodes.checkListItem.create( + { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + checked: false, + }, + [schema.text("first")], + ); + const item2 = schema.nodes.paragraph.create(null, [ + schema.text("second"), + ]); + const fragment = Fragment.from([item1, item2]); + + const result = wrapFragmentInDoc(fragment, schema); + + expect(result).not.toBeNull(); + expect(result!.type.name).toBe("doc"); + const blockGroup = result!.firstChild!; + expect(blockGroup.type.name).toBe("blockGroup"); + // Should have two blockContainers + expect(blockGroup.childCount).toBe(2); + expect(blockGroup.child(0).firstChild!.type.name).toBe("checkListItem"); + expect(blockGroup.child(0).firstChild!.textContent).toBe("first"); + expect(blockGroup.child(1).firstChild!.type.name).toBe("paragraph"); + expect(blockGroup.child(1).firstChild!.textContent).toBe("second"); + }); + + it("returns null for an empty fragment", () => { + const editor = BlockNoteEditor.create(); + const result = wrapFragmentInDoc(Fragment.empty, editor.pmSchema); + expect(result).toBeNull(); + }); +}); diff --git a/packages/core/src/y/extensions/YSuggestions.ts b/packages/core/src/y/extensions/YSuggestions.ts new file mode 100644 index 0000000000..1a6e0a8029 --- /dev/null +++ b/packages/core/src/y/extensions/YSuggestions.ts @@ -0,0 +1,403 @@ +import { Diff, MapDiffArgs, ySuggestionDecorationPlugin } from "@y/prosemirror"; +import { createExtension } from "../../editor/BlockNoteExtension.js"; +import { Decoration, DecorationAttrs, EditorView } from "prosemirror-view"; +import { + DOMSerializer, + Fragment, + Node, + NodeType, + Schema, + Slice, +} from "prosemirror-model"; +import { EditorState } from "prosemirror-state"; +import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; +import { prosemirrorSliceToSlicedBlocks } from "../../api/nodeConversions/nodeToBlock.js"; + +/** + * Reconstruct removed content as a non-editable DOM node by serializing the + * Fragment to HTML. Works for inline text and for whole block nodes alike. + */ +export const renderDeletedContent = ( + fragment: Fragment, + schema: Schema, + opts: { authorIds?: string[]; color?: string; title?: string } = {}, +): HTMLElement => { + const serializer = DOMSerializer.fromSchema(schema); + const isBlock = fragment?.firstChild?.isBlock ?? false; + const container = document.createElement(isBlock ? "div" : "span"); + container.className = "pm-suggest pm-suggest--delete"; + container.setAttribute( + "data-diff-type", + isBlock ? "block-delete" : "inline-delete", + ); + if (opts.authorIds?.length) { + container.setAttribute("data-diff-user-id", opts.authorIds.join(",")); + } + if (opts.color) { + container.style.setProperty("--author-color", opts.color); + } + if (opts.title) { + container.setAttribute("title", opts.title); + } + container.contentEditable = "false"; + if (fragment) { + container.appendChild(serializer.serializeFragment(fragment, { document })); + } + return container; +}; + +/** + * Build a human-readable hover title from diff attribution. + */ +const hoverTitle = (diff: Diff): string => { + const parts = []; + const authorIds = diff.attribution.authorIds; + if (authorIds.length) { + parts.push(authorIds.join(", ")); + } + if (diff.attribution.timestamp) { + parts.push(new Date(diff.attribution.timestamp).toLocaleString()); + } + const typeLabel = diff.type.replace("-", " "); + if (parts.length) { + return `${typeLabel}: ${parts.join(" — ")}`; + } + return typeLabel; +}; + +/** + * Build a summary string for a block-update diff showing what changed + * (e.g. "level: 1 → 2"). + */ +const blockUpdateSummary = (diff: Diff): string => { + if (diff.type !== "block-update") { + return ""; + } + const attrs = diff.attributes; + const prev = diff.previousAttributes; + if (!attrs) { + return ""; + } + const parts = []; + for (const key of Object.keys(attrs)) { + const newVal = attrs[key]; + const oldVal = prev?.[key]; + if (oldVal !== undefined && oldVal !== newVal) { + parts.push(`${key}: ${oldVal} → ${newVal}`); + } else { + parts.push(`${key}: ${newVal}`); + } + } + return parts.join(", "); +}; + +const decorationAttrs = ( + diff: Diff, + { authorIds, color }: { authorIds: string[]; color?: string }, +): DecorationAttrs => { + const attrs: DecorationAttrs = { + class: `pm-suggest pm-suggest--${diff.type}`, + "data-diff-type": diff.type, + }; + if (authorIds.length) { + attrs["data-diff-user-id"] = authorIds.join(","); + } + if (color) { + attrs.style = `--author-color: ${color}`; + } + // Hover metadata: show author(s), timestamp, and attribute changes + let title = hoverTitle(diff); + const summary = blockUpdateSummary(diff); + if (summary) { + title += ` (${summary})`; + } + attrs.title = title; + return attrs; +}; + +/** + * Find a chain of ancestor node types from `doc` down to a type that can + * contain `child`. Returns the path of NodeTypes (including doc at index 0) + * but NOT including the child's own type. + * + * Uses BFS from the doc type so the shortest wrapping wins. + * Returns null if no valid wrapping path exists. + */ +export const findWrappingPath = ( + schema: Schema, + child: Node, +): NodeType[] | null => { + const docType = schema.topNodeType; + + // BFS: each entry is a path of node types from doc → ... → parent of child + type Path = NodeType[]; + const queue: Path[] = [[docType]]; + const visited = new Set([docType.name]); + + while (queue.length > 0) { + const path = queue.shift()!; + const tip = path[path.length - 1]; + + // Check if this node type can contain `child` anywhere in its content expr + if (tip.contentMatch.matchType(child.type)) { + return path; + } + + // Expand: walk the content match automaton edges to find all node types + // this tip can contain, using the public edgeCount/edge API. + const match = tip.contentMatch; + const seenMatches = new Set(); + const matchQueue = [match]; + while (matchQueue.length > 0) { + const m = matchQueue.shift()!; + if (seenMatches.has(m)) { + continue; + } + seenMatches.add(m); + for (let i = 0; i < m.edgeCount; i++) { + const { type: edgeType, next } = m.edge(i); + if (!visited.has(edgeType.name)) { + visited.add(edgeType.name); + queue.push([...path, edgeType]); + } + // Follow the next match state to discover further edges + matchQueue.push(next); + } + } + } + + return null; +}; + +/** + * Wrap a fragment of nodes into a valid doc by finding the schema-required + * ancestor wrappers for each node. + */ +export const wrapFragmentInDoc = ( + fragment: Fragment, + schema: Schema, +): Node | null => { + const firstChild = fragment.firstChild; + if (!firstChild) { + return null; + } + + // Fast path: if doc can directly contain the fragment, just create it + const directDoc = schema.topNodeType.createAndFill(null, fragment); + if (directDoc) { + return directDoc; + } + + // Find the wrapping path: e.g. [doc, blockGroup, blockContainer] + const path = findWrappingPath(schema, firstChild); + if (!path || path.length === 0) { + return null; + } + + // The last type in the path directly contains our content nodes. + // Wrap each fragment child in that innermost type individually. + const innermostType = path[path.length - 1]; + const wrappedChildren: Node[] = []; + fragment.forEach((child) => { + const wrapped = innermostType.createAndFill(null, child); + if (wrapped) { + wrappedChildren.push(wrapped); + } + }); + + if (!wrappedChildren.length) { + return null; + } + + // Now wrap all children together through the remaining ancestor types, + // from second-to-last back to doc (index 0). + // e.g. [doc, blockGroup, blockContainer]: + // wrappedChildren = [blockContainer(child1), blockContainer(child2)] + // → blockGroup([blockContainer(child1), blockContainer(child2)]) + // → doc(blockGroup(...)) + let currentNodes: Node[] | Node = wrappedChildren; + for (let i = path.length - 2; i >= 0; i--) { + const wrapped = path[i].createAndFill(null, currentNodes); + if (!wrapped) { + return null; + } + currentNodes = wrapped; + } + + return currentNodes as Node; +}; + +/** + * Check whether any node in a fragment has a registered node view. + */ +const fragmentHasNodeView = ( + fragment: Fragment, + nodeViews: Record, +): boolean => { + let found = false; + fragment.forEach((node) => { + if (found) { + return; + } + if (nodeViews[node.type.name]) { + found = true; + return; + } + if (node.content.size > 0 && fragmentHasNodeView(node.content, nodeViews)) { + found = true; + } + }); + return found; +}; + +/** + * Default mapping from a single `Diff` to decoration(s). Returns a `Decoration`, + * an array of them, or `null` to skip. + */ +export const defaultMapDiffToDecorations = + (editor: BlockNoteEditor) => + ({ + diff, + doc, + schema, + index, + color, + attributes = {}, + }: MapDiffArgs): Decoration[] | Decoration | null => { + const authorIds = diff.attribution.authorIds; + const attrs = { + ...decorationAttrs(diff, { authorIds, color }), + ...attributes, + }; + const spec = { diff }; + + switch (diff.type) { + case "inline-insert": + case "inline-update": + return Decoration.inline(diff.from, diff.to, attrs, { + ...spec, + inclusiveStart: false, + inclusiveEnd: true, + }); + + case "block-update": + return Decoration.node(diff.from, diff.to, attrs, spec); + + case "block-insert": { + const $from = doc.resolve(diff.from); + const after = $from.nodeAfter; + if (after && diff.from + after.nodeSize === diff.to) { + return Decoration.node(diff.from, diff.to, attrs, spec); + } + + const decos: Decoration[] = []; + doc.nodesBetween(diff.from, diff.to, (node, pos) => { + if ( + pos >= diff.from && + pos + node.nodeSize <= diff.to && + node.isBlock + ) { + decos.push(Decoration.node(pos, pos + node.nodeSize, attrs, spec)); + return false; + } + return undefined; + }); + if (!decos.length) { + decos.push(Decoration.inline(diff.from, diff.to, attrs, spec)); + } + return decos; + } + + case "inline-delete": + return Decoration.widget( + diff.from, + () => + renderDeletedContent(diff.content ?? Fragment.empty, schema, { + authorIds, + color, + title: hoverTitle(diff), + }), + { + side: 1, + key: `diff-del-${index}-${diff.content?.size ?? 0}`, + diff, + }, + ); + + case "block-delete": { + const fragment = diff.content ?? Fragment.empty; + + let ghostView: EditorView | null = null; + return Decoration.widget( + diff.from, + (view) => { + const container = document.createElement("div"); + container.className = "pm-suggest pm-suggest--delete"; + container.setAttribute("data-diff-type", "block-delete"); + if (authorIds.length) { + container.setAttribute("data-diff-user-id", authorIds.join(",")); + } + if (color) { + container.style.setProperty("--author-color", color); + } + container.setAttribute("title", hoverTitle(diff)); + container.contentEditable = "false"; + if ( + fragment.size > 0 && + view.props.nodeViews && + fragmentHasNodeView(fragment, view.props.nodeViews) + ) { + const ghostDoc = wrapFragmentInDoc(fragment, schema); + + if (ghostDoc) { + const slicedBlocks = prosemirrorSliceToSlicedBlocks( + ghostDoc.slice(0, ghostDoc.nodeSize - 2), + editor.pmSchema, + ); + const htmlRepresentation = editor.blocksToFullHTML( + slicedBlocks.blocks, + ); + container.innerHTML = htmlRepresentation; + } else { + // Fallback: use DOMSerializer if wrapping failed + const serializer = DOMSerializer.fromSchema(schema); + container.appendChild( + serializer.serializeFragment(fragment, { document }), + ); + } + } else if (fragment.size > 0) { + const serializer = DOMSerializer.fromSchema(schema); + container.appendChild( + serializer.serializeFragment(fragment, { document }), + ); + } + return container; + }, + { + side: 1, + key: `diff-del-${index}-${fragment.size}`, + diff, + destroy: () => { + ghostView?.destroy(); + ghostView = null; + }, + }, + ); + } + + default: + return null; + } + }; + +export const YSuggestionPlugin = createExtension(({ editor }) => { + return { + key: "ySuggestion", + prosemirrorPlugins: [ + ySuggestionDecorationPlugin({ + mapDiffToDecorations: defaultMapDiffToDecorations(editor), + }), + ], + runsBefore: ["default"], + } as const; +}); diff --git a/packages/core/src/y/extensions/YSync.ts b/packages/core/src/y/extensions/YSync.ts index f4c9f73574..cca5919586 100644 --- a/packages/core/src/y/extensions/YSync.ts +++ b/packages/core/src/y/extensions/YSync.ts @@ -5,94 +5,6 @@ import { } from "../../editor/BlockNoteExtension.js"; import { CollaborationOptions } from "./index.js"; -/** - * Deterministic hash of a string to an unsigned 32-bit integer. - */ -const hashStr = (s: string): number => { - let h = 0; - for (let i = 0; i < s.length; i++) { - h = ((h << 5) - h + s.charCodeAt(i)) | 0; - } - return h >>> 0; -}; - -/** - * Pick a deterministic user-color from a palette based on user ids. - * Must be deterministic so the sync plugin's readback matches the mapper output. - */ -const userColorPalette = [ - "#30bced", - "#6eeb83", - "#ffbc42", - "#ecd444", - "#ee6352", - "#9ac2c9", - "#8acb88", - "#1be7ff", -]; - -const colorForUserIds = ( - userIds: readonly string[] | undefined | null, -): string => { - if (!userIds || userIds.length === 0) { - return userColorPalette[0]; - } - return userColorPalette[ - hashStr(String(userIds[0])) % userColorPalette.length - ]; -}; - -/** - * Map a Y attribution to BlockNote's `y-attributed-*` mark attrs. - * - * The mapper must be deterministic in `(format, attribution)` and emit - * attrs that exactly match the declared mark schema in SuggestionMarks.ts. - * Any mismatch causes the sync plugin to fire phantom reconcile dispatches - * in a loop. See ATTRIBUTION.md in @y/prosemirror. - * - * Declared attrs per mark (all three are the same shape): - * - y-attributed-insert: { id, "user-color" } - * - y-attributed-delete: { id, "user-color" } - * - y-attributed-format: { id, "user-color" } - */ -const mapAttributionToMark = ( - format: Record | null, - attribution: { - insert?: readonly string[]; - delete?: readonly string[]; - format?: Record; - insertAt?: number; - deleteAt?: number; - formatAt?: number; - }, -): Record => { - const out: Record = { ...format }; - - if (attribution.insert) { - out["y-attributed-insert"] = { - id: attribution.insert[0] ?? null, - "user-color": colorForUserIds(attribution.insert), - }; - } - - if (attribution.delete) { - out["y-attributed-delete"] = { - id: attribution.delete[0] ?? null, - "user-color": colorForUserIds(attribution.delete), - }; - } - - if (attribution.format) { - const userIds = [...new Set(Object.values(attribution.format).flat())]; - out["y-attributed-format"] = { - id: userIds[0] ?? null, - "user-color": colorForUserIds(userIds), - }; - } - - return out; -}; - export const YSyncExtension = createExtension( ({ options, @@ -117,7 +29,6 @@ export const YSyncExtension = createExtension( prosemirrorPlugins: [ syncPlugin({ suggestionDoc: options.suggestionDoc, - mapAttributionToMark, }), ], runsBefore: ["default"], diff --git a/packages/core/src/y/extensions/index.ts b/packages/core/src/y/extensions/index.ts index fe137197db..047e787a21 100644 --- a/packages/core/src/y/extensions/index.ts +++ b/packages/core/src/y/extensions/index.ts @@ -7,6 +7,7 @@ import { import { RelativePositionMappingExtension } from "./RelativePositionMapping.js"; import { CollaborationUser, YCursorExtension } from "./YCursorPlugin.js"; import { YSyncExtension } from "./YSync.js"; +import { YSuggestionPlugin } from "./YSuggestions.js"; import { BlockNoteEditorOptions } from "../../editor/BlockNoteEditor.js"; import { SuggestionsExtension } from "./Suggestions.js"; import { createYjsVersioningAdapter } from "./Versioning.js"; @@ -66,6 +67,7 @@ export const CollaborationExtension = createExtension( RelativePositionMappingExtension(), YSyncExtension(options), YCursorExtension(options), + YSuggestionPlugin(), options.versioningEndpoints ? VersioningExtension({ ...createYjsVersioningAdapter(editor, options.fragment), diff --git a/patches/@y__prosemirror@2.0.0-2.patch b/patches/@y__prosemirror@2.0.0-2.patch index dab913697b..7182f6d842 100644 --- a/patches/@y__prosemirror@2.0.0-2.patch +++ b/patches/@y__prosemirror@2.0.0-2.patch @@ -1,16 +1,10 @@ diff --git a/dist/src/commands.d.ts b/dist/src/commands.d.ts new file mode 100644 -index 0000000000000000000000000000000000000000..a12f7150273c27fef6621b685a608c0c13f0eefa +index 0000000000000000000000000000000000000000..52db229917ea97da2d45900b873ba416d5c58c41 --- /dev/null +++ b/dist/src/commands.d.ts -@@ -0,0 +1,27 @@ -+/** -+ * Switch to pause mode (stop synchronization between prosemirror and ytype) -+ * @param {import('prosemirror-state').EditorState} state -+ * @param {CommandDispatch?} dispatch -+ * @returns {boolean} -+ */ -+export function pauseSync(state: import("prosemirror-state").EditorState, dispatch: CommandDispatch | null): boolean; +@@ -0,0 +1,21 @@ ++export function pauseSync(state: import("prosemirror-state").EditorState, dispatch?: (tr: import("prosemirror-state").Transaction) => void, view?: import("prosemirror-view").EditorView): boolean; +export function configureYProsemirror(opts?: { + ytype?: Y.Type | null | undefined; + attributionManager?: Y.AbstractAttributionManager | null | undefined; @@ -34,11 +28,11 @@ index 0000000000000000000000000000000000000000..a12f7150273c27fef6621b685a608c0c \ No newline at end of file diff --git a/dist/src/commands.d.ts.map b/dist/src/commands.d.ts.map new file mode 100644 -index 0000000000000000000000000000000000000000..817e319bd77f9d07a25146614a47636171902b1f +index 0000000000000000000000000000000000000000..c20bcd20ca4fc83c03acbb1a846fee773532158f --- /dev/null +++ b/dist/src/commands.d.ts.map @@ -0,0 +1 @@ -+{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/commands.js"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,iCAJW,OAAO,mBAAmB,EAAE,WAAW,YACvC,eAAe,OAAC,GACd,OAAO,CAanB;AAeM,6CAJJ;IAAsB,KAAK;IACQ,kBAAkB;CACrD,GAAU,OAAO,mBAAmB,EAAE,OAAO,CA8B/C;AAQM,4BAHI,OAAO,mBAAmB,EAAE,WAAW,GACtC,OAAO,CAEqE;AAQjF,4BAHI,OAAO,mBAAmB,EAAE,WAAW,GACtC,OAAO,CAEqE;AAExF;;GAEG;AACH,0BAFU,OAAO,mBAAmB,EAAE,OAAO,CAEqG;AAElJ;;GAEG;AACH,0BAFU,OAAO,mBAAmB,EAAE,OAAO,CAEqG;AAQ3I,qCAJI,MAAM,QACN,MAAM,GACJ,OAAO,mBAAmB,EAAE,OAAO,CAc/C;AAQM,qCAJI,MAAM,QACN,MAAM,GACJ,OAAO,mBAAmB,EAAE,OAAO,CAc/C;AAMM,oCAFM,OAAO,mBAAmB,EAAE,OAAO,CAW/C;AAMM,oCAFM,OAAO,mBAAmB,EAAE,OAAO,CAW/C;mBA/JkB,MAAM"} ++{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/commands.js"],"names":[],"mappings":";AAkCO,6CAJJ;IAA6B,KAAK;IACiB,kBAAkB;CACrE,GAAU,OAAO,mBAAmB,EAAE,OAAO,CAyB/C;AAQM,4BAHI,OAAO,mBAAmB,EAAE,WAAW,GACtC,OAAO,CAEqE;AAQjF,4BAHI,OAAO,mBAAmB,EAAE,WAAW,GACtC,OAAO,CAEqE;AAExF;;GAEG;AACH,0BAFU,OAAO,mBAAmB,EAAE,OAAO,CAEqG;AAElJ;;GAEG;AACH,0BAFU,OAAO,mBAAmB,EAAE,OAAO,CAEqG;AAQ3I,qCAJI,MAAM,QACN,MAAM,GACJ,OAAO,mBAAmB,EAAE,OAAO,CAc/C;AAQM,qCAJI,MAAM,QACN,MAAM,GACJ,OAAO,mBAAmB,EAAE,OAAO,CAc/C;AAMM,oCAFM,OAAO,mBAAmB,EAAE,OAAO,CAW/C;AAMM,oCAFM,OAAO,mBAAmB,EAAE,OAAO,CAW/C;mBAtJkB,MAAM"} \ No newline at end of file diff --git a/dist/src/cursor-plugin.d.ts b/dist/src/cursor-plugin.d.ts new file mode 100644 @@ -99,11 +93,73 @@ index 0000000000000000000000000000000000000000..f09b4e94cfb42585d13b700cef3f4fb0 @@ -0,0 +1 @@ +{"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/diff-decorations.d.ts b/dist/src/diff-decorations.d.ts +new file mode 100644 +index 0000000000000000000000000000000000000000..66c8346cd99337a1087c50dec735ed0f1f897e3e +--- /dev/null ++++ b/dist/src/diff-decorations.d.ts +@@ -0,0 +1,47 @@ ++export function renderDeletedContent(fragment: import("prosemirror-model").Fragment, schema: import("prosemirror-model").Schema, opts?: { ++ authorIds?: string[]; ++ color?: string; ++ title?: string; ++}): HTMLElement; ++/** ++ * Default mapping from a single `Diff` to decoration(s). Returns a `Decoration`, ++ * an array of them, or `null` to skip. ++ * ++ * @type {MapDiffToDecorations} ++ */ ++export const defaultMapDiffToDecorations: MapDiffToDecorations; ++export function buildDiffDecorationSet(doc: import("prosemirror-model").Node, diffs: DiffSet, schema: import("prosemirror-model").Schema, opts?: SuggestionDecorationOptions): DecorationSet; ++export function suggestionDiffPlugin({ diffs, mapDiffToDecorations, colorForAuthors }?: SuggestionDecorationOptions & { ++ diffs?: DiffSet; ++}): Plugin; ++export type Diff = import("./y-attribution-to-diffset.js").Diff; ++export type DiffSet = import("./y-attribution-to-diffset.js").DiffSet; ++export type DiffType = import("./y-attribution-to-diffset.js").DiffType; ++export type Attribution = import("./y-attribution-to-diffset.js").Attribution; ++/** ++ * Arguments passed to a `mapDiffToDecorations` callback. ++ */ ++export type MapDiffArgs = { ++ diff: Diff; ++ doc: import("prosemirror-model").Node; ++ schema: import("prosemirror-model").Schema; ++ index: number; ++ color?: string; ++ attributes?: import("prosemirror-view").DecorationAttrs; ++ defaultMapDiffToDecorations?: MapDiffToDecorations; ++}; ++/** ++ * Callback that converts a single `Diff` to decoration(s). ++ * Return a `Decoration`, an array of them, or `null` to skip. ++ */ ++export type MapDiffToDecorations = (args: MapDiffArgs) => import("prosemirror-view").Decoration | import("prosemirror-view").Decoration[] | null; ++/** ++ * Options shared by `buildDiffDecorationSet` and `ySuggestionDecorationPlugin`. ++ */ ++export type SuggestionDecorationOptions = { ++ colorForAuthors?: (authorIds: string[]) => (string | undefined); ++ mapDiffToDecorations?: MapDiffToDecorations; ++}; ++import { DecorationSet } from 'prosemirror-view'; ++import { Plugin } from 'prosemirror-state'; ++//# sourceMappingURL=diff-decorations.d.ts.map +\ No newline at end of file +diff --git a/dist/src/diff-decorations.d.ts.map b/dist/src/diff-decorations.d.ts.map +new file mode 100644 +index 0000000000000000000000000000000000000000..fda53e2c6b828544e6abf535b3851a0e755d8d81 +--- /dev/null ++++ b/dist/src/diff-decorations.d.ts.map +@@ -0,0 +1 @@ ++{"version":3,"file":"diff-decorations.d.ts","sourceRoot":"","sources":["../../src/diff-decorations.js"],"names":[],"mappings":"AAkEO,+CALI,OAAO,mBAAmB,EAAE,QAAQ,UACpC,OAAO,mBAAmB,EAAE,MAAM,SAClC;IAAE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACtD,WAAW,CAsBvB;AAoED;;;;;GAKG;AACH,0CAFU,oBAAoB,CA+C7B;AAWM,4CANI,OAAO,mBAAmB,EAAE,IAAI,SAChC,OAAO,UACP,OAAO,mBAAmB,EAAE,MAAM,SAClC,2BAA2B,GACzB,aAAa,CAazB;AAWM,wFAHI,2BAA2B,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAC/C,MAAM,CAAC,aAAa,CAAC,CAmB9B;mBA5OS,OAAO,+BAA+B,EAAE,IAAI;sBAC5C,OAAO,+BAA+B,EAAE,OAAO;uBAC/C,OAAO,+BAA+B,EAAE,QAAQ;0BAChD,OAAO,+BAA+B,EAAE,WAAW;;;;0BAMnD;IACR,IAAI,EAAE,IAAI,CAAC;IACX,GAAG,EAAE,OAAO,mBAAmB,EAAE,IAAI,CAAC;IACtC,MAAM,EAAE,OAAO,mBAAmB,EAAE,MAAM,CAAC;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,kBAAkB,EAAE,eAAe,CAAC;IACxD,2BAA2B,CAAC,EAAE,oBAAoB,CAAA;CACnD;;;;;0CAQO,WAAW,KACT,OAAO,kBAAkB,EAAE,UAAU,GAAG,OAAO,kBAAkB,EAAE,UAAU,EAAE,GAAG,IAAI;;;;0CAMtF;IACR,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAChE,oBAAoB,CAAC,EAAE,oBAAoB,CAAA;CAC5C;8BAzCsC,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..6191cc561c6e913f46921419bcc5c88b9f162a96 100644 --- a/dist/src/index.d.ts +++ b/dist/src/index.d.ts -@@ -1,84 +1,8 @@ +@@ -1,84 +1,18 @@ -/** - * @param {Y.XmlFragment} ytype - * @param {object} opts @@ -194,23 +250,34 @@ 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 { ydeltaToDiffSet } from "./y-attribution-to-diffset.js"; ++export { ySuggestionDecorationPlugin } from "./suggestion-decoration-plugin.js"; ++export type Attribution = import("./y-attribution-to-diffset.js").Attribution; ++export type Diff = import("./y-attribution-to-diffset.js").Diff; ++export type DiffType = import("./y-attribution-to-diffset.js").DiffType; ++export type DiffSet = import("./y-attribution-to-diffset.js").DiffSet; ++export type SuggestionDecorationOptions = import("./diff-decorations.js").SuggestionDecorationOptions; ++export type MapDiffToDecorations = import("./diff-decorations.js").MapDiffToDecorations; ++export type MapDiffArgs = import("./diff-decorations.js").MapDiffArgs; ++export { docToDelta, $prosemirrorDelta } from "./sync-utils.js"; ++export { buildDiffDecorationSet, suggestionDiffPlugin, renderDeletedContent, defaultMapDiffToDecorations } from "./diff-decorations.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 new file mode 100644 -index 0000000000000000000000000000000000000000..4b136e26cf4d54488bfbbaf749a89197c074cd91 +index 0000000000000000000000000000000000000000..d131ae17f5d02df2af137cb93c380689b1e5464d --- /dev/null +++ b/dist/src/index.d.ts.map @@ -0,0 +1 @@ -+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.js"],"names":[],"mappings":""} ++{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.js"],"names":[],"mappings":";;;;;;;;0BAQc,OAAO,+BAA+B,EAAE,WAAW;mBACnD,OAAO,+BAA+B,EAAE,IAAI;uBAC5C,OAAO,+BAA+B,EAAE,QAAQ;sBAChD,OAAO,+BAA+B,EAAE,OAAO;0CAE/C,OAAO,uBAAuB,EAAE,2BAA2B;mCAC3D,OAAO,uBAAuB,EAAE,oBAAoB;0BACpD,OAAO,uBAAuB,EAAE,WAAW"} \ No newline at end of file diff --git a/dist/src/keys.d.ts b/dist/src/keys.d.ts new file mode 100644 -index 0000000000000000000000000000000000000000..e60986981f3d3835d7842915790cc6df50f4f1e7 +index 0000000000000000000000000000000000000000..0b8d192e3e3099785c3bda2450b923ee540b4b8d --- /dev/null +++ b/dist/src/keys.d.ts -@@ -0,0 +1,23 @@ +@@ -0,0 +1,39 @@ ++/** @typedef {import('lib0/schema').Unwrap} SyncPluginState */ +/** + * The unique prosemirror plugin key for {@link import('./sync-plugin.js').syncPlugin} + * @@ -232,16 +299,31 @@ index 0000000000000000000000000000000000000000..e60986981f3d3835d7842915790cc6df + * @type {PluginKey} + */ +export const yCursorPluginKey: PluginKey; ++/** ++ * The unique prosemirror plugin key for {@link import('./suggestion-decoration-plugin.js').ySuggestionDecorationPlugin} ++ * ++ * @public ++ * @type {PluginKey} ++ */ ++export const ySuggestionDecorationPluginKey: PluginKey; ++/** ++ * The unique prosemirror plugin key for {@link import('./diff-decorations.js').suggestionDiffPlugin} ++ * ++ * @public ++ * @type {PluginKey} ++ */ ++export const suggestionDiffPluginKey: PluginKey; ++export type SyncPluginState = import("lib0/schema").Unwrap; +import { PluginKey } from 'prosemirror-state'; +//# sourceMappingURL=keys.d.ts.map \ No newline at end of file diff --git a/dist/src/keys.d.ts.map b/dist/src/keys.d.ts.map new file mode 100644 -index 0000000000000000000000000000000000000000..9f12f341c63e7ae2bd51640eefd3df47015b4398 +index 0000000000000000000000000000000000000000..a2f34d3f21d0b22d2d1f07fd1c1d77841fc14665 --- /dev/null +++ b/dist/src/keys.d.ts.map @@ -0,0 +1 @@ -+{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/keys.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,6BAFU,SAAS,CAAC,eAAe,CAAC,CAEiB;AAErD;;;;;GAKG;AACH,6BAFU,SAAS,CAAC,OAAO,kBAAkB,EAAE,eAAe,CAAC,CAEV;AAErD;;;;;GAKG;AACH,+BAFU,SAAS,CAAC,OAAO,kBAAkB,EAAE,aAAa,CAAC,CAEJ;0BAxB/B,mBAAmB"} ++{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/keys.js"],"names":[],"mappings":"AAEA,kHAAkH;AAElH;;;;;GAKG;AACH,6BAFU,SAAS,CAAC,eAAe,CAAC,CAEiB;AAErD;;;;;GAKG;AACH,6BAFU,SAAS,CAAC,OAAO,kBAAkB,EAAE,eAAe,CAAC,CAEV;AAErD;;;;;GAKG;AACH,+BAFU,SAAS,CAAC,OAAO,kBAAkB,EAAE,aAAa,CAAC,CAEJ;AAEzD;;;;;GAKG;AACH,6CAFU,SAAS,CAAC,OAAO,kBAAkB,EAAE,aAAa,CAAC,CAE0B;AAEvF;;;;;GAKG;AACH,sCAFU,SAAS,CAAC,OAAO,kBAAkB,EAAE,aAAa,CAAC,CAEU;8BAxCzD,OAAO,aAAa,EAAE,MAAM,CAAC,cAAc,kBAAkB,EAAE,gBAAgB,CAAC;0BAFpE,mBAAmB"} \ No newline at end of file diff --git a/dist/src/lib.d.ts b/dist/src/lib.d.ts deleted file mode 100644 @@ -284,14 +366,35 @@ index 0000000000000000000000000000000000000000..e4f768c579f11b08055a31cc166e8c34 @@ -0,0 +1 @@ +{"version":3,"file":"positions.d.ts","sourceRoot":"","sources":["../../src/positions.js"],"names":[],"mappings":"AAWO,gEALI,OAAO,mBAAmB,EAAE,WAAW,QACvC,CAAC,CAAC,IAAI,OACN,CAAC,CAAC,0BAA0B,GAAG,IAAI,GAClC,CAAC,CAAC,gBAAgB,CA6C7B;AAUM,2DANI,CAAC,CAAC,gBAAgB,gBAClB,CAAC,CAAC,IAAI,SACN,OAAO,mBAAmB,EAAE,IAAI,OAChC,CAAC,CAAC,0BAA0B,GAAG,IAAI,GAClC,IAAI,GAAC,MAAM,CAmDtB;AASM,mDALI,OAAO,mBAAmB,EAAE,WAAW,QACvC,CAAC,CAAC,IAAI,OACN,CAAC,CAAC,0BAA0B,GAC1B,CAAC,GAAG,EAAE,OAAO,mBAAmB,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,kBAAkB,CAAC,EAAE,CAAC,CAAC,0BAA0B,KAAK,MAAM,CAWvI;AAyBM,mDAHI,CAAC,CAAC,IAAI,GACJ;IAAC,cAAc,EAAE,cAAc,CAAC;IAAC,cAAc,EAAE,cAAc,CAAA;CAAC,CAyD5E;mCA5EU,OAAO,mBAAmB,EAAE,IAAI,wFAG9B,OAAO,uBAAuB,EAAE,QAAQ;oCAK1C,CAAC,CAAC,IAAI,SACN,OAAO,mBAAmB,EAAE,IAAI,2DAE9B,OAAO,uBAAuB,EAAE,QAAQ;mBAlJlC,MAAM"} \ No newline at end of file +diff --git a/dist/src/suggestion-decoration-plugin.d.ts b/dist/src/suggestion-decoration-plugin.d.ts +new file mode 100644 +index 0000000000000000000000000000000000000000..9c8a6d99d690d36a8d01af6b0542b8605ef269d7 +--- /dev/null ++++ b/dist/src/suggestion-decoration-plugin.d.ts +@@ -0,0 +1,5 @@ ++export function ySuggestionDecorationPlugin(opts?: SuggestionDecorationOptions): Plugin; ++export type SuggestionDecorationOptions = import("./diff-decorations.js").SuggestionDecorationOptions; ++import { Plugin } from 'prosemirror-state'; ++import { DecorationSet } from 'prosemirror-view'; ++//# sourceMappingURL=suggestion-decoration-plugin.d.ts.map +\ No newline at end of file +diff --git a/dist/src/suggestion-decoration-plugin.d.ts.map b/dist/src/suggestion-decoration-plugin.d.ts.map +new file mode 100644 +index 0000000000000000000000000000000000000000..29116f660b11e7587cbea06b6b4cf9719f0fa228 +--- /dev/null ++++ b/dist/src/suggestion-decoration-plugin.d.ts.map +@@ -0,0 +1 @@ ++{"version":3,"file":"suggestion-decoration-plugin.d.ts","sourceRoot":"","sources":["../../src/suggestion-decoration-plugin.js"],"names":[],"mappings":"AAiDO,mDAHI,2BAA2B,GACzB,MAAM,CAAC,aAAa,CAAC,CA2B9B;0CAvDU,OAAO,uBAAuB,EAAE,2BAA2B;uBALlD,mBAAmB;8BACZ,kBAAkB"} +\ 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..229de152a175bfe747471b352465b9545e9bbb26 --- /dev/null +++ b/dist/src/sync-plugin.d.ts -@@ -0,0 +1,41 @@ +@@ -0,0 +1,39 @@ +/** -+ * This Prosemirror {@link Plugin} is responsible for synchronizing the prosemirror {@link EditorState} with a {@link Y.XmlFragment} ++ * This Prosemirror {@link Plugin} is responsible for synchronizing the prosemirror ++ * {@link import('prosemirror-state').EditorState} with a {@link Y.XmlFragment}. + * + * The PM->Y diff/apply pipeline runs in the plugin's `view().update` + * hook (i.e. after the dispatch has been committed to the view), not @@ -299,53 +402,50 @@ index 0000000000000000000000000000000000000000..c1da2aa33b86511936e9b1ba4d2d3c84 + * cause speculative `state.apply` callers to write to Y as a side + * effect. + * -+ * @param {object} opts -+ * @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)`. ++ * The PM document always mirrors the **clean** Y content (no attribution ++ * marks, no deleted text). The write path applies diffs through the AM ++ * so edits are tagged as suggestions. Attribution rendering is handled ++ * by the separate {@link import('./suggestion-decoration-plugin.js').ySuggestionDecorationPlugin}. ++ * ++ * @param {object} [opts] + * @returns {Plugin} + */ -+export function syncPlugin(opts?: { -+ suggestionDoc?: Y.Doc | undefined; -+ mapAttributionToMark?: AttributionMapper | undefined; -+ attributedNodes?: AttributedNodesPredicate | undefined; -+}): Plugin; ++export function syncPlugin(opts?: object): Plugin; +/** -+ * The y-prosemirror binding is a bi-directional synchronization with the provided Y.Type and the EditorView ++ * The y-prosemirror binding is a bi-directional synchronization with the provided Y.Type and the EditorView. + * Any change applied to the EditorView will be applied (via deltas) to the Y.Type, and vice versa. ++ * ++ * The PM document always contains **clean** content — no attribution marks, ++ * no deleted text inline. Attribution is rendered as decorations by ++ * {@link import('./suggestion-decoration-plugin.js').ySuggestionDecorationPlugin}. + */ +export const $syncPluginState: s.Schema<{ + ytype: Y.Type | null; + attributionManager: Y.AbstractAttributionManager | null; -+ attributionMapper: AttributionMapper; -+ attributedNodes: AttributedNodesPredicate; +}>; +export const $syncPluginStateUpdate: s.Schema<{ + ytype?: Y.Type | null | undefined; + attributionManager?: Y.AbstractAttributionManager | null | undefined; -+ attributionMapper?: AttributionMapper | null | undefined; -+ attributedNodes?: AttributedNodesPredicate | null | undefined; -+ change?: Y.YEvent | null | undefined; +}>; -+import * as Y from '@y/y'; +import { Plugin } from 'prosemirror-state'; ++import * as Y from '@y/y'; +import * as s from 'lib0/schema'; +//# sourceMappingURL=sync-plugin.d.ts.map \ 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..257913a7524665058045da2e87779eaf0563fc18 --- /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":"AAgCA;;;;;;;;;;;;;;;;;GAiBG;AACH,kCAHW,MAAM,GACJ,MAAM,CAkKlB;AArMD;;;;;;;GAOG;AACH;;;GAGE;AAEF;;;GAGE;uBA5BqB,mBAAmB;mBADvB,MAAM;mBAUN,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..d57c7fe0a258d821eb58d615bd93fa3a1deffcf1 --- /dev/null +++ b/dist/src/sync-utils.d.ts -@@ -0,0 +1,146 @@ +@@ -0,0 +1,105 @@ +/** + * Transforms a {@link Node} into a {@link Y.XmlFragment} + * @param {Node} node @@ -358,28 +458,6 @@ index 0000000000000000000000000000000000000000..dfb00a847adcc5a1db01d557a8b0b056 + attributionManager?: Y.AbstractAttributionManager | undefined; +}): Y.Type; +/** -+ * Applies a {@link Y.XmlFragment}'s content as a ProseMirror {@link Transaction} -+ * @param {Y.Type} fragment -+ * @param {import('prosemirror-state').Transaction} tr -+ * @param {object} ctx -+ * @param {Y.AbstractAttributionManager} [ctx.attributionManager] -+ * @param {typeof defaultMapAttributionToMark} [ctx.mapAttributionToMark] -+ * @param {AttributedNodesPredicate} [ctx.attributedNodes] -+ * @returns {import('prosemirror-state').Transaction} -+ */ -+export function fragmentToTr(fragment: Y.Type, tr: import("prosemirror-state").Transaction, { attributionManager, mapAttributionToMark, attributedNodes }?: { -+ attributionManager?: Y.AbstractAttributionManager | undefined; -+ mapAttributionToMark?: ((format: Record | null, attribution: T) => Record | null) | undefined; -+ attributedNodes?: AttributedNodesPredicate | undefined; -+}): import("prosemirror-state").Transaction; -+/** -+ * Transforms a {@link Y.XmlFragment} into a {@link Node} -+ * @param {Y.Type} fragment -+ * @param {import('prosemirror-state').Transaction} tr -+ * @return {Node} -+ */ -+export function fragmentToPm(fragment: Y.Type, tr: import("prosemirror-state").Transaction): Node; -+/** + * This function is used to find the delta offset for a given prosemirror offset in a node. + * Given the following document: + *

Hello world

Hello world!

@@ -413,31 +491,11 @@ index 0000000000000000000000000000000000000000..dfb00a847adcc5a1db01d557a8b0b056 + text: true; + recursiveChildren: true; +}>>; -+/** -+ * Suffix appended to a node name when it is rendered as its "attributed -+ * variant" (see `attributedNodes` on {@link syncPlugin}). The suffix is fixed -+ * so that canonicalizing back (PM -> Y) is a pure string operation and can -+ * never drift from the forward mapping. `--attributed` is a *reserved* suffix: -+ * a real node type literally ending in it would be canonicalized away on the -+ * way to Y. -+ */ -+export const ATTRIBUTED_SUFFIX: "--attributed"; -+/** -+ * Default `attributedNodes` predicate - the feature is off, so every node keeps -+ * its canonical name. -+ * -+ * @type {AttributedNodesPredicate} -+ */ -+export const defaultAttributedNodes: AttributedNodesPredicate; -+export function canonicalNodeName(name: string): string; -+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 formattingAttributesToMarks(formatting: { + [key: string]: any; +} | null, schema: import("prosemirror-model").Schema): import("prosemirror-model").Mark[]; +export function nodesToDelta(ns: Array): ProsemirrorDelta; -+export function nodeToDelta(n: Node, nodeName?: string | null, canonicalize?: boolean): ProsemirrorDelta; ++export function nodeToDelta(n: Node, nodeName?: string | null): ProsemirrorDelta; +export function docToDelta(doc: Node): delta.Delta<{ + name: string; + attrs: { @@ -448,8 +506,8 @@ index 0000000000000000000000000000000000000000..dfb00a847adcc5a1db01d557a8b0b056 +}>; +export function deltaToPSteps(tr: import("prosemirror-state").Transaction, d: ProsemirrorDelta, pnode?: Node, currPos?: { + i: number; -+}, attributedNodes?: AttributedNodesPredicate): import("prosemirror-state").Transaction; -+export function deltaToPNode(d: ProsemirrorDelta, schema: import("prosemirror-model").Schema, dformat: delta.FormattingAttributes | null, attributedNodes?: AttributedNodesPredicate): Node; ++}): import("prosemirror-state").Transaction; ++export function deltaToPNode(d: ProsemirrorDelta, schema: import("prosemirror-model").Schema, dformat: delta.FormattingAttributes | null): Node; +export function docDiffToDelta(beforeDoc: Node, afterDoc: Node): delta.Delta<{ + name: string; + attrs: { @@ -458,7 +516,7 @@ index 0000000000000000000000000000000000000000..dfb00a847adcc5a1db01d557a8b0b056 + text: true; + recursiveChildren: true; +}>; -+export function trToDelta(tr: Transaction): delta.Delta<{ ++export function trToDelta(tr: import("prosemirror-state").Transaction): delta.Delta<{ + name: string; + attrs: { + [x: string]: any; @@ -468,6 +526,7 @@ index 0000000000000000000000000000000000000000..dfb00a847adcc5a1db01d557a8b0b056 +}>; +export function stepToDelta(step: import("prosemirror-transform").Step, beforeDoc: import("prosemirror-model").Node): ProsemirrorDelta; +export function deltaModifyNodeAt(node: Node, pmOffset: number, mod: (d: delta.DeltaBuilderAny) => any): ProsemirrorDelta; ++export type ProsemirrorDelta = import("lib0/schema").Unwrap; +/** + * A single child op of a {@link ProsemirrorDelta} (retain / modify / insert / + * text / delete). @@ -495,11 +554,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..f5b016e30248aa8b3d25153344d28b1acbc52386 --- /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":"AA+DA;;;;;;;GAOG;AACH,mCANW,IAAI,YACJ,CAAC,CAAC,IAAI,2BAEd;IAA4C,kBAAkB;CAC9D,GAAU,CAAC,CAAC,IAAI,CAOlB;AA+XD;;;;;;;;;;;;;;;;;GAiBG;AACH,oCAJW,IAAI,mBACJ,MAAM,GACL,MAAM,EAAE,CA2BnB;AAED;;;;;GAKG;AACH,yCAJW,MAAM,EAAE,QACR,IAAI,GACH,MAAM,CAsCjB;AAjhBD;;;;;;;IAA4I;AA4BrI,wDAHI;IAAC,CAAC,GAAG,EAAC,MAAM,GAAE,GAAG,CAAA;CAAC,GAAC,IAAI,UACvB,OAAO,mBAAmB,EAAE,MAAM,sCAGwD;AAM9F,iCAHI,KAAK,CAAC,IAAI,CAAC,GACV,gBAAgB,CAW3B;AAsBM,+BAJI,IAAI,aACJ,MAAM,OAAC,GACN,gBAAgB,CAS3B;AAKM,gCAFI,IAAI;;;;;;;GAEwC;AA2ChD,kCANI,OAAO,mBAAmB,EAAE,WAAW,KACvC,gBAAgB,UAChB,IAAI,YACJ;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GACZ,OAAO,mBAAmB,EAAE,WAAW,CA0JlD;AAQM,gCALI,gBAAgB,UAChB,OAAO,mBAAmB,EAAE,MAAM,WAClC,KAAK,CAAC,oBAAoB,GAAC,IAAI,GAC9B,IAAI,CA6Bf;AAMM,0CAHI,IAAI,YACJ,IAAI;;;;;;;GAMd;AAKM,8BAFI,OAAO,mBAAmB,EAAE,WAAW;;;;;;;GAkBjD;AAmFM,kCAJI,OAAO,uBAAuB,EAAE,IAAI,aACpC,OAAO,mBAAmB,EAAE,IAAI,GAC/B,gBAAgB,CAQ3B;AA6GM,wCALI,IAAI,YACJ,MAAM,OACN,CAAC,CAAC,EAAC,KAAK,CAAC,eAAe,KAAG,GAAG,GAC7B,gBAAgB,CAa3B;+BAliBa,OAAO,aAAa,EAAE,MAAM,CAAC,OAAO,iBAAiB,CAAC;;;;;iCAoGvD,KAAK,CAAC,aAAa;;;;;;;;;aAQlB,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAC,KAAK,CAAC,MAAM,CAAC;;;;aACvC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC;;qBA3HG,mBAAmB;mBALtC,MAAM;uBACF,YAAY;mBAGhB,aAAa"} \ 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 @@ -533,6 +592,61 @@ index 0000000000000000000000000000000000000000..665bb84203a88b35e2961e7221a31896 diff --git a/dist/src/utils.d.ts b/dist/src/utils.d.ts deleted file mode 100644 index 9006a87dd42992dfe0aa0f7ab5298983deb3357a..0000000000000000000000000000000000000000 +diff --git a/dist/src/y-attribution-to-diffset.d.ts b/dist/src/y-attribution-to-diffset.d.ts +new file mode 100644 +index 0000000000000000000000000000000000000000..d9a219ef72198224a123e58b31cbd509cb2cfbf5 +--- /dev/null ++++ b/dist/src/y-attribution-to-diffset.d.ts +@@ -0,0 +1,40 @@ ++export function ydeltaToDiffSet(attributedDelta: d.DeltaAny, { displayedDoc, schema }: { ++ displayedDoc: import("prosemirror-model").Node; ++ schema: import("prosemirror-model").Schema; ++}): DiffSet; ++/** ++ * Who made a change and whether it was an insertion or deletion. ++ */ ++export type Attribution = { ++ type: "added" | "removed"; ++ authorIds: string[]; ++ timestamp?: number; ++}; ++/** ++ * The six diff types produced by `ydeltaToDiffSet`. ++ */ ++export type DiffType = "inline-insert" | "inline-delete" | "block-insert" | "block-delete" | "inline-update" | "block-update"; ++/** ++ * A single difference between the base and suggestion documents. ++ * ++ * `from`/`to` are positions in the *displayed* (clean) PM document. ++ * For delete types, `from === to` (zero-width) and `content` holds the ++ * removed fragment for ghost rendering. For update types, `attributes` ++ * holds the new values and `previousAttributes` (when available) the old. ++ */ ++export type Diff = { ++ type: DiffType; ++ from: number; ++ to: number; ++ content?: import("prosemirror-model").Fragment; ++ attribution: Attribution; ++ attributes?: Record; ++ previousAttributes?: Record; ++}; ++/** ++ * An ordered array of `Diff` objects covering all changes between the ++ * base and suggestion documents. ++ */ ++export type DiffSet = Diff[]; ++import * as d from 'lib0/delta'; ++//# sourceMappingURL=y-attribution-to-diffset.d.ts.map +\ No newline at end of file +diff --git a/dist/src/y-attribution-to-diffset.d.ts.map b/dist/src/y-attribution-to-diffset.d.ts.map +new file mode 100644 +index 0000000000000000000000000000000000000000..229fa9e700846567721d8dedaa5721ac7b29eae8 +--- /dev/null ++++ b/dist/src/y-attribution-to-diffset.d.ts.map +@@ -0,0 +1 @@ ++{"version":3,"file":"y-attribution-to-diffset.d.ts","sourceRoot":"","sources":["../../src/y-attribution-to-diffset.js"],"names":[],"mappings":"AAkEO,iDAJI,CAAC,CAAC,QAAQ,4BACV;IAAE,YAAY,EAAE,OAAO,mBAAmB,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,mBAAmB,EAAE,MAAM,CAAA;CAAE,GAC5F,OAAO,CAQnB;;;;0BArDY;IACR,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;;;;uBAMS,eAAe,GAAG,eAAe,GAAG,cAAc,GAAG,cAAc,GAAG,eAAe,GAAG,cAAc;;;;;;;;;mBAWtG;IACR,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,OAAO,mBAAmB,EAAE,QAAQ,CAAC;IAC/C,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACzC;;;;;sBAOS,IAAI,EAAE;mBA3CA,YAAY"} +\ 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 @@ -627,21 +741,20 @@ index 8eaef6bf2b216933047f528e3c3b0aa469df45e7..258a3b18cc50c11181b70a716953fdb1 "@rollup/plugin-commonjs": "^28.0.8", diff --git a/src/commands.js b/src/commands.js new file mode 100644 -index 0000000000000000000000000000000000000000..504167d4a50fbbb1198a3f9108edba262738504a +index 0000000000000000000000000000000000000000..6c4ff47a6cbe8a9cacc400af7755791daa8a0af1 --- /dev/null +++ b/src/commands.js -@@ -0,0 +1,163 @@ -+import * as d from 'lib0/delta' +@@ -0,0 +1,154 @@ +import { ySyncPluginKey, yUndoPluginKey } from './keys.js' -+import { deltaToPSteps, deltaAttributionToFormat, nodeToDelta, deltaToPNode } from './sync-utils.js' ++import { deltaToPSteps, nodeToDelta, deltaToPNode } from './sync-utils.js' ++import * as d from 'lib0/delta' +import * as Y from '@y/y' +import { absolutePositionToRelativePosition } from './positions.js' + +/** -+ * Switch to pause mode (stop synchronization between prosemirror and ytype) -+ * @param {import('prosemirror-state').EditorState} state -+ * @param {CommandDispatch?} dispatch -+ * @returns {boolean} ++ * Switch to pause mode (stop synchronization between prosemirror and ytype). ++ * ++ * @type {import('prosemirror-state').Command} + */ +export function pauseSync (state, dispatch) { + const pluginState = ySyncPluginKey.getState(state) @@ -656,17 +769,14 @@ index 0000000000000000000000000000000000000000..504167d4a50fbbb1198a3f9108edba26 + return true +} + -+const debugging = false -+ +/** + * Reconfigure y-prosemirror. + * - enable syncing to (different) ytype -+ * - render attributions + * - pause sync (by setting ytype=null) + * + * @param {object} [opts] -+ * @param {YType?} [opts.ytype] Sync different ytype. Set to null to pause sync -+ * @param {AttributionManager?} [opts.attributionManager] Optional attribution manager to switch to ++ * @param {Y.Type | null} [opts.ytype] Sync different ytype. Set to null to pause sync. ++ * @param {Y.AbstractAttributionManager | null} [opts.attributionManager] Optional attribution manager to switch to. + * @returns {import('prosemirror-state').Command} + */ +export const configureYProsemirror = (opts = {}) => (state, dispatch) => { @@ -680,18 +790,13 @@ index 0000000000000000000000000000000000000000..504167d4a50fbbb1198a3f9108edba26 + const tr = state.tr.setMeta(ySyncPluginKey, opts) + tr.setMeta('addToHistory', false) + if (ytype) { -+ /** -+ * @type {ProsemirrorDelta} -+ */ -+ const ycontent = deltaAttributionToFormat(ytype.toDeltaDeep(attributionManager || Y.noAttributionsManager), pluginState.attributionMapper) -+ // @todo it is preferred to apply the minimal diff - at least for debugging purposes. the -+ // document replacal is more reliable though -+ if (debugging) { -+ const pcontent = nodeToDelta(tr.doc, undefined, true) ++ const ycontent = ytype.toDeltaDeep() ++ try { ++ const pcontent = nodeToDelta(tr.doc) + const diff = d.diff(pcontent.done(), ycontent.done()) -+ deltaToPSteps(tr, diff, undefined, undefined, pluginState.attributedNodes) -+ } else { -+ tr.replaceWith(0, tr.doc.content.size, deltaToPNode(ycontent, tr.doc.type.schema, null, pluginState.attributedNodes)) ++ deltaToPSteps(tr, diff) ++ } catch { ++ tr.replaceWith(0, tr.doc.content.size, deltaToPNode(ycontent, tr.doc.type.schema, null)) + } + } + dispatch(tr) @@ -1143,11 +1248,273 @@ index 0000000000000000000000000000000000000000..79fa8f273361c11282e2c2df76c38895 + } + } + }) +diff --git a/src/diff-decorations.js b/src/diff-decorations.js +new file mode 100644 +index 0000000000000000000000000000000000000000..a6e6d6b271242dc7d7dbe9bdc814c10447880db4 +--- /dev/null ++++ b/src/diff-decorations.js +@@ -0,0 +1,256 @@ ++/** ++ * Render a `DiffSet` as ProseMirror decorations over the *displayed* (final) ++ * document. The document is never mutated. ++ * ++ * - inline-insert / inline-update -> Decoration.inline ++ * - block-insert / block-update -> Decoration.node (per top-level node) ++ * - inline-delete / block-delete -> Decoration.widget showing the removed ++ * content, reconstructed by serializing the removed Fragment to HTML. ++ * ++ * Each decoration carries its `diff` in the decoration `spec` so future ++ * node-views / attribute-change extraction can read it back. Decorations ++ * also expose `data-diff-type` and `data-diff-user-id` attributes for CSS. ++ */ ++import { Decoration, DecorationSet } from 'prosemirror-view' ++import { Plugin } from 'prosemirror-state' ++import { DOMSerializer, Fragment } from 'prosemirror-model' ++import { suggestionDiffPluginKey } from './keys.js' ++ ++/** ++ * @typedef {import('./y-attribution-to-diffset.js').Diff} Diff ++ * @typedef {import('./y-attribution-to-diffset.js').DiffSet} DiffSet ++ * @typedef {import('./y-attribution-to-diffset.js').DiffType} DiffType ++ * @typedef {import('./y-attribution-to-diffset.js').Attribution} Attribution ++ */ ++ ++/** ++ * Arguments passed to a `mapDiffToDecorations` callback. ++ * ++ * @typedef {{ ++ * diff: Diff, ++ * doc: import('prosemirror-model').Node, ++ * schema: import('prosemirror-model').Schema, ++ * index: number, ++ * color?: string, ++ * attributes?: import('prosemirror-view').DecorationAttrs, ++ * defaultMapDiffToDecorations?: MapDiffToDecorations ++ * }} MapDiffArgs ++ */ ++ ++/** ++ * Callback that converts a single `Diff` to decoration(s). ++ * Return a `Decoration`, an array of them, or `null` to skip. ++ * ++ * @callback MapDiffToDecorations ++ * @param {MapDiffArgs} args ++ * @returns {import('prosemirror-view').Decoration | import('prosemirror-view').Decoration[] | null} ++ */ ++ ++/** ++ * Options shared by `buildDiffDecorationSet` and `ySuggestionDecorationPlugin`. ++ * ++ * @typedef {{ ++ * colorForAuthors?: (authorIds: string[]) => (string | undefined), ++ * mapDiffToDecorations?: MapDiffToDecorations ++ * }} SuggestionDecorationOptions ++ */ ++ ++/** ++ * Reconstruct removed content as a non-editable DOM node by serializing the ++ * Fragment to HTML. Works for inline text and for whole block nodes alike. ++ * ++ * @param {import('prosemirror-model').Fragment} fragment ++ * @param {import('prosemirror-model').Schema} schema ++ * @param {{ authorIds?: string[], color?: string, title?: string }} [opts] ++ * @returns {HTMLElement} ++ */ ++export const renderDeletedContent = (fragment, schema, opts = {}) => { ++ const serializer = DOMSerializer.fromSchema(schema) ++ const isBlock = fragment?.firstChild?.isBlock ?? false ++ const container = document.createElement(isBlock ? 'div' : 'span') ++ container.className = 'pm-suggest pm-suggest--delete' ++ container.setAttribute('data-diff-type', isBlock ? 'block-delete' : 'inline-delete') ++ if (opts.authorIds?.length) { ++ container.setAttribute('data-diff-user-id', opts.authorIds.join(',')) ++ } ++ if (opts.color) { ++ container.style.setProperty('--author-color', opts.color) ++ } ++ if (opts.title) { ++ container.setAttribute('title', opts.title) ++ } ++ container.contentEditable = 'false' ++ if (fragment) { ++ container.appendChild(serializer.serializeFragment(fragment, { document })) ++ } ++ return container ++} ++ ++/** ++ * Build a human-readable hover title from diff attribution. ++ * ++ * @param {Diff} diff ++ * @returns {string} ++ */ ++const hoverTitle = (diff) => { ++ const parts = [] ++ const authorIds = diff.attribution.authorIds ++ if (authorIds.length) { ++ parts.push(authorIds.join(', ')) ++ } ++ if (diff.attribution.timestamp) { ++ parts.push(new Date(diff.attribution.timestamp).toLocaleString()) ++ } ++ const typeLabel = diff.type.replace('-', ' ') ++ if (parts.length) return `${typeLabel}: ${parts.join(' — ')}` ++ return typeLabel ++} ++ ++/** ++ * Build a summary string for a block-update diff showing what changed ++ * (e.g. "level: 1 → 2"). ++ * ++ * @param {Diff} diff ++ * @returns {string} ++ */ ++const blockUpdateSummary = (diff) => { ++ if (diff.type !== 'block-update') return '' ++ const attrs = diff.attributes ++ const prev = diff.previousAttributes ++ if (!attrs) return '' ++ const parts = [] ++ for (const key of Object.keys(attrs)) { ++ const newVal = attrs[key] ++ const oldVal = prev?.[key] ++ if (oldVal !== undefined && oldVal !== newVal) { ++ parts.push(`${key}: ${oldVal} → ${newVal}`) ++ } else { ++ parts.push(`${key}: ${newVal}`) ++ } ++ } ++ return parts.join(', ') ++} ++ ++/** ++ * @param {Diff} diff ++ * @param {{ authorIds: string[], color?: string }} ctx ++ * @returns {import('prosemirror-view').DecorationAttrs} ++ */ ++const decorationAttrs = (diff, { authorIds, color }) => { ++ /** @type {import('prosemirror-view').DecorationAttrs} */ ++ const attrs = { ++ class: `pm-suggest pm-suggest--${diff.type}`, ++ 'data-diff-type': diff.type ++ } ++ if (authorIds.length) attrs['data-diff-user-id'] = authorIds.join(',') ++ if (color) attrs.style = `--author-color: ${color}` ++ // Hover metadata: show author(s), timestamp, and attribute changes ++ let title = hoverTitle(diff) ++ const summary = blockUpdateSummary(diff) ++ if (summary) title += ` (${summary})` ++ attrs.title = title ++ return attrs ++} ++ ++/** ++ * Default mapping from a single `Diff` to decoration(s). Returns a `Decoration`, ++ * an array of them, or `null` to skip. ++ * ++ * @type {MapDiffToDecorations} ++ */ ++export const defaultMapDiffToDecorations = ({ diff, doc, schema, index, color, attributes = {} }) => { ++ const authorIds = diff.attribution.authorIds ++ const attrs = { ...decorationAttrs(diff, { authorIds, color }), ...attributes } ++ const spec = { diff } ++ ++ switch (diff.type) { ++ case 'inline-insert': ++ case 'inline-update': ++ return Decoration.inline(diff.from, diff.to, attrs, { ...spec, inclusiveStart: false, inclusiveEnd: true }) ++ ++ case 'block-update': ++ return Decoration.node(diff.from, diff.to, attrs, spec) ++ ++ case 'block-insert': { ++ const $from = doc.resolve(diff.from) ++ const after = $from.nodeAfter ++ if (after && diff.from + after.nodeSize === diff.to) { ++ return Decoration.node(diff.from, diff.to, attrs, spec) ++ } ++ /** @type {Decoration[]} */ ++ const decos = [] ++ doc.nodesBetween(diff.from, diff.to, (node, pos) => { ++ if (pos >= diff.from && pos + node.nodeSize <= diff.to && node.isBlock) { ++ decos.push(Decoration.node(pos, pos + node.nodeSize, attrs, spec)) ++ return false ++ } ++ return undefined ++ }) ++ if (!decos.length) { ++ decos.push(Decoration.inline(diff.from, diff.to, attrs, spec)) ++ } ++ return decos ++ } ++ ++ case 'inline-delete': ++ case 'block-delete': ++ return Decoration.widget( ++ diff.from, ++ () => renderDeletedContent(diff.content ?? Fragment.empty, schema, { authorIds, color, title: hoverTitle(diff) }), ++ { side: 1, key: `diff-del-${index}-${diff.content?.size ?? 0}`, diff } ++ ) ++ ++ default: ++ return null ++ } ++} ++ ++/** ++ * Build a `DecorationSet` for `diffs` over `doc`. Pure - does not touch the doc. ++ * ++ * @param {import('prosemirror-model').Node} doc ++ * @param {DiffSet} diffs ++ * @param {import('prosemirror-model').Schema} schema ++ * @param {SuggestionDecorationOptions} [opts] ++ * @returns {DecorationSet} ++ */ ++export const buildDiffDecorationSet = (doc, diffs, schema, opts = {}) => { ++ const map = opts.mapDiffToDecorations ?? defaultMapDiffToDecorations ++ /** @type {Decoration[]} */ ++ const decorations = [] ++ diffs.forEach((diff, index) => { ++ const color = opts.colorForAuthors?.(diff.attribution.authorIds) ++ const result = map({ diff, doc, schema, index, color, defaultMapDiffToDecorations }) ++ if (Array.isArray(result)) decorations.push(...result.filter(Boolean)) ++ else if (result) decorations.push(result) ++ }) ++ return DecorationSet.create(doc, decorations) ++} ++ ++/** ++ * ProseMirror plugin that overlays a `DiffSet` as decorations. ++ * ++ * Update the diffs at runtime by dispatching ++ * `tr.setMeta(suggestionDiffPluginKey, { diffs })`. ++ * ++ * @param {SuggestionDecorationOptions & { diffs?: DiffSet }} [config] ++ * @returns {Plugin} ++ */ ++export const suggestionDiffPlugin = ({ diffs = [], mapDiffToDecorations, colorForAuthors } = {}) => ++ new Plugin({ ++ key: suggestionDiffPluginKey, ++ state: { ++ init: (_config, state) => ++ buildDiffDecorationSet(state.doc, diffs, state.schema, { mapDiffToDecorations, colorForAuthors }), ++ apply: (tr, prev, _old, newState) => { ++ const meta = tr.getMeta(suggestionDiffPluginKey) ++ if (meta?.diffs) { ++ return buildDiffDecorationSet(newState.doc, meta.diffs, newState.schema, { mapDiffToDecorations, colorForAuthors }) ++ } ++ return prev.map(tr.mapping, tr.doc) ++ } ++ }, ++ props: { ++ decorations: (state) => suggestionDiffPluginKey.getState(state) ++ } ++ }) diff --git a/src/index.js b/src/index.js -index ac407e0c363309c970f3dbcbd66db00f9cd1656a..0c20333ce9f66f1a1e3e8e44da1ac4017bbba4cc 100644 +index ac407e0c363309c970f3dbcbd66db00f9cd1656a..a168ef17e646c9378899e4ff3b515740ec0b07eb 100644 --- a/src/index.js +++ b/src/index.js -@@ -1,627 +1,7 @@ +@@ -1,627 +1,17 @@ -import * as delta from 'lib0/delta' -import * as math from 'lib0/math' -import * as mux from 'lib0/mutex' @@ -1778,18 +2145,30 @@ 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 } from './sync-utils.js' +export * from './commands.js' +export * from './undo-plugin.js' +export * from './cursor-plugin.js' ++export { ydeltaToDiffSet } from './y-attribution-to-diffset.js' ++/** @typedef {import('./y-attribution-to-diffset.js').Attribution} Attribution */ ++/** @typedef {import('./y-attribution-to-diffset.js').Diff} Diff */ ++/** @typedef {import('./y-attribution-to-diffset.js').DiffType} DiffType */ ++/** @typedef {import('./y-attribution-to-diffset.js').DiffSet} DiffSet */ ++export { buildDiffDecorationSet, suggestionDiffPlugin, renderDeletedContent, defaultMapDiffToDecorations } from './diff-decorations.js' ++/** @typedef {import('./diff-decorations.js').SuggestionDecorationOptions} SuggestionDecorationOptions */ ++/** @typedef {import('./diff-decorations.js').MapDiffToDecorations} MapDiffToDecorations */ ++/** @typedef {import('./diff-decorations.js').MapDiffArgs} MapDiffArgs */ ++export { ySuggestionDecorationPlugin } from './suggestion-decoration-plugin.js' diff --git a/src/keys.js b/src/keys.js new file mode 100644 -index 0000000000000000000000000000000000000000..7490849525d1ff00da44aa34b7588531d5f5fd7e +index 0000000000000000000000000000000000000000..5430d28aa03577d34dfdf647eee1096393f30ca0 --- /dev/null +++ b/src/keys.js -@@ -0,0 +1,25 @@ +@@ -0,0 +1,43 @@ +import { PluginKey } from 'prosemirror-state' // eslint-disable-line + ++/** @typedef {import('lib0/schema').Unwrap} SyncPluginState */ ++ +/** + * The unique prosemirror plugin key for {@link import('./sync-plugin.js').syncPlugin} + * @@ -1813,6 +2192,22 @@ index 0000000000000000000000000000000000000000..7490849525d1ff00da44aa34b7588531 + * @type {PluginKey} + */ +export const yCursorPluginKey = new PluginKey('y-cursor') ++ ++/** ++ * The unique prosemirror plugin key for {@link import('./suggestion-decoration-plugin.js').ySuggestionDecorationPlugin} ++ * ++ * @public ++ * @type {PluginKey} ++ */ ++export const ySuggestionDecorationPluginKey = new PluginKey('y-suggestion-decorations') ++ ++/** ++ * The unique prosemirror plugin key for {@link import('./diff-decorations.js').suggestionDiffPlugin} ++ * ++ * @public ++ * @type {PluginKey} ++ */ ++export const suggestionDiffPluginKey = new PluginKey('suggestion-diff') diff --git a/src/lib.js b/src/lib.js deleted file mode 100644 index 698f0c8c42ffed9804a2c13f48bd4c51f27794dc..0000000000000000000000000000000000000000 @@ -2046,21 +2441,100 @@ index 0000000000000000000000000000000000000000..963ea708dbe0e92b2d43fc031243c2e7 + } + } +} +diff --git a/src/suggestion-decoration-plugin.js b/src/suggestion-decoration-plugin.js +new file mode 100644 +index 0000000000000000000000000000000000000000..b6ba25f283511c74d4fd43456fe05651c980b9fd +--- /dev/null ++++ b/src/suggestion-decoration-plugin.js +@@ -0,0 +1,75 @@ ++/** ++ * ProseMirror plugin that renders Yjs attribution as decorations over a ++ * clean document. Follows the cursor-plugin pattern: a separate plugin ++ * with its own key, state, and decorations prop. ++ * ++ * Use alongside `syncPlugin()` which always syncs clean content (no ++ * attribution marks, no deleted text). This plugin reads the attributed ++ * delta separately and overlays suggestions as decorations. ++ * ++ * The sync plugin dispatches y-sync-transaction meta on remote Y changes ++ * and AM change events. This plugin rebuilds decorations in `apply` ++ * when it sees that meta or a doc change. ++ */ ++import * as Y from '@y/y' ++import { Plugin } from 'prosemirror-state' ++import { DecorationSet } from 'prosemirror-view' ++import { ySyncPluginKey, ySuggestionDecorationPluginKey } from './keys.js' ++import { ydeltaToDiffSet } from './y-attribution-to-diffset.js' ++import { buildDiffDecorationSet } from './diff-decorations.js' // eslint-disable-line ++/** @typedef {import('./diff-decorations.js').SuggestionDecorationOptions} SuggestionDecorationOptions */ ++ ++/** ++ * Build decorations from the Yjs attribution delta. ++ * ++ * @param {import('prosemirror-model').Node} doc ++ * @param {import('prosemirror-model').Schema} schema ++ * @param {Y.Type | null} ytype ++ * @param {Y.AbstractAttributionManager | null} am ++ * @param {SuggestionDecorationOptions} opts ++ * @returns {DecorationSet} ++ */ ++function computeDecorations (doc, schema, ytype, am, opts) { ++ if (!ytype || !am || am === Y.noAttributionsManager) { ++ return DecorationSet.empty ++ } ++ const attributedDelta = ytype.toDeltaDeep(am) ++ const diffs = ydeltaToDiffSet(attributedDelta, { displayedDoc: doc, schema }) ++ try { ++ return buildDiffDecorationSet(doc, diffs, schema, opts) ++ } catch (err) { ++ console.error('[y-prosemirror] decoration build failed:', err) ++ return DecorationSet.empty ++ } ++} ++ ++/** ++ * @param {SuggestionDecorationOptions} [opts] ++ * @returns {Plugin} ++ */ ++export const ySuggestionDecorationPlugin = (opts = {}) => ++ new Plugin({ ++ key: ySuggestionDecorationPluginKey, ++ state: { ++ init: () => DecorationSet.empty, ++ apply: (tr, prev, oldState, newState) => { ++ const ySyncMeta = tr.getMeta('y-sync-transaction') ++ const configMeta = tr.getMeta(ySyncPluginKey) ++ const metaOverride = configMeta || ySyncMeta ++ if (metaOverride) { ++ const baseSync = ySyncPluginKey.getState(oldState) || ySyncPluginKey.getState(newState) ++ const ystate = Object.assign({}, baseSync, metaOverride) ++ if (ystate?.attributionManager && ystate.attributionManager !== Y.noAttributionsManager) { ++ return computeDecorations( ++ newState.doc, newState.schema, ystate.ytype, ystate.attributionManager, opts ++ ) ++ } ++ } ++ if (tr.docChanged) return prev.map(tr.mapping, tr.doc) ++ return prev ++ } ++ }, ++ props: { ++ decorations: (state) => ySuggestionDecorationPluginKey.getState(state) ++ } ++ }) diff --git a/src/sync-plugin.js b/src/sync-plugin.js new file mode 100644 -index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054814dda91 +index 0000000000000000000000000000000000000000..e18bf04380f182a7cf9e14b74d2d39c9044b7320 --- /dev/null +++ b/src/sync-plugin.js -@@ -0,0 +1,301 @@ +@@ -0,0 +1,211 @@ +import * as Y from '@y/y' +import { Plugin } from 'prosemirror-state' +import { -+ $prosemirrorDelta, -+ defaultAttributedNodes, -+ defaultMapAttributionToMark, -+ deltaAttributionToFormat, + deltaToPSteps, -+ nodeToDelta ++ docDiffToDelta, ++ nodeToDelta, ++ stepToDelta +} from './sync-utils.js' +import * as d from 'lib0/delta' +import { ySyncPluginKey } from './keys.js' @@ -2068,88 +2542,27 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 +import * as object from 'lib0/object' + +/** -+ * The y-prosemirror binding is a bi-directional synchronization with the provided Y.Type and the EditorView ++ * The y-prosemirror binding is a bi-directional synchronization with the provided Y.Type and the EditorView. + * Any change applied to the EditorView will be applied (via deltas) to the Y.Type, and vice versa. ++ * ++ * The PM document always contains **clean** content — no attribution marks, ++ * no deleted text inline. Attribution is rendered as decorations by ++ * {@link import('./suggestion-decoration-plugin.js').ySuggestionDecorationPlugin}. + */ +export const $syncPluginState = s.$object({ + ytype: Y.$ytypeAny.nullable, -+ /** -+ * If provided, will switch to the given attribution manager instead of the current attribution manager -+ */ -+ attributionManager: Y.$attributionManager.nullable, -+ attributionMapper: /** @type {s.Schema} */ (s.$function), -+ /** -+ * Predicate deciding which attributed nodes render under their -+ * `{nodeName}--attributed` variant. See {@link syncPlugin}. -+ */ -+ attributedNodes: /** @type {s.Schema} */ (s.$function) ++ attributionManager: Y.$attributionManager.nullable +}) + +export const $syncPluginStateUpdate = s.$object({ + ytype: Y.$ytypeAny.nullable.optional, -+ attributionManager: Y.$attributionManager.nullable.optional, -+ attributionMapper: /** @type {s.Schema} */ (s.$function).nullable.optional, -+ attributedNodes: /** @type {s.Schema} */ (s.$function).nullable.optional, -+ change: /** @type {s.Schema>} */ (s.$any).nullable.optional ++ attributionManager: Y.$attributionManager.nullable.optional +}) +const $maybeSyncPluginStateUpdate = $syncPluginStateUpdate.nullable + -+const attributedDeleteMark = 'y-attributed-delete' -+const attributionMarkNames = [ -+ 'y-attributed-insert', -+ 'y-attributed-format', -+ attributedDeleteMark -+] -+ -+/** -+ * Strip attribution-mark formats (`y-attributed-*`). Returns a fresh -+ * delta - **never mutates** the input. `lib0/delta.diff` reuses op -+ * references (and nested delta references) from its inputs, so an -+ * in-place mutation here would also mutate `pcontent`/`desiredPM` and -+ * corrupt subsequent diff calls. `lib0/delta.clone` only deep-clones -+ * the top level - nested deltas inside an `InsertOp.insert` array stay -+ * shared by reference - so cloning then mutating is also unsafe. -+ * -+ * @param {d.DeltaAny} input -+ * @returns {d.DeltaAny} -+ */ -+const stripAttributionFormattingFromDelta = (input) => { -+ /** @param {Record | null | undefined} format */ -+ const stripFormat = (format) => { -+ if (format == null) return format -+ /** @type {Record} */ -+ const out = {} -+ for (const k in format) { -+ if (!attributionMarkNames.includes(k)) out[k] = format[k] -+ } -+ return out -+ } -+ const out = /** @type {any} */ (d.create(input.name, $prosemirrorDelta)) -+ for (const attr of input.attrs) { -+ // @ts-ignore -+ out.attrs[attr.key] = attr.clone() -+ } -+ for (const child of input.children) { -+ if (d.$retainOp.check(child)) { -+ out.retain(child.retain, stripFormat(child.format)) -+ } else if (d.$textOp.check(child)) { -+ out.insert(child.insert, stripFormat(child.format)) -+ } else if (d.$insertOp.check(child)) { -+ const newInsert = child.insert.map(ins => -+ d.$deltaAny.check(ins) ? stripAttributionFormattingFromDelta(ins) : ins -+ ) -+ out.insert(newInsert, stripFormat(child.format)) -+ } else if (d.$deleteOp.check(child)) { -+ out.delete(child.delete) -+ } else if (d.$modifyOp.check(child)) { -+ out.modify(stripAttributionFormattingFromDelta(child.value), stripFormat(child.format)) -+ } -+ } -+ return out.done(false) -+} -+ +/** -+ * This Prosemirror {@link Plugin} is responsible for synchronizing the prosemirror {@link EditorState} with a {@link Y.XmlFragment} ++ * This Prosemirror {@link Plugin} is responsible for synchronizing the prosemirror ++ * {@link import('prosemirror-state').EditorState} with a {@link Y.XmlFragment}. + * + * The PM->Y diff/apply pipeline runs in the plugin's `view().update` + * hook (i.e. after the dispatch has been committed to the view), not @@ -2157,10 +2570,12 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 + * cause speculative `state.apply` callers to write to Y as a side + * effect. + * -+ * @param {object} opts -+ * @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)`. ++ * The PM document always mirrors the **clean** Y content (no attribution ++ * marks, no deleted text). The write path applies diffs through the AM ++ * so edits are tagged as suggestions. Attribution rendering is handled ++ * by the separate {@link import('./suggestion-decoration-plugin.js').ySuggestionDecorationPlugin}. ++ * ++ * @param {object} [opts] + * @returns {Plugin} + */ +export function syncPlugin (opts = {}) { @@ -2170,78 +2585,47 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 + init: () => { + return $syncPluginState.expect({ + ytype: null, -+ attributionManager: null, -+ attributionMapper: opts.mapAttributionToMark || defaultMapAttributionToMark, -+ attributedNodes: opts.attributedNodes || defaultAttributedNodes ++ attributionManager: null + }) + }, + apply: (tr, prevPluginState) => { + const stateUpdate = $maybeSyncPluginStateUpdate.expect(tr.getMeta(ySyncPluginKey) || null) ++ const prev = /** @type {any} */ (prevPluginState) ++ const isSyncTr = tr.getMeta('y-sync-transaction') != null || tr.getMeta(ySyncPluginKey) != null ++ const pendingTrs = isSyncTr ? [] : [...(prev.pendingTrs || []), tr] + if (!stateUpdate) { -+ return prevPluginState ++ return /** @type {any} */ ({ ...prev, pendingTrs }) + } -+ return object.assign({}, prevPluginState, stateUpdate, stateUpdate.attributionManager == null ? { attributionManager: Y.noAttributionsManager } : {}) ++ return /** @type {any} */ (object.assign({}, prev, stateUpdate, stateUpdate.attributionManager == null ? { attributionManager: Y.noAttributionsManager } : {}, { pendingTrs })) + } + }, + view () { + /** @type {(() => void) | null} */ + let unsubscribeFn = null + /** -+ * Subscribe to ytype changes and apply remote updates to prosemirror ++ * Subscribe to ytype changes and apply remote updates to prosemirror. + * @param {object} opts + * @param {import('prosemirror-view').EditorView} opts.view + * @param {Y.Type?} opts.ytype + * @param {Y.AbstractAttributionManager?} opts.attributionManager -+ * @param {AttributionMapper} opts.attributionMapper -+ * @param {AttributedNodesPredicate} opts.attributedNodes + */ -+ function subscribeToYType ({ view, ytype, attributionManager, attributionMapper, attributedNodes }) { ++ function subscribeToYType ({ view, ytype, attributionManager }) { + unsubscribeFn?.() + if (ytype != null) { -+ // Listen on the doc's `afterTransaction` event rather than -+ // `ytype.observeDeep`. `observeDeep` skips firing for any -+ // changes whose path runs through a *deleted* parent type -+ // (Y.js `Transaction._callObserver` short-circuits when -+ // `parent._item.deleted`). That happens in suggestion-mode -+ // when one peer suggestion-deletes a paragraph and another -+ // peer then inserts into it - the integrate path leaves the -+ // root deep observer silent, so the PM view never reconciles -+ // and goes stale (see `testCohortReplayConvergesAfterInsert -+ // IntoSuggestionDeletedParagraph`). `afterTransaction` fires -+ // unconditionally, so the reconcile pass always runs. + /** @type {Y.Doc} */ + const ydoc = /** @type {Y.Doc} */ (ytype.doc) + const onAfterTransaction = (/** @type {any} */ tr) => { + if (!view || view.isDestroyed) { + return unsubscribeFn?.() + } -+ // Skip changes we wrote ourselves from `view().update` -+ // - the PM->Y commit there already handled the reconcile -+ // dispatch in the same call. + if (/** @type {any} */ (tr).origin === ySyncPluginKey.get(view.state)) return -+ // Same pipeline as the PM->Y sync in `view().update`: -+ // render ytype through the AM, diff against the current PM doc, -+ // apply only the difference. Using `change.getDelta` here -+ // produced wrong/asymmetric output for some interleavings -+ // (notably commits-to-base from one peer that touched suggestion -+ // overlays from another), causing PM views to diverge from each -+ // other and from the canonical AM render. The full re-render is -+ // more expensive per update but is the only diff target all -+ // peers agree on. -+ const am = attributionManager || Y.noAttributionsManager -+ const desiredPM = deltaAttributionToFormat( -+ ytype.toDeltaDeep(am), -+ attributionMapper -+ ).done() -+ const pcontent = nodeToDelta(view.state.doc, undefined, true).done() ++ const desiredPM = ytype.toDeltaDeep().done() ++ const pcontent = nodeToDelta(view.state.doc).done() + const diff = d.diff(pcontent, desiredPM) -+ if (diff.isEmpty()) return -+ const ptr = deltaToPSteps(view.state.tr, diff, undefined, undefined, attributedNodes) ++ const ptr = diff.isEmpty() ? view.state.tr : deltaToPSteps(view.state.tr, diff) + ptr.setMeta('addToHistory', false) + ptr.setMeta('y-sync-transaction', $syncPluginStateUpdate.expect({ -+ change: null, + attributionManager, -+ attributionMapper, + ytype + })) + view.dispatch(ptr) @@ -2251,26 +2635,13 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 + if (!view || view.isDestroyed) { + return unsubscribeFn?.() + } -+ // Same pipeline as the PM->Y sync in `view().update`: -+ // render ytype through the AM, diff against the current PM doc, -+ // apply only the difference. We give up the `itemsToRender` -+ // targeted-rerender optimization in exchange for going through -+ // the same path that the rest of the plugin uses, which keeps -+ // the deltas shallow (only what actually changed). -+ const desiredPM = deltaAttributionToFormat( -+ ytype.toDeltaDeep(attributionManager || Y.noAttributionsManager), -+ attributionMapper -+ ).done() -+ const pcontent = nodeToDelta(view.state.doc, undefined, true).done() ++ const desiredPM = ytype.toDeltaDeep().done() ++ const pcontent = nodeToDelta(view.state.doc).done() + const diff = d.diff(pcontent, desiredPM) -+ if (diff.isEmpty()) return -+ const ptr = deltaToPSteps(view.state.tr, diff, undefined, undefined, attributedNodes) ++ const ptr = diff.isEmpty() ? view.state.tr : deltaToPSteps(view.state.tr, diff) + ptr.setMeta('addToHistory', false) -+ // @todo stop updating meta on every transaction + ptr.setMeta('y-sync-transaction', $syncPluginStateUpdate.expect({ -+ change: null, // @todo - remove this property + attributionManager, -+ attributionMapper, + ytype + })) + view.dispatch(ptr) @@ -2293,55 +2664,70 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 + const ytypeChanged = prevYtype !== ytype + const attributionManagerChanged = prevAttributionManager !== attributionManager + if (ytypeChanged || attributionManagerChanged) { -+ // Subscribe to the new ytype/attributionManager -+ // (subscribeToYType will automatically unsubscribe from previous if needed) -+ subscribeToYType({ -+ view, -+ ytype, -+ attributionManager, -+ attributionMapper: pluginState.attributionMapper, -+ attributedNodes: pluginState.attributedNodes -+ }) ++ subscribeToYType({ view, ytype, attributionManager }) + } + if (ytype == null) return + if (view.state.doc === prevState.doc) return -+ // PM->Y diff/apply pipeline. Runs after the dispatch is -+ // committed to the view, so speculative `state.apply` calls -+ // do not write to Y. The Y `afterTransaction` observer -+ // skips the write we make here via the origin check. The -+ // AM `change` handler may, however, dispatch its own -+ // reconcile synchronously during `transact` - so we -+ // re-read `pcontent` from `view.state.doc` after the write -+ // before computing our own reconcile, otherwise we'd -+ // apply the same insert twice. + const am = attributionManager || Y.noAttributionsManager -+ const mapper = pluginState.attributionMapper -+ const attributedNodes = pluginState.attributedNodes -+ const ycontent = deltaAttributionToFormat( -+ ytype.toDeltaDeep(am), -+ mapper -+ ).done() -+ const pcontent = nodeToDelta(view.state.doc, undefined, true).done() -+ const pmToYDiff = stripAttributionFormattingFromDelta(d.diff(ycontent, pcontent)) -+ if (!pmToYDiff.isEmpty()) { ++ const pendingTrs = /** @type {any} */ (pluginState).pendingTrs || [] ++ // The diff is in "clean" coordinates where AM-deleted items are ++ // invisible. But applyDelta navigates Y items via ++ // am.contentLength(), and a DiffAttributionManager counts ++ // AM-deleted items at full length — so retain counts land at ++ // wrong positions. This proxy overrides contentLength and ++ // readContent to use clean counting (deleted items = 0) while ++ // passing everything else through to the real AM. ++ // ++ // The proxy's distinct identity (≠ noAttributionsManager) keeps ++ // applyDelta's attribution-aware code paths active. Attribution ++ // recording is unaffected: the DiffAttributionManager's listener ++ // on the Y.Doc fires based on the transaction, not the AM passed ++ // to applyDelta. ++ const navAM = am === Y.noAttributionsManager ++ ? am ++ : new Proxy(am, { ++ get (target, prop, receiver) { ++ if (prop === 'contentLength') return Y.noAttributionsManager.contentLength ++ if (prop === 'readContent') return Y.noAttributionsManager.readContent ++ return Reflect.get(target, prop, receiver) ++ } ++ }) ++ const docChangedTrs = pendingTrs.filter(/** @param {any} ptr */ ptr => ptr.docChanged) ++ if (am === Y.noAttributionsManager && ++ docChangedTrs.length > 0 && ++ docChangedTrs[0].before === prevState.doc) { + /** @type {Y.Doc} */ (ytype.doc).transact(() => { -+ ytype.applyDelta(pmToYDiff, am) ++ for (const ptr of docChangedTrs) { ++ if (ptr.steps.length === 1) { ++ ytype.applyDelta(stepToDelta(ptr.steps[0], ptr.docs[0]), navAM) ++ } else { ++ ytype.applyDelta(docDiffToDelta(ptr.before, ptr.doc), navAM) ++ } ++ } + }, ySyncPluginKey.get(view.state)) ++ } else { ++ const ycontent = ytype.toDeltaDeep().done() ++ const pcontent = nodeToDelta(view.state.doc).done() ++ const pmToYDiff = d.diff(ycontent, pcontent) ++ if (!pmToYDiff.isEmpty()) { ++ /** @type {Y.Doc} */ (ytype.doc).transact(() => { ++ ytype.applyDelta(pmToYDiff, navAM) ++ }, ySyncPluginKey.get(view.state)) ++ } + } -+ const desiredPM = deltaAttributionToFormat( -+ ytype.toDeltaDeep(am), -+ mapper -+ ).done() -+ const pcontentAfter = nodeToDelta(view.state.doc, undefined, true).done() ++ // Reconcile: ensure PM matches the clean Y render after the write. ++ // Always dispatch y-sync-transaction meta so the decoration plugin ++ // can rebuild (even if the reconcile diff is empty). ++ const desiredPM = ytype.toDeltaDeep().done() ++ const pcontentAfter = nodeToDelta(view.state.doc).done() + const pmReconcileDiff = d.diff(pcontentAfter, desiredPM) -+ if (pmReconcileDiff.isEmpty()) return + const tr = view.state.tr -+ deltaToPSteps(tr, pmReconcileDiff, undefined, undefined, attributedNodes) ++ if (!pmReconcileDiff.isEmpty()) { ++ deltaToPSteps(tr, pmReconcileDiff) ++ } + tr.setMeta('addToHistory', false) + tr.setMeta('y-sync-transaction', $syncPluginStateUpdate.expect({ -+ change: null, + attributionManager, -+ attributionMapper: mapper, + ytype + })) + view.dispatch(tr) @@ -2355,14 +2741,12 @@ index 0000000000000000000000000000000000000000..079bc7e465f98612907d36adc9854054 +} diff --git a/src/sync-utils.js b/src/sync-utils.js new file mode 100644 -index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d887b38d28 +index 0000000000000000000000000000000000000000..a09e1d95e0f4b4092fbf2559ab60889c58dbd215 --- /dev/null +++ b/src/sync-utils.js -@@ -0,0 +1,752 @@ +@@ -0,0 +1,566 @@ +import * as Y from '@y/y' -+import * as array from 'lib0/array' +import * as delta from 'lib0/delta' -+import * as error from 'lib0/error' +import * as math from 'lib0/math' +import * as object from 'lib0/object' +import * as s from 'lib0/schema' @@ -2380,156 +2764,7 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + +export const $prosemirrorDelta = delta.$delta({ name: s.$string, attrs: s.$record(s.$string, s.$any), text: true, recursiveChildren: true }) + -+/** -+ * Suffix appended to a node name when it is rendered as its "attributed -+ * variant" (see `attributedNodes` on {@link syncPlugin}). The suffix is fixed -+ * so that canonicalizing back (PM -> Y) is a pure string operation and can -+ * never drift from the forward mapping. `--attributed` is a *reserved* suffix: -+ * a real node type literally ending in it would be canonicalized away on the -+ * way to Y. -+ */ -+export const ATTRIBUTED_SUFFIX = '--attributed' -+ -+/** -+ * Default `attributedNodes` predicate - the feature is off, so every node keeps -+ * its canonical name. -+ * -+ * @type {AttributedNodesPredicate} -+ */ -+export const defaultAttributedNodes = () => false -+ -+/** -+ * Strip the {@link ATTRIBUTED_SUFFIX} so a PM node name maps back to the -+ * canonical name stored in the Y document. Identity for canonical names. -+ * -+ * @param {string} name -+ * @return {string} -+ */ -+export const canonicalNodeName = (name) => -+ name.endsWith(ATTRIBUTED_SUFFIX) -+ ? name.slice(0, -ATTRIBUTED_SUFFIX.length) -+ : name -+ -+/** -+ * Resolve the PM node name to render for `canonicalName` given the attribution -+ * carried in `format`. Returns `canonicalName + ATTRIBUTED_SUFFIX` when the -+ * `attributedNodes` predicate opts in *and* the variant exists in the schema; -+ * otherwise returns `canonicalName` unchanged. -+ * -+ * @param {string} canonicalName -+ * @param {Record | null | undefined} format -+ * @param {AttributedNodesPredicate} attributedNodes -+ * @param {import('prosemirror-model').Schema} schema -+ * @return {string} -+ */ -+export const attributedVariant = (canonicalName, format, attributedNodes, schema) => { -+ const kinds = { -+ insert: format?.['y-attributed-insert'] != null, -+ delete: format?.['y-attributed-delete'] != null, -+ format: format?.['y-attributed-format'] != null -+ } -+ if ((kinds.insert || kinds.delete || kinds.format) && attributedNodes(canonicalName, kinds)) { -+ const variant = canonicalName + ATTRIBUTED_SUFFIX -+ if (schema.nodes[variant] != null) return variant -+ } -+ return canonicalName -+} -+ -+/** -+ * Default attribution-to-mark mapper. -+ * -+ * **The mark names are part of `y-prosemirror`'s public contract and cannot be -+ * changed.** A custom `mapAttributionToMark` may return a different *value* -+ * (different attrs, omit some attribution kinds, etc.), but it must use the -+ * exact mark names below - other internals reference them by name and will not -+ * find marks named anything else: -+ * -+ * - `y-attributed-insert` -+ * - `y-attributed-delete` -+ * - `y-attributed-format` -+ * -+ * The integrator's ProseMirror schema must (a) define mark types with exactly -+ * these names and (b) ensure they are allowed on every node where attribution -+ * marks may land. See `CAVEATS.md` ("Attribution mark names are fixed") for the -+ * full rationale and the schema gotcha around mark-group resolution. -+ * -+ * Note: a single op may carry multiple attribution kinds simultaneously -+ * (e.g. inserted text whose format was also suggested), so the mapper sets -+ * each applicable mark independently rather than picking one. Absent kinds -+ * are not added to the format object - the diff layer naturally produces a -+ * format-remove when comparing PM content (where a stale mark is present) -+ * against the freshly-rendered AM delta (where the key is absent). -+ * -+ * @template {import('lib0/delta').Attribution} T -+ * @param {Record | null} format -+ * @param {T} attribution -+ * @returns {Record | null} -+ */ -+export const defaultMapAttributionToMark = (format, attribution) => { -+ const out = /** @type {Record} */ (object.assign({}, format)) -+ // Set each attribution kind that is present. Do NOT explicitly null out -+ // the absent kinds: lib0/delta's diff naturally produces a format-remove -+ // when comparing pcontent (where the mark is present) with desiredPM -+ // (where the key is absent). Including explicit `null` here would change -+ // the delta op's fingerprint and prevent the diff from matching ops by -+ // content, causing spurious text-node splits. -+ if (attribution.insert) { -+ out['y-attributed-insert'] = { -+ userIds: attribution.insert, -+ timestamp: attribution.insertAt ?? null -+ } -+ } -+ if (attribution.delete) { -+ out['y-attributed-delete'] = { -+ userIds: attribution.delete, -+ timestamp: attribution.deleteAt ?? null -+ } -+ } -+ if (attribution.format) { -+ // `userIdsByAttr` keeps the per-format-key authorship for callers that -+ // need it; `userIds` is the deduped union across all format keys for -+ // callers that just want "who suggested any format on this span". -+ out['y-attributed-format'] = { -+ userIds: array.unique(object.map(attribution.format, v => v).flat()), -+ userIdsByAttr: attribution.format, -+ timestamp: attribution.formatAt ?? null -+ } -+ } -+ return out -+} -+ -+/** -+ * Transform delta with attributions to delta with formats (marks). -+ * @param {delta.DeltaAny} d -+ * @param {function} attributionsToFormat -+ */ -+export const deltaAttributionToFormat = (d, attributionsToFormat) => { -+ const r = delta.create(d.name, $prosemirrorDelta) -+ for (const attr of d.attrs) { -+ // @ts-ignore -+ r.attrs[attr.key] = attr.clone() -+ } -+ for (const child of d.children) { -+ if (delta.$deleteOp.check(child)) { -+ r.delete(child.delete) -+ } else { -+ const format = child.attribution ? attributionsToFormat(child.format, child.attribution) : child.format -+ if (delta.$insertOp.check(child)) { -+ r.insert(child.insert.map(c => delta.$deltaAny.check(c) ? deltaAttributionToFormat(c, attributionsToFormat) : c), format) -+ } else if (delta.$textOp.check(child)) { -+ r.insert(child.insert, format) -+ } else if (delta.$retainOp.check(child)) { -+ r.retain(child.retain, format) -+ } else if (delta.$modifyOp.check(child)) { -+ // @ts-ignore -+ r.modify(/** @type {any} */ (deltaAttributionToFormat(child.value, attributionsToFormat)), format) -+ } else { -+ error.unexpectedCase() -+ } -+ } -+ } -+ return /** @type {ProsemirrorDelta} */ (r.done(false)) -+} ++/** @typedef {import('lib0/schema').Unwrap} ProsemirrorDelta */ + +/** + * @param {readonly import('prosemirror-model').Mark[]} marks @@ -2582,75 +2817,22 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + * @returns {Y.Type} + */ +export function pmToFragment (node, fragment, { attributionManager = Y.noAttributionsManager } = {}) { -+ // Canonicalize so the Y document never stores an attributed-variant name -+ // (`--attributed` is a reserved suffix - identity when no variant is present). -+ const initialPDelta = nodeToDelta(node, undefined, true).done() ++ const initialPDelta = nodeToDelta(node).done() + fragment.applyDelta(initialPDelta, attributionManager) + + return fragment +} + +/** -+ * Applies a {@link Y.XmlFragment}'s content as a ProseMirror {@link Transaction} -+ * @param {Y.Type} fragment -+ * @param {import('prosemirror-state').Transaction} tr -+ * @param {object} ctx -+ * @param {Y.AbstractAttributionManager} [ctx.attributionManager] -+ * @param {typeof defaultMapAttributionToMark} [ctx.mapAttributionToMark] -+ * @param {AttributedNodesPredicate} [ctx.attributedNodes] -+ * @returns {import('prosemirror-state').Transaction} -+ */ -+export function fragmentToTr (fragment, tr, { -+ attributionManager = Y.noAttributionsManager, -+ mapAttributionToMark = defaultMapAttributionToMark, -+ attributedNodes = defaultAttributedNodes -+} = {}) { -+ const fragmentContent = deltaAttributionToFormat( -+ fragment.toDelta(attributionManager, { deep: true }), -+ mapAttributionToMark -+ ) -+ const initialPDelta = nodeToDelta(tr.doc, undefined, true).done() -+ const deltaBetweenPmAndFragment = delta.diff(initialPDelta, fragmentContent).done() -+ -+ return deltaToPSteps(tr, deltaBetweenPmAndFragment, undefined, undefined, attributedNodes).setMeta('y-sync-hydration', { -+ delta: deltaBetweenPmAndFragment -+ }) -+} -+ -+/** -+ * Transforms a {@link Y.XmlFragment} into a {@link Node} -+ * @param {Y.Type} fragment -+ * @param {import('prosemirror-state').Transaction} tr -+ * @return {Node} -+ */ -+export function fragmentToPm (fragment, tr) { -+ return fragmentToTr(fragment, tr).doc -+} -+ -+/** + * @param {Node} n + * @param {string?} nodeName -+ * @param {boolean} [canonicalize] When `true`, the emitted name has the -+ * {@link ATTRIBUTED_SUFFIX} stripped (PM -> Y direction). The flag propagates -+ * through the child recursion. + * @return {ProsemirrorDelta} + */ -+export const nodeToDelta = (n, nodeName = n.type.name, canonicalize = false) => { -+ const d = delta.create(canonicalize && nodeName != null ? canonicalNodeName(nodeName) : nodeName, $prosemirrorDelta) -+ // `y-attributed` is a render-only marker injected when a node is rendered -+ // under its `--attributed` variant (see the injections in `applyNodeFormat` -+ // and `deltaToPNode`). It must never persist in Y - strip it on the PM->Y -+ // (canonicalize) path, symmetric to the variant-name canonicalization above. -+ // Otherwise Y stores a canonical node carrying `y-attributed`, which the -+ // canonical PM type cannot round-trip, and the reconcile loop never converges. -+ if (canonicalize && n.attrs['y-attributed'] !== undefined) { -+ const { 'y-attributed': _omit, ...rest } = n.attrs -+ d.setAttrs(rest) -+ } else { -+ d.setAttrs(n.attrs) -+ } ++export const nodeToDelta = (n, nodeName = n.type.name) => { ++ const d = delta.create(nodeName, $prosemirrorDelta) ++ d.setAttrs(n.attrs) + n.content.content.forEach(c => { -+ d.insert(c.isText ? (c.text ?? []) : [nodeToDelta(c, undefined, canonicalize)], marksToFormattingAttributes(c.marks)) ++ d.insert(c.isText ? (c.text ?? []) : [nodeToDelta(c)], marksToFormattingAttributes(c.marks)) + }) + return d.done(false) +} @@ -2661,44 +2843,21 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 +export const docToDelta = doc => nodeToDelta(doc, null) + +/** -+ * Apply node-level format (node marks) at `pos`. When the resulting attribution -+ * marks change the node's {@link attributedVariant}, flip the node type with a -+ * single size-preserving `setNodeMarkup` (which also sets the resulting mark -+ * set atomically - this avoids an intermediate state where the canonical type -+ * would carry a mark it does not declare). Otherwise this is byte-identical to -+ * the previous per-key `addNodeMark`/`removeNodeMark` loop. ++ * Apply node-level format (node marks) at `pos`. + * + * @param {import('prosemirror-state').Transaction} tr + * @param {number} pos + * @param {Record | null | undefined} format -+ * @param {AttributedNodesPredicate} attributedNodes + */ -+const applyNodeFormat = (tr, pos, format, attributedNodes) => { ++const applyNodeFormat = (tr, pos, format) => { + const schema = tr.doc.type.schema -+ const node = tr.doc.nodeAt(pos) -+ if (node == null) return -+ let resultingMarks = node.marks + object.forEach(format ?? {}, (v, k) => { -+ const markType = schema.marks[k] -+ if (markType == null) return -+ resultingMarks = v == null -+ ? markType.removeFromSet(resultingMarks) -+ : schema.mark(k, v).addToSet(resultingMarks) ++ if (v == null) { ++ tr.removeNodeMark(pos, schema.marks[k]) ++ } else { ++ tr.addNodeMark(pos, schema.mark(k, v)) ++ } + }) -+ const targetType = schema.nodes[ -+ attributedVariant(canonicalNodeName(node.type.name), marksToFormattingAttributes(resultingMarks), attributedNodes, schema) -+ ] -+ if (targetType !== node.type) { -+ tr.setNodeMarkup(pos, targetType, object.assign({ 'y-attributed': true }, node.attrs), resultingMarks) -+ } else { -+ object.forEach(format ?? {}, (v, k) => { -+ if (v == null) { -+ tr.removeNodeMark(pos, schema.marks[k]) -+ } else { -+ tr.addNodeMark(pos, schema.mark(k, v)) -+ } -+ }) -+ } +} + +/** @@ -2722,10 +2881,9 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + * @param {ProsemirrorDelta} d + * @param {Node} [pnode] + * @param {{ i: number }} [currPos] -+ * @param {AttributedNodesPredicate} [attributedNodes] + * @return {import('prosemirror-state').Transaction} + */ -+export const deltaToPSteps = (tr, d, pnode = tr.doc, currPos = { i: 0 }, attributedNodes = defaultAttributedNodes) => { ++export const deltaToPSteps = (tr, d, pnode = tr.doc, currPos = { i: 0 }) => { + const schema = tr.doc.type.schema + let currParentIndex = 0 + let nOffset = 0 @@ -2760,6 +2918,9 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + runInserts = [] + runDeletes = [] + } ++ // @ts-ignore TS2589: tsc hits "excessively deep" expanding the recursive ++ // `$prosemirrorDelta` op type while iterating; the `delta.$*Op.check` guards ++ // below re-narrow each op precisely. + for (const op of d.children) { + if (delta.$retainOp.check(op) || delta.$modifyOp.check(op)) { + flushRun() @@ -2805,21 +2966,21 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + } + } else { + // TODO see schema.js for more info on marking nodes -+ applyNodeFormat(tr, currPos.i, op.format, attributedNodes) ++ applyNodeFormat(tr, currPos.i, op.format) + currParentIndex++ + currPos.i += pc.nodeSize + i-- + } + } + } else if (delta.$modifyOp.check(op)) { -+ applyNodeFormat(tr, currPos.i, op.format, attributedNodes) ++ applyNodeFormat(tr, currPos.i, op.format) + const child = pchildren[currParentIndex++] + const childStart = currPos.i + // Snapshot `tr.doc.content.size` so we can detect inserts/deletes + // appended inside the recursion below. + const sizeBefore = tr.doc.content.size + currPos.i = childStart + 1 -+ deltaToPSteps(tr, op.value, child, currPos, attributedNodes) ++ deltaToPSteps(tr, op.value, child, currPos) + // `lib0/delta.diff` produces short deltas that omit trailing + // retains, so the recursive call may exit before `currPos.i` + // reaches the child's close tag. Snap forward to the position right @@ -2839,7 +3000,7 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + for (const ins of bundle.inserts) { + if (delta.$insertOp.check(ins)) { + for (const n of ins.insert) { -+ newPChildren.push(deltaToPNode(n, schema, ins.format, attributedNodes)) ++ newPChildren.push(deltaToPNode(n, schema, ins.format)) + } + } else { // text op + newPChildren.push(schema.text(ins.insert, formattingAttributesToMarks(ins.format, schema))) @@ -2880,10 +3041,9 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + * @param {ProsemirrorDelta} d + * @param {import('prosemirror-model').Schema} schema + * @param {delta.FormattingAttributes|null} dformat -+ * @param {AttributedNodesPredicate} [attributedNodes] + * @return {Node} + */ -+export const deltaToPNode = (d, schema, dformat, attributedNodes = defaultAttributedNodes) => { ++export const deltaToPNode = (d, schema, dformat) => { + /** + * @type {Object} + */ @@ -2891,9 +3051,9 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + for (const attr of d.attrs) { + attrs[attr.key] = attr.value + } -+ const dc = d.children.map(c => delta.$insertOp.check(c) ? c.insert.map(cn => deltaToPNode(cn, schema, c.format, attributedNodes)) : (delta.$textOp.check(c) ? [schema.text(c.insert, formattingAttributesToMarks(c.format, schema))] : [])) -+ const canonical = d.name == null ? 'doc' : canonicalNodeName(d.name) -+ const nodeType = schema.nodes[attributedVariant(canonical, dformat, attributedNodes, schema)] ++ const dc = d.children.map(c => delta.$insertOp.check(c) ? c.insert.map(cn => deltaToPNode(cn, schema, c.format)) : (delta.$textOp.check(c) ? [schema.text(c.insert, formattingAttributesToMarks(c.format, schema))] : [])) ++ const nodeName = d.name == null ? 'doc' : d.name ++ const nodeType = schema.nodes[nodeName] + if (!nodeType) { + throw new Error( + '[y/prosemirror]: node type does not exist in the schema: ' + d.name @@ -2901,13 +3061,8 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + } + const inputChildren = dc.flat(1) + const inputMarks = formattingAttributesToMarks(dformat, schema) -+ const finalAttrs = canonical !== nodeType.name -+ ? object.assign({ -+ 'y-attributed': true -+ }, attrs) -+ : attrs + const pNode = nodeType.createAndFill( -+ finalAttrs, ++ attrs, + inputChildren, + inputMarks + ) @@ -2928,7 +3083,7 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 +} + +/** -+ * @param {Transaction} tr ++ * @param {import('prosemirror-state').Transaction} tr + */ +export const trToDelta = (tr) => { + // const d = delta.create($prosemirrorDelta) @@ -2964,15 +3119,31 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + const stepDelta = deltaModifyNodeAt(beforeDoc, oldBlockRange?.start || newBlockRange?.start || 0, d => { d.append(diffD) }) + return stepDelta + }) -+ .if(AddMarkStep, (step, { beforeDoc }) => -+ deltaModifyNodeAt(beforeDoc, step.from, d => { d.retain(step.to - step.from, marksToFormattingAttributes([step.mark])) }) -+ ) ++ .if(AddMarkStep, (step, { beforeDoc, afterDoc }) => { ++ const fromResolved = beforeDoc.resolve(step.from) ++ const toResolved = beforeDoc.resolve(step.to) ++ if (fromResolved.sameParent(toResolved)) { ++ return deltaModifyNodeAt(beforeDoc, step.from, d => { d.retain(step.to - step.from, marksToFormattingAttributes([step.mark])) }) ++ } ++ const oldBlockRange = fromResolved.blockRange(toResolved) ++ const newBlockRange = afterDoc.resolve(step.from).blockRange(afterDoc.resolve(step.to)) ++ const diffD = delta.diff(deltaForBlockRange(oldBlockRange), deltaForBlockRange(newBlockRange)) ++ return deltaModifyNodeAt(beforeDoc, oldBlockRange?.start || newBlockRange?.start || 0, d => { d.append(diffD) }) ++ }) ++ .if(RemoveMarkStep, (step, { beforeDoc, afterDoc }) => { ++ const fromResolved = beforeDoc.resolve(step.from) ++ const toResolved = beforeDoc.resolve(step.to) ++ if (fromResolved.sameParent(toResolved)) { ++ return deltaModifyNodeAt(beforeDoc, step.from, d => { d.retain(step.to - step.from, { [step.mark.type.name]: null }) }) ++ } ++ const oldBlockRange = fromResolved.blockRange(toResolved) ++ const newBlockRange = afterDoc.resolve(step.from).blockRange(afterDoc.resolve(step.to)) ++ const diffD = delta.diff(deltaForBlockRange(oldBlockRange), deltaForBlockRange(newBlockRange)) ++ return deltaModifyNodeAt(beforeDoc, oldBlockRange?.start || newBlockRange?.start || 0, d => { d.append(diffD) }) ++ }) + .if(AddNodeMarkStep, (step, { beforeDoc }) => + 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 }) }) -+ ) + .if(RemoveNodeMarkStep, (step, { beforeDoc }) => + deltaModifyNodeAt(beforeDoc, step.pos, d => { d.retain(1, { [step.mark.type.name]: null }) }) + ) @@ -2982,9 +3153,29 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + .if(DocAttrStep, step => + delta.create().setAttr(step.attr, step.value) + ) -+ .else(_step => { -+ // unknown step kind -+ error.unexpectedCase() ++ // @ts-ignore TS2589: tsc hits "excessively deep" expanding the recursive matcher ++ .else((step, { beforeDoc, afterDoc }) => { ++ const map = step.getMap() ++ let oldFrom = Infinity ++ let oldTo = 0 ++ map.forEach(/** @param {number} from @param {number} to @param {number} _newSize */ (from, to, _newSize) => { ++ oldFrom = math.min(oldFrom, from) ++ oldTo = math.max(oldTo, to) ++ }) ++ if (oldFrom === Infinity) { ++ return delta.create($prosemirrorDelta) ++ } ++ const mappedTo = map.map(oldTo) ++ const oldStart = beforeDoc.resolve(oldFrom) ++ const oldEnd = beforeDoc.resolve(oldTo) ++ const newStart = afterDoc.resolve(oldFrom) ++ const newEnd = afterDoc.resolve(mappedTo) ++ const oldBlockRange = oldStart.blockRange(oldEnd) ++ const newBlockRange = newStart.blockRange(newEnd) ++ const oldDelta = deltaForBlockRange(oldBlockRange) ++ const newDelta = deltaForBlockRange(newBlockRange) ++ const diffD = delta.diff(oldDelta, newDelta) ++ return deltaModifyNodeAt(beforeDoc, oldBlockRange?.start || newBlockRange?.start || 0, d => { d.append(diffD) }) + }) + .done() + @@ -3049,8 +3240,11 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + path.push(resolvedOffset.index(d)) + } + -+ // add any offset into the parent node to the path -+ path.push(resolvedOffset.parentOffset) ++ if (resolvedOffset.parent.inlineContent) { ++ path.push(resolvedOffset.parentOffset) ++ } else { ++ path.push(resolvedOffset.index(depth)) ++ } + + return path +} @@ -3087,8 +3281,14 @@ index 0000000000000000000000000000000000000000..2234e5506a5341f39c80f389288823d8 + curNode = curNode.children[childIndex] + } + -+ // Last element is an offset within the current node -+ pmOffset += deltaPath[deltaPath.length - 1] ++ const lastEl = deltaPath[deltaPath.length - 1] ++ if (curNode.inlineContent) { ++ pmOffset += lastEl ++ } else { ++ for (let j = 0; j < lastEl; j++) { ++ pmOffset += curNode.children[j].nodeSize ++ } ++ } + + return pmOffset +} @@ -3360,6 +3560,418 @@ index 0000000000000000000000000000000000000000..70a7ae423be9bfd7a061984ce4ca74f4 diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index f62b6a1abc732b9c13eb83fd667534173706273d..0000000000000000000000000000000000000000 +diff --git a/src/y-attribution-to-diffset.js b/src/y-attribution-to-diffset.js +new file mode 100644 +index 0000000000000000000000000000000000000000..e2184c88bbc4fd72e3a1278d8adc726b8ba5c355 +--- /dev/null ++++ b/src/y-attribution-to-diffset.js +@@ -0,0 +1,406 @@ ++/** ++ * Convert a Yjs attributed delta into a DiffSet for decoration rendering. ++ * ++ * The attributed delta (from ytype.toDeltaDeep(am)) includes both current ++ * content and deleted content (retained because gc:false). This function ++ * walks the delta, maps each span onto the clean (displayed) PM document, ++ * and produces a Diff[] in the same contract that diff-decorations.js expects. ++ * ++ * Strategy B: the PM document mirrors the clean suggestion content (synced ++ * without attribution marks). This function reads the attribution separately ++ * and produces decorations without touching the document model. ++ */ ++import * as d from 'lib0/delta' ++import { Fragment } from 'prosemirror-model' ++import { formattingAttributesToMarks } from './sync-utils.js' ++ ++/** ++ * Who made a change and whether it was an insertion or deletion. ++ * ++ * @typedef {{ ++ * type: 'added' | 'removed', ++ * authorIds: string[], ++ * timestamp?: number ++ * }} Attribution ++ */ ++ ++/** ++ * The six diff types produced by `ydeltaToDiffSet`. ++ * ++ * @typedef {'inline-insert' | 'inline-delete' | 'block-insert' | 'block-delete' | 'inline-update' | 'block-update'} DiffType ++ */ ++ ++/** ++ * A single difference between the base and suggestion documents. ++ * ++ * `from`/`to` are positions in the *displayed* (clean) PM document. ++ * For delete types, `from === to` (zero-width) and `content` holds the ++ * removed fragment for ghost rendering. For update types, `attributes` ++ * holds the new values and `previousAttributes` (when available) the old. ++ * ++ * @typedef {{ ++ * type: DiffType, ++ * from: number, ++ * to: number, ++ * content?: import('prosemirror-model').Fragment, ++ * attribution: Attribution, ++ * attributes?: Record, ++ * previousAttributes?: Record ++ * }} Diff ++ */ ++ ++/** ++ * An ordered array of `Diff` objects covering all changes between the ++ * base and suggestion documents. ++ * ++ * @typedef {Diff[]} DiffSet ++ */ ++ ++/** ++ * Convert an attributed delta from `ytype.toDeltaDeep(am)` into a DiffSet ++ * whose positions are expressed in the clean (displayed) PM document. ++ * ++ * @param {d.DeltaAny} attributedDelta ++ * @param {{ displayedDoc: import('prosemirror-model').Node, schema: import('prosemirror-model').Schema }} opts ++ * @returns {DiffSet} ++ */ ++export const ydeltaToDiffSet = (attributedDelta, { displayedDoc, schema }) => { ++ /** @type {Diff[]} */ ++ const diffs = [] ++ const pos = { i: 0 } ++ walkBlockLevel(attributedDelta, pos, diffs, displayedDoc, schema) ++ return suppressSplitDeletes(diffs) ++} ++ ++/** ++ * Drop the spurious delete halves produced when a block is *split* in ++ * suggestion mode. ++ * ++ * A CRDT cannot move text: splitting "abc|def" into two blocks is recorded as ++ * "delete `def` from the first block" + "insert a new block whose content is ++ * `def`". `toDeltaDeep(am)` therefore reports `def` twice - once with ++ * `{delete}` at the tail of the original block, once with `{insert}` in the ++ * brand-new block - even though the clean displayed doc only shows it once (in ++ * the new block). Rendering the delete half draws a phantom strikethrough of ++ * text the user can already see (now green) in the inserted block right after ++ * it, and turns a trivial "add a paragraph below" into "delete the tail and ++ * re-insert it merged with the new text". ++ * ++ * Detection is structural: a delete diff (`inline-delete`/`block-delete`) ++ * immediately followed by a `block-insert` whose inserted content *contains* ++ * the deleted text as a contiguous substring is the source half of a split. ++ * The inserted block genuinely is new content (typed text plus the moved text), ++ * so we keep it as-is and only drop the redundant delete ghost. ++ * ++ * @param {Diff[]} diffs ++ * @returns {Diff[]} ++ */ ++function suppressSplitDeletes (diffs) { ++ /** @type {Set} */ ++ const drop = new Set() ++ for (let i = 0; i < diffs.length - 1; i++) { ++ const cur = diffs[i] ++ const next = diffs[i + 1] ++ if (cur.type !== 'inline-delete' && cur.type !== 'block-delete') continue ++ if (next.type !== 'block-insert') continue ++ // The moved-out block must sit right after the split point: only the ++ // source block's close token and the new block's open token (<= 2 PM ++ // positions) separate the delete anchor from the inserted block's start. ++ // This keeps the heuristic to genuine splits and avoids suppressing an ++ // unrelated delete that merely happens to share text with a later insert. ++ if (next.from - cur.from > 2) continue ++ const deletedText = fragmentText(cur.content) ++ if (deletedText.length === 0) continue ++ const insertedText = fragmentText(next.content) ++ if (insertedText.includes(deletedText)) { ++ drop.add(i) ++ } ++ } ++ if (drop.size === 0) return diffs ++ return diffs.filter((_, i) => !drop.has(i)) ++} ++ ++/** ++ * @param {import('prosemirror-model').Fragment | undefined} fragment ++ * @returns {string} ++ */ ++function fragmentText (fragment) { ++ if (!fragment || fragment.size === 0) return '' ++ return fragment.textBetween(0, fragment.size) ++} ++ ++/** ++ * Walk the children of a block-level delta (the doc delta or a container like ++ * blockquote). Each child is an insert op containing a sub-delta (block node). ++ * ++ * @param {d.DeltaAny} parentDelta ++ * @param {{ i: number }} pos - mutable PM position cursor ++ * @param {Diff[]} diffs ++ * @param {import('prosemirror-model').Node} displayedDoc ++ * @param {import('prosemirror-model').Schema} schema ++ */ ++function walkBlockLevel (parentDelta, pos, diffs, displayedDoc, schema) { ++ for (const op of parentDelta.children) { ++ if (d.$insertOp.check(op)) { ++ for (const child of op.insert) { ++ if (d.$deltaAny.check(child)) { ++ processBlockNode(child, op, pos, diffs, displayedDoc, schema) ++ } ++ } ++ } else if (d.$textOp.check(op)) { ++ processTextOp(op, pos, diffs, schema) ++ } ++ } ++} ++ ++/** ++ * Process a single block node from the attributed delta. ++ * ++ * @param {d.DeltaAny} nodeDelta - the block node's delta (paragraph, heading, etc.) ++ * @param {d.InsertOp} parentOp - the insert op that contains this node ++ * @param {{ i: number }} pos ++ * @param {Diff[]} diffs ++ * @param {import('prosemirror-model').Node} displayedDoc ++ * @param {import('prosemirror-model').Schema} schema ++ */ ++function processBlockNode (nodeDelta, parentOp, pos, diffs, displayedDoc, schema) { ++ const attr = parentOp.attribution ++ ++ if (attr?.delete) { ++ const content = reconstructNodeFragment(nodeDelta, schema) ++ diffs.push({ ++ type: 'block-delete', ++ from: pos.i, ++ to: pos.i, ++ content, ++ attribution: { type: 'removed', authorIds: attr.delete } ++ }) ++ return ++ } ++ ++ if (attr?.insert) { ++ const node = displayedDoc.nodeAt(pos.i) ++ if (node) { ++ diffs.push({ ++ type: 'block-insert', ++ from: pos.i, ++ to: pos.i + node.nodeSize, ++ content: Fragment.from(node), ++ attribution: { type: 'added', authorIds: attr.insert } ++ }) ++ pos.i += node.nodeSize ++ } ++ return ++ } ++ ++ if (attr?.format) { ++ const node = displayedDoc.nodeAt(pos.i) ++ if (node) { ++ const authorIds = uniqueAuthors(attr.format) ++ const formattedKeys = Object.keys(attr.format) ++ const allAttrs = extractDeltaAttrs(nodeDelta) ++ /** @type {Record} */ ++ const attributes = {} ++ for (const key of formattedKeys) { ++ attributes[key] = allAttrs[key] ++ } ++ diffs.push({ ++ type: 'block-update', ++ from: pos.i, ++ to: pos.i + node.nodeSize, ++ attribution: { type: 'added', authorIds }, ++ attributes, ++ previousAttributes: undefined ++ }) ++ } ++ } ++ ++ // Leaf block nodes (e.g. horizontal_rule) have no content and a fixed ++ // nodeSize of 1 - there is no separate close token to skip, so advancing ++ // by the open/close pair below would over-count by 1 and drift every ++ // subsequent position. Detect via the schema (not the document, which may ++ // transiently disagree with the attributed delta mid-edit) and advance by 1. ++ const nodeType = nodeDelta.name ? schema.nodes[nodeDelta.name] : null ++ if (nodeType?.isLeaf) { ++ pos.i += 1 ++ return ++ } ++ pos.i += 1 ++ walkInlineLevel(nodeDelta, pos, diffs, displayedDoc, schema) ++ pos.i += 1 ++} ++ ++/** ++ * Walk inline children of a block node delta. ++ * ++ * @param {d.DeltaAny} blockDelta ++ * @param {{ i: number }} pos ++ * @param {Diff[]} diffs ++ * @param {import('prosemirror-model').Node} displayedDoc ++ * @param {import('prosemirror-model').Schema} schema ++ */ ++function walkInlineLevel (blockDelta, pos, diffs, displayedDoc, schema) { ++ for (const op of blockDelta.children) { ++ if (d.$textOp.check(op)) { ++ processTextOp(op, pos, diffs, schema) ++ } else if (d.$insertOp.check(op)) { ++ for (const child of op.insert) { ++ if (d.$deltaAny.check(child)) { ++ processBlockNode(child, op, pos, diffs, displayedDoc, schema) ++ } ++ } ++ } ++ } ++} ++ ++/** ++ * Process a text op from the attributed delta. ++ * ++ * @param {d.TextOp} op ++ * @param {{ i: number }} pos ++ * @param {Diff[]} diffs ++ * @param {import('prosemirror-model').Schema} schema ++ */ ++function processTextOp (op, pos, diffs, schema) { ++ const text = op.insert ++ const len = text.length ++ const attr = op.attribution ++ ++ if (attr?.delete) { ++ const marks = safeMarks(op.format, schema) ++ diffs.push({ ++ type: 'inline-delete', ++ from: pos.i, ++ to: pos.i, ++ content: Fragment.from(schema.text(text, marks.length ? marks : undefined)), ++ attribution: { type: 'removed', authorIds: attr.delete } ++ }) ++ return ++ } ++ ++ if (attr?.insert) { ++ diffs.push({ ++ type: 'inline-insert', ++ from: pos.i, ++ to: pos.i + len, ++ attribution: { type: 'added', authorIds: attr.insert } ++ }) ++ pos.i += len ++ return ++ } ++ ++ if (attr?.format) { ++ const authorIds = uniqueAuthors(attr.format) ++ diffs.push({ ++ type: 'inline-update', ++ from: pos.i, ++ to: pos.i + len, ++ attribution: { type: 'added', authorIds }, ++ attributes: formatToDiffAttributes(op.format) ++ }) ++ pos.i += len ++ return ++ } ++ ++ pos.i += len ++} ++ ++/** ++ * Reconstruct a PM Fragment from a delta for ghost rendering. ++ * ++ * @param {d.DeltaAny} nodeDelta ++ * @param {import('prosemirror-model').Schema} schema ++ * @returns {Fragment} ++ */ ++function reconstructNodeFragment (nodeDelta, schema) { ++ const node = reconstructNode(nodeDelta, schema) ++ return node ? Fragment.from(node) : Fragment.empty ++} ++ ++/** ++ * @param {d.DeltaAny} nodeDelta ++ * @param {import('prosemirror-model').Schema} schema ++ * @returns {import('prosemirror-model').Node | null} ++ */ ++function reconstructNode (nodeDelta, schema) { ++ const nodeName = nodeDelta.name || 'paragraph' ++ const nodeType = schema.nodes[nodeName] ++ if (!nodeType) return null ++ ++ /** @type {Record} */ ++ const attrs = {} ++ for (const attr of nodeDelta.attrs) { ++ attrs[attr.key] = attr.value ++ } ++ ++ /** @type {import('prosemirror-model').Node[]} */ ++ const children = [] ++ for (const op of nodeDelta.children) { ++ if (d.$textOp.check(op)) { ++ const marks = safeMarks(op.format, schema) ++ children.push(schema.text(op.insert, marks.length ? marks : undefined)) ++ } else if (d.$insertOp.check(op)) { ++ for (const sub of op.insert) { ++ if (d.$deltaAny.check(sub)) { ++ const subNode = reconstructNode(sub, schema) ++ if (subNode) children.push(subNode) ++ } ++ } ++ } ++ } ++ ++ return nodeType.createAndFill(attrs, children) ++} ++ ++/** ++ * @param {Record | null | undefined} format ++ * @param {import('prosemirror-model').Schema} schema ++ * @returns {import('prosemirror-model').Mark[]} ++ */ ++function safeMarks (format, schema) { ++ if (!format) return [] ++ try { ++ return formattingAttributesToMarks(format, schema) ++ } catch { ++ return [] ++ } ++} ++ ++/** ++ * @param {Record} formatAttribution ++ * @returns {string[]} ++ */ ++function uniqueAuthors (formatAttribution) { ++ return [...new Set(Object.values(formatAttribution).flat())] ++} ++ ++/** ++ * @param {d.DeltaAny} nodeDelta ++ * @returns {Record} ++ */ ++function extractDeltaAttrs (nodeDelta) { ++ /** @type {Record} */ ++ const attrs = {} ++ for (const attr of nodeDelta.attrs) { ++ attrs[attr.key] = attr.value ++ } ++ return attrs ++} ++ ++/** ++ * Convert a delta format object to the Diff.attributes shape. ++ * The result captures which formatting keys changed (e.g. `{strong: {}, em: {}}`), ++ * stored under `format` so downstream code can display what changed. ++ * ++ * @param {Record | null | undefined} fmt ++ * @returns {{ format: Record } | undefined} ++ */ ++function formatToDiffAttributes (fmt) { ++ if (!fmt) return undefined ++ /** @type {Record} */ ++ const format = {} ++ for (const [k, v] of Object.entries(fmt)) { ++ format[k] = v ++ } ++ return { format } ++} diff --git a/src/y-prosemirror.js b/src/y-prosemirror.js deleted file mode 100644 index bb072b6e31a0184a56d7873dcae647f0d5711559..0000000000000000000000000000000000000000 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dc72646ac..dc4915ac4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ overrides: '@y/prosemirror>lib0': 1.0.0-rc.13 patchedDependencies: - '@y/prosemirror@2.0.0-2': 802334aa5d2410b9fc999d5fb5ffef01650a04e58b59fca24e142c55b3379728 + '@y/prosemirror@2.0.0-2': f5850be55fb1ba9d76c899bd8f6454e88773e22d978d3f222bc039b467e2f880 '@y/y@14.0.0-rc.16': 4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9 lib0@1.0.0-rc.13: 328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e @@ -236,7 +236,7 @@ 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=f5850be55fb1ba9d76c899bd8f6454e88773e22d978d3f222bc039b467e2f880)(@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/protocols': specifier: ^1.0.6-rc.1 version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) @@ -4101,7 +4101,7 @@ 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=f5850be55fb1ba9d76c899bd8f6454e88773e22d978d3f222bc039b467e2f880)(@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/protocols': specifier: ^1.0.6-rc.1 version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) @@ -4958,7 +4958,7 @@ 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=f5850be55fb1ba9d76c899bd8f6454e88773e22d978d3f222bc039b467e2f880)(@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/protocols': specifier: ^1.0.6-rc.1 version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) @@ -23044,7 +23044,7 @@ 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=f5850be55fb1ba9d76c899bd8f6454e88773e22d978d3f222bc039b467e2f880)(@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)': 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) From 2c453429963b39560be790ca362c531771b37ed0 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 8 Jun 2026 17:10:21 +0200 Subject: [PATCH 3/6] feat: better rendering of block-deletes --- .../src/y/extensions/YSuggestions.test.ts | 509 +++++++++++++++++- .../core/src/y/extensions/YSuggestions.ts | 191 +++++-- patches/@y__prosemirror@2.0.0-2.patch | 32 +- pnpm-lock.yaml | 10 +- 4 files changed, 671 insertions(+), 71 deletions(-) diff --git a/packages/core/src/y/extensions/YSuggestions.test.ts b/packages/core/src/y/extensions/YSuggestions.test.ts index a66e249f81..8d4a9aea25 100644 --- a/packages/core/src/y/extensions/YSuggestions.test.ts +++ b/packages/core/src/y/extensions/YSuggestions.test.ts @@ -1,7 +1,13 @@ import { describe, expect, it } from "vitest"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { Fragment } from "prosemirror-model"; -import { findWrappingPath, wrapFragmentInDoc } from "./YSuggestions.js"; +import { Decoration } from "prosemirror-view"; +import type { Diff } from "@y/prosemirror"; +import { + defaultMapDiffToDecorations, + findWrappingPath, + wrapFragmentInDoc, +} from "./YSuggestions.js"; /** * @vitest-environment jsdom @@ -118,4 +124,505 @@ describe("wrapFragmentInDoc", () => { const result = wrapFragmentInDoc(Fragment.empty, editor.pmSchema); expect(result).toBeNull(); }); + +}); + +describe("defaultMapDiffToDecorations", () => { + /** + * Helper: create a BlockNoteEditor and build its initial ProseMirror doc. + * Returns the editor, its schema, and the doc. + */ + const setup = () => { + const editor = BlockNoteEditor.create(); + const schema = editor.pmSchema; + const doc = editor.prosemirrorView!.state.doc; + return { editor, schema, doc }; + }; + + const baseAttribution = { + type: "added" as const, + authorIds: ["user-1"], + timestamp: Date.now(), + }; + + // ── inline-insert ─────────────────────────────────────────────────── + it("returns an inline decoration for inline-insert", () => { + const { editor, schema, doc } = setup(); + + const diff: Diff = { + type: "inline-insert", + from: 3, + to: 5, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + + // Should be a single Decoration (not array) + expect(result).not.toBeNull(); + const deco = result as Decoration; + expect(deco.from).toBe(3); + expect(deco.to).toBe(5); + expect((deco as any).type.attrs.class).toContain("pm-suggest--inline-insert"); + }); + + // ── inline-update ────────────────────────────────────────────────── + it("returns an inline decoration for inline-update", () => { + const { editor, schema, doc } = setup(); + + const diff: Diff = { + type: "inline-update", + from: 3, + to: 5, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + + expect(result).not.toBeNull(); + const deco = result as Decoration; + expect(deco.from).toBe(3); + expect(deco.to).toBe(5); + expect((deco as any).type.attrs.class).toContain("pm-suggest--inline-update"); + }); + + // ── block-update ─────────────────────────────────────────────────── + it("returns a node decoration for block-update", () => { + const { editor, schema, doc } = setup(); + + // Target the first blockContainer in the doc + let from = -1; + let to = -1; + doc.descendants((node, pos) => { + if (from === -1 && node.type.name === "blockContainer") { + from = pos; + to = pos + node.nodeSize; + return false; + } + return undefined; + }); + + const diff: Diff = { + type: "block-update", + from, + to, + attribution: baseAttribution, + attributes: { level: "2" }, + previousAttributes: { level: "1" }, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + + expect(result).not.toBeNull(); + const deco = result as Decoration; + expect(deco.from).toBe(from); + expect(deco.to).toBe(to); + expect((deco as any).type.attrs.class).toContain("pm-suggest--block-update"); + // Title should contain the attribute change summary + expect((deco as any).type.attrs.title).toContain("level: 1 → 2"); + }); + + // ── block-insert (single node) ──────────────────────────────────── + it("returns a node decoration for block-insert spanning a single node", () => { + const { editor, schema, doc } = setup(); + + let from = -1; + let to = -1; + doc.descendants((node, pos) => { + if (from === -1 && node.type.name === "blockContainer") { + from = pos; + to = pos + node.nodeSize; + return false; + } + return undefined; + }); + + const diff: Diff = { + type: "block-insert", + from, + to, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + + expect(result).not.toBeNull(); + const deco = result as Decoration; + expect(deco.from).toBe(from); + expect(deco.to).toBe(to); + expect((deco as any).type.attrs.class).toContain("pm-suggest--block-insert"); + }); + + // ── inline-delete with plain text ───────────────────────────────── + it("returns a widget decoration for inline-delete with text content", () => { + const { editor, schema, doc } = setup(); + + const fragment = Fragment.from(schema.text("deleted text")); + + const diff: Diff = { + type: "inline-delete", + from: 3, + to: 3, + content: fragment, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + + expect(result).not.toBeNull(); + const deco = result as Decoration; + expect(deco.from).toBe(3); + // Widget decorations have from === to + expect(deco.to).toBe(3); + // Invoke the widget toDOM to get the rendered element + const el = (deco as any).type.toDOM() as HTMLElement; + expect(el.tagName).toBe("SPAN"); + expect(el.className).toContain("pm-suggest--delete"); + expect(el.getAttribute("data-diff-type")).toBe("inline-delete"); + expect(el.textContent).toContain("deleted text"); + expect(el.contentEditable).toBe("false"); + }); + + // ── inline-delete with bold text ────────────────────────────────── + it("preserves marks (bold) in inline-delete rendering", () => { + const { editor, schema, doc } = setup(); + + const boldMark = schema.marks.bold.create(); + const fragment = Fragment.from( + schema.text("bold deleted", [boldMark]), + ); + + const diff: Diff = { + type: "inline-delete", + from: 3, + to: 3, + content: fragment, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + const el = (result as any).type.toDOM() as HTMLElement; + + expect(el.textContent).toContain("bold deleted"); + // The rendered HTML should contain a element for the bold mark + expect(el.querySelector("strong")).not.toBeNull(); + }); + + // ── inline-delete with empty fragment ───────────────────────────── + it("returns a widget for inline-delete with empty content", () => { + const { editor, schema, doc } = setup(); + + const diff: Diff = { + type: "inline-delete", + from: 3, + to: 3, + content: Fragment.empty, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + + expect(result).not.toBeNull(); + const el = (result as any).type.toDOM() as HTMLElement; + expect(el.tagName).toBe("SPAN"); + expect(el.textContent).toBe(""); + }); + + // ── block-delete with a paragraph ───────────────────────────────── + it("returns a widget decoration for block-delete with paragraph content", () => { + const { editor, schema, doc } = setup(); + + const paragraph = schema.nodes.paragraph.create(null, [ + schema.text("removed block"), + ]); + const fragment = Fragment.from(paragraph); + + const diff: Diff = { + type: "block-delete", + from: 0, + to: 0, + content: fragment, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + + expect(result).not.toBeNull(); + const deco = result as Decoration; + expect(deco.from).toBe(0); + const el = (deco as any).type.toDOM() as HTMLElement; + expect(el.tagName).toBe("DIV"); + expect(el.className).toContain("pm-suggest--delete"); + expect(el.getAttribute("data-diff-type")).toBe("block-delete"); + expect(el.textContent).toContain("removed block"); + expect(el.contentEditable).toBe("false"); + }); + + // ── block-delete with empty fragment ────────────────────────────── + it("returns a widget for block-delete with empty content", () => { + const { editor, schema, doc } = setup(); + + const diff: Diff = { + type: "block-delete", + from: 0, + to: 0, + content: Fragment.empty, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + + expect(result).not.toBeNull(); + const el = (result as any).type.toDOM() as HTMLElement; + expect(el.tagName).toBe("DIV"); + expect(el.textContent).toBe(""); + }); + + // ── block-delete with heading ───────────────────────────────────── + it("renders block-delete with a heading node", () => { + const { editor, schema, doc } = setup(); + + const heading = schema.nodes.heading.create( + { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + level: 2, + }, + [schema.text("Deleted Heading")], + ); + const fragment = Fragment.from(heading); + + const diff: Diff = { + type: "block-delete", + from: 0, + to: 0, + content: fragment, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + const el = (result as any).type.toDOM() as HTMLElement; + + expect(el.textContent).toContain("Deleted Heading"); + expect(el.getAttribute("data-diff-type")).toBe("block-delete"); + }); + + // ── block-delete with checkListItem (should include checkbox) ───── + it("renders block-delete checkListItem with checkbox", () => { + const { editor, schema, doc } = setup(); + + const checkListItem = schema.nodes.checkListItem.create( + { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + checked: false, + }, + [schema.text("task item")], + ); + const fragment = Fragment.from(checkListItem); + + const diff: Diff = { + type: "block-delete", + from: 0, + to: 0, + content: fragment, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + const el = (result as any).type.toDOM() as HTMLElement; + + expect(el.textContent).toContain("task item"); + expect(el.querySelector("input[type='checkbox']")).not.toBeNull(); + }); + + // ── block-delete with a blockContainer fragment ──────────────────── + it("renders block-delete when fragment is a blockContainer", () => { + const { editor, schema, doc } = setup(); + + // In real Yjs diffs, the fragment may be a blockContainer node + const checkListItem = schema.nodes.checkListItem.create( + { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + checked: true, + }, + [schema.text("a task")], + ); + const blockContainer = schema.nodes.blockContainer.createAndFill( + { id: "test-id" }, + checkListItem, + )!; + const fragment = Fragment.from(blockContainer); + + const diff: Diff = { + type: "block-delete", + from: 0, + to: 0, + content: fragment, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + const el = (result as any).type.toDOM() as HTMLElement; + + expect(el.textContent).toContain("a task"); + expect(el.querySelector("input[type='checkbox']")).not.toBeNull(); + }); + + + + // ── block-delete with table cells (column delete) ───────────────── + it("renders block-delete with table cells (simulating column delete)", () => { + const { editor, schema, doc } = setup(); + + // A column delete in a table produces tableCell fragments + const cell1 = schema.nodes.tableCell.create(null, [ + schema.nodes.tableParagraph.create(null, [schema.text("cell A")]), + ]); + const cell2 = schema.nodes.tableCell.create(null, [ + schema.nodes.tableParagraph.create(null, [schema.text("cell B")]), + ]); + const fragment = Fragment.from([cell1, cell2]); + + const diff: Diff = { + type: "block-delete", + from: 0, + to: 0, + content: fragment, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + const el = (result as any).type.toDOM() as HTMLElement; + + console.log("Table column delete HTML:", el.outerHTML); + expect(el.tagName).toBe("DIV"); + expect(el.textContent).toContain("cell A"); + expect(el.textContent).toContain("cell B"); + }); + + // ── author color and title propagation ──────────────────────────── + it("sets author color and hover title on inline-delete widgets", () => { + const { editor, schema, doc } = setup(); + + const fragment = Fragment.from(schema.text("colored")); + + const diff: Diff = { + type: "inline-delete", + from: 3, + to: 3, + content: fragment, + attribution: { + type: "removed", + authorIds: ["alice", "bob"], + timestamp: 1700000000000, + }, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ + diff, + doc, + schema, + index: 0, + color: "#ff0000", + }); + + const el = (result as any).type.toDOM() as HTMLElement; + expect(el.getAttribute("data-diff-user-id")).toBe("alice,bob"); + expect(el.style.getPropertyValue("--author-color")).toBe("#ff0000"); + expect(el.getAttribute("title")).toContain("alice"); + expect(el.getAttribute("title")).toContain("bob"); + }); + + // ── inline-delete with link mark ────────────────────────────────── + it("preserves link marks in inline-delete rendering", () => { + const { editor, schema, doc } = setup(); + + const linkMark = schema.marks.link.create({ + href: "https://example.com", + }); + const fragment = Fragment.from( + schema.text("click here", [linkMark]), + ); + + const diff: Diff = { + type: "inline-delete", + from: 3, + to: 3, + content: fragment, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + const el = (result as any).type.toDOM() as HTMLElement; + + expect(el.textContent).toContain("click here"); + // Should contain an anchor tag + expect(el.querySelector("a")).not.toBeNull(); + expect(el.querySelector("a")?.getAttribute("href")).toBe( + "https://example.com", + ); + }); + + // ── inline-delete with multiple marks ───────────────────────────── + it("preserves multiple marks (bold + italic) in inline-delete", () => { + const { editor, schema, doc } = setup(); + + const marks = [ + schema.marks.bold.create(), + schema.marks.italic.create(), + ]; + const fragment = Fragment.from(schema.text("styled", marks)); + + const diff: Diff = { + type: "inline-delete", + from: 3, + to: 3, + content: fragment, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + const el = (result as any).type.toDOM() as HTMLElement; + + expect(el.textContent).toContain("styled"); + expect(el.querySelector("strong")).not.toBeNull(); + expect(el.querySelector("em")).not.toBeNull(); + }); + + // ── unknown diff type returns null ──────────────────────────────── + it("returns null for an unknown diff type", () => { + const { editor, schema, doc } = setup(); + + const diff: Diff = { + type: "unknown-type" as any, + from: 0, + to: 0, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + expect(result).toBeNull(); + }); }); diff --git a/packages/core/src/y/extensions/YSuggestions.ts b/packages/core/src/y/extensions/YSuggestions.ts index 1a6e0a8029..feb5776017 100644 --- a/packages/core/src/y/extensions/YSuggestions.ts +++ b/packages/core/src/y/extensions/YSuggestions.ts @@ -1,15 +1,13 @@ import { Diff, MapDiffArgs, ySuggestionDecorationPlugin } from "@y/prosemirror"; import { createExtension } from "../../editor/BlockNoteExtension.js"; -import { Decoration, DecorationAttrs, EditorView } from "prosemirror-view"; +import { Decoration, DecorationAttrs } from "prosemirror-view"; import { DOMSerializer, Fragment, Node, NodeType, Schema, - Slice, } from "prosemirror-model"; -import { EditorState } from "prosemirror-state"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { prosemirrorSliceToSlicedBlocks } from "../../api/nodeConversions/nodeToBlock.js"; @@ -224,7 +222,9 @@ export const wrapFragmentInDoc = ( currentNodes = wrapped; } - return currentNodes as Node; + const doc = currentNodes as Node; + + return doc; }; /** @@ -250,6 +250,124 @@ const fragmentHasNodeView = ( return found; }; +/** + * Render a deleted fragment (inline or block) as a non-editable DOM element + * using BlockNote's `blocksToFullHTML` pipeline when possible, falling back + * to `DOMSerializer`. + * + * For inline fragments the content is first wrapped in a paragraph node so + * it can be converted to blocks; the rendered inline content is then extracted + * from the `.bn-inline-content` wrapper so it stays inline in the document. + */ +const renderDeletedFragment = ( + fragment: Fragment, + schema: Schema, + editor: BlockNoteEditor, + opts: { + isInline: boolean; + authorIds: string[]; + color?: string; + title: string; + }, +): HTMLElement => { + const tag = opts.isInline ? "span" : "div"; + const diffType = opts.isInline ? "inline-delete" : "block-delete"; + + const container = document.createElement(tag); + container.className = "pm-suggest pm-suggest--delete"; + container.setAttribute("data-diff-type", diffType); + if (opts.authorIds.length) { + container.setAttribute("data-diff-user-id", opts.authorIds.join(",")); + } + if (opts.color) { + container.style.setProperty("--author-color", opts.color); + } + container.setAttribute("title", opts.title); + container.contentEditable = "false"; + + if (fragment.size === 0) { + return container; + } + + // For inline content, wrap in a paragraph so it forms a valid block tree. + let blockFragment = fragment; + if (opts.isInline) { + const paragraphType = schema.nodes["paragraph"]; + const paragraphNode = paragraphType?.createAndFill(null, fragment); + if (paragraphNode) { + blockFragment = Fragment.from(paragraphNode); + } else { + // Can't wrap in paragraph — fall back to DOMSerializer + const serializer = DOMSerializer.fromSchema(schema); + container.appendChild( + serializer.serializeFragment(fragment, { document }), + ); + return container; + } + } + + // Check if the fragment nodes are "inner" nodes that live deeper than + // block content types (e.g. tableCell, tableRow). For these, the + // blocksToFullHTML pipeline would wrap them in unnecessary nesting + // (full table + block wrappers), so we use DOMSerializer directly. + const firstChild = blockFragment.firstChild; + const wrappingPath = firstChild + ? findWrappingPath(schema, firstChild) + : null; + const isSubBlockContent = wrappingPath && wrappingPath.length > 3; + + let rendered = false; + + if (!isSubBlockContent) { + const ghostDoc = wrapFragmentInDoc(blockFragment, schema); + + if (ghostDoc) { + try { + const slicedBlocks = prosemirrorSliceToSlicedBlocks( + ghostDoc.slice(0, ghostDoc.nodeSize - 2), + editor.pmSchema, + ); + const html = editor.blocksToFullHTML(slicedBlocks.blocks); + + if (opts.isInline) { + // Extract just the inline content from the block wrapper. + const temp = document.createElement("div"); + temp.innerHTML = html; + const inlineContentEl = temp.querySelector(".bn-inline-content"); + if (inlineContentEl) { + while (inlineContentEl.firstChild) { + container.appendChild(inlineContentEl.firstChild); + } + } else { + container.innerHTML = html; + } + } else { + container.innerHTML = html; + } + rendered = true; + } catch (e) { + // prosemirrorSliceToSlicedBlocks doesn't support all node structures. + // Fall through to DOMSerializer fallback. + console.warn( + "[BlockNote] renderDeletedFragment: blocksToFullHTML pipeline failed, falling back to DOMSerializer", + e, + ); + } + } + } + + if (!rendered) { + // Fallback: use DOMSerializer for sub-block nodes (tableCell, etc.) + // or when wrapping/conversion failed. + const serializer = DOMSerializer.fromSchema(schema); + container.appendChild( + serializer.serializeFragment(fragment, { document }), + ); + } + + return container; +}; + /** * Default mapping from a single `Diff` to decoration(s). Returns a `Decoration`, * an array of them, or `null` to skip. @@ -308,79 +426,40 @@ export const defaultMapDiffToDecorations = return decos; } - case "inline-delete": + case "inline-delete": { + const inlineFragment = diff.content ?? Fragment.empty; return Decoration.widget( diff.from, () => - renderDeletedContent(diff.content ?? Fragment.empty, schema, { + renderDeletedFragment(inlineFragment, schema, editor, { + isInline: true, authorIds, color, title: hoverTitle(diff), }), { side: 1, - key: `diff-del-${index}-${diff.content?.size ?? 0}`, + key: `diff-del-${index}-${inlineFragment.size}`, diff, }, ); + } case "block-delete": { const fragment = diff.content ?? Fragment.empty; - - let ghostView: EditorView | null = null; return Decoration.widget( diff.from, - (view) => { - const container = document.createElement("div"); - container.className = "pm-suggest pm-suggest--delete"; - container.setAttribute("data-diff-type", "block-delete"); - if (authorIds.length) { - container.setAttribute("data-diff-user-id", authorIds.join(",")); - } - if (color) { - container.style.setProperty("--author-color", color); - } - container.setAttribute("title", hoverTitle(diff)); - container.contentEditable = "false"; - if ( - fragment.size > 0 && - view.props.nodeViews && - fragmentHasNodeView(fragment, view.props.nodeViews) - ) { - const ghostDoc = wrapFragmentInDoc(fragment, schema); - - if (ghostDoc) { - const slicedBlocks = prosemirrorSliceToSlicedBlocks( - ghostDoc.slice(0, ghostDoc.nodeSize - 2), - editor.pmSchema, - ); - const htmlRepresentation = editor.blocksToFullHTML( - slicedBlocks.blocks, - ); - container.innerHTML = htmlRepresentation; - } else { - // Fallback: use DOMSerializer if wrapping failed - const serializer = DOMSerializer.fromSchema(schema); - container.appendChild( - serializer.serializeFragment(fragment, { document }), - ); - } - } else if (fragment.size > 0) { - const serializer = DOMSerializer.fromSchema(schema); - container.appendChild( - serializer.serializeFragment(fragment, { document }), - ); - } - return container; - }, + () => + renderDeletedFragment(fragment, schema, editor, { + isInline: false, + authorIds, + color, + title: hoverTitle(diff), + }), { side: 1, key: `diff-del-${index}-${fragment.size}`, diff, - destroy: () => { - ghostView?.destroy(); - ghostView = null; - }, }, ); } diff --git a/patches/@y__prosemirror@2.0.0-2.patch b/patches/@y__prosemirror@2.0.0-2.patch index 7182f6d842..64e9ca2c0f 100644 --- a/patches/@y__prosemirror@2.0.0-2.patch +++ b/patches/@y__prosemirror@2.0.0-2.patch @@ -554,11 +554,11 @@ index 0000000000000000000000000000000000000000..d57c7fe0a258d821eb58d615bd93fa3a \ 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..f5b016e30248aa8b3d25153344d28b1acbc52386 +index 0000000000000000000000000000000000000000..b0b2547bc3ccb9951702eb80e8ea52b43b2f2a93 --- /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":"AA+DA;;;;;;;GAOG;AACH,mCANW,IAAI,YACJ,CAAC,CAAC,IAAI,2BAEd;IAA4C,kBAAkB;CAC9D,GAAU,CAAC,CAAC,IAAI,CAOlB;AA+XD;;;;;;;;;;;;;;;;;GAiBG;AACH,oCAJW,IAAI,mBACJ,MAAM,GACL,MAAM,EAAE,CA2BnB;AAED;;;;;GAKG;AACH,yCAJW,MAAM,EAAE,QACR,IAAI,GACH,MAAM,CAsCjB;AAjhBD;;;;;;;IAA4I;AA4BrI,wDAHI;IAAC,CAAC,GAAG,EAAC,MAAM,GAAE,GAAG,CAAA;CAAC,GAAC,IAAI,UACvB,OAAO,mBAAmB,EAAE,MAAM,sCAGwD;AAM9F,iCAHI,KAAK,CAAC,IAAI,CAAC,GACV,gBAAgB,CAW3B;AAsBM,+BAJI,IAAI,aACJ,MAAM,OAAC,GACN,gBAAgB,CAS3B;AAKM,gCAFI,IAAI;;;;;;;GAEwC;AA2ChD,kCANI,OAAO,mBAAmB,EAAE,WAAW,KACvC,gBAAgB,UAChB,IAAI,YACJ;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GACZ,OAAO,mBAAmB,EAAE,WAAW,CA0JlD;AAQM,gCALI,gBAAgB,UAChB,OAAO,mBAAmB,EAAE,MAAM,WAClC,KAAK,CAAC,oBAAoB,GAAC,IAAI,GAC9B,IAAI,CA6Bf;AAMM,0CAHI,IAAI,YACJ,IAAI;;;;;;;GAMd;AAKM,8BAFI,OAAO,mBAAmB,EAAE,WAAW;;;;;;;GAkBjD;AAmFM,kCAJI,OAAO,uBAAuB,EAAE,IAAI,aACpC,OAAO,mBAAmB,EAAE,IAAI,GAC/B,gBAAgB,CAQ3B;AA6GM,wCALI,IAAI,YACJ,MAAM,OACN,CAAC,CAAC,EAAC,KAAK,CAAC,eAAe,KAAG,GAAG,GAC7B,gBAAgB,CAa3B;+BAliBa,OAAO,aAAa,EAAE,MAAM,CAAC,OAAO,iBAAiB,CAAC;;;;;iCAoGvD,KAAK,CAAC,aAAa;;;;;;;;;aAQlB,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAC,KAAK,CAAC,MAAM,CAAC;;;;aACvC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC;;qBA3HG,mBAAmB;mBALtC,MAAM;uBACF,YAAY;mBAGhB,aAAa"} ++{"version":3,"file":"sync-utils.d.ts","sourceRoot":"","sources":["../../src/sync-utils.js"],"names":[],"mappings":"AA+DA;;;;;;;GAOG;AACH,mCANW,IAAI,YACJ,CAAC,CAAC,IAAI,2BAEd;IAA4C,kBAAkB;CAC9D,GAAU,CAAC,CAAC,IAAI,CAOlB;AA6YD;;;;;;;;;;;;;;;;;GAiBG;AACH,oCAJW,IAAI,mBACJ,MAAM,GACL,MAAM,EAAE,CA2BnB;AAED;;;;;GAKG;AACH,yCAJW,MAAM,EAAE,QACR,IAAI,GACH,MAAM,CAsCjB;AA/hBD;;;;;;;IAA4I;AA4BrI,wDAHI;IAAC,CAAC,GAAG,EAAC,MAAM,GAAE,GAAG,CAAA;CAAC,GAAC,IAAI,UACvB,OAAO,mBAAmB,EAAE,MAAM,sCAGwD;AAM9F,iCAHI,KAAK,CAAC,IAAI,CAAC,GACV,gBAAgB,CAW3B;AAsBM,+BAJI,IAAI,aACJ,MAAM,OAAC,GACN,gBAAgB,CAS3B;AAKM,gCAFI,IAAI;;;;;;;GAEwC;AA2ChD,kCANI,OAAO,mBAAmB,EAAE,WAAW,KACvC,gBAAgB,UAChB,IAAI,YACJ;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GACZ,OAAO,mBAAmB,EAAE,WAAW,CA0JlD;AAQM,gCALI,gBAAgB,UAChB,OAAO,mBAAmB,EAAE,MAAM,WAClC,KAAK,CAAC,oBAAoB,GAAC,IAAI,GAC9B,IAAI,CA6Bf;AAMM,0CAHI,IAAI,YACJ,IAAI;;;;;;;GAMd;AAKM,8BAFI,OAAO,mBAAmB,EAAE,WAAW;;;;;;;GAkBjD;AAiGM,kCAJI,OAAO,uBAAuB,EAAE,IAAI,aACpC,OAAO,mBAAmB,EAAE,IAAI,GAC/B,gBAAgB,CAQ3B;AA6GM,wCALI,IAAI,YACJ,MAAM,OACN,CAAC,CAAC,EAAC,KAAK,CAAC,eAAe,KAAG,GAAG,GAC7B,gBAAgB,CAa3B;+BAhjBa,OAAO,aAAa,EAAE,MAAM,CAAC,OAAO,iBAAiB,CAAC;;;;;iCAoGvD,KAAK,CAAC,aAAa;;;;;;;;;aAQlB,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAC,KAAK,CAAC,MAAM,CAAC;;;;aACvC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC;;qBA3Hc,mBAAmB;mBALjD,MAAM;uBACF,YAAY;mBAGhB,aAAa"} \ 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 @@ -2741,16 +2741,16 @@ index 0000000000000000000000000000000000000000..e18bf04380f182a7cf9e14b74d2d39c9 +} diff --git a/src/sync-utils.js b/src/sync-utils.js new file mode 100644 -index 0000000000000000000000000000000000000000..a09e1d95e0f4b4092fbf2559ab60889c58dbd215 +index 0000000000000000000000000000000000000000..bf96bea9f7382248ba10906e1da92a88d04908e7 --- /dev/null +++ b/src/sync-utils.js -@@ -0,0 +1,566 @@ +@@ -0,0 +1,580 @@ +import * as Y from '@y/y' +import * as delta from 'lib0/delta' +import * as math from 'lib0/math' +import * as object from 'lib0/object' +import * as s from 'lib0/schema' -+import { Node, Slice, Fragment } from 'prosemirror-model' ++import { Node, NodeRange, Slice, Fragment } from 'prosemirror-model' +import { + AddMarkStep, + AddNodeMarkStep, @@ -3111,8 +3111,17 @@ index 0000000000000000000000000000000000000000..a09e1d95e0f4b4092fbf2559ab60889c + + const newEnd = afterDoc.resolve(step instanceof ReplaceAroundStep ? step.getMap().map(step.to) : step.from + step.slice.size) + -+ const oldBlockRange = oldStart.blockRange(oldEnd) -+ const newBlockRange = newStart.blockRange(newEnd) ++ let oldBlockRange = oldStart.blockRange(oldEnd) ++ let newBlockRange = newStart.blockRange(newEnd) ++ // A block split (from === to) makes the old positions share a leaf block, ++ // so blockRange returns a deeper range than the new positions which span ++ // across the split. Normalize to the shallower depth so the diff compares ++ // children of the same parent type. ++ if (oldBlockRange && newBlockRange && oldBlockRange.depth !== newBlockRange.depth) { ++ const minDepth = Math.min(oldBlockRange.depth, newBlockRange.depth) ++ oldBlockRange = new NodeRange(oldStart, oldEnd, minDepth) ++ newBlockRange = new NodeRange(newStart, newEnd, minDepth) ++ } + const oldDelta = deltaForBlockRange(oldBlockRange) + const newDelta = deltaForBlockRange(newBlockRange) + const diffD = delta.diff(oldDelta, newDelta) @@ -3170,8 +3179,13 @@ index 0000000000000000000000000000000000000000..a09e1d95e0f4b4092fbf2559ab60889c + const oldEnd = beforeDoc.resolve(oldTo) + const newStart = afterDoc.resolve(oldFrom) + const newEnd = afterDoc.resolve(mappedTo) -+ const oldBlockRange = oldStart.blockRange(oldEnd) -+ const newBlockRange = newStart.blockRange(newEnd) ++ let oldBlockRange = oldStart.blockRange(oldEnd) ++ let newBlockRange = newStart.blockRange(newEnd) ++ if (oldBlockRange && newBlockRange && oldBlockRange.depth !== newBlockRange.depth) { ++ const minDepth = Math.min(oldBlockRange.depth, newBlockRange.depth) ++ oldBlockRange = new NodeRange(oldStart, oldEnd, minDepth) ++ newBlockRange = new NodeRange(newStart, newEnd, minDepth) ++ } + const oldDelta = deltaForBlockRange(oldBlockRange) + const newDelta = deltaForBlockRange(newBlockRange) + const diffD = delta.diff(oldDelta, newDelta) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc4915ac4c..2a0765ae57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ overrides: '@y/prosemirror>lib0': 1.0.0-rc.13 patchedDependencies: - '@y/prosemirror@2.0.0-2': f5850be55fb1ba9d76c899bd8f6454e88773e22d978d3f222bc039b467e2f880 + '@y/prosemirror@2.0.0-2': 31a04e3d011e8a286dfca9f0c8cc95119ee07e497420b9b58a2990945d3b842d '@y/y@14.0.0-rc.16': 4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9 lib0@1.0.0-rc.13: 328c50b547222f0e54a7a9f4f70926c0532c006dec4f24d6954635b8c3adb69e @@ -236,7 +236,7 @@ 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=f5850be55fb1ba9d76c899bd8f6454e88773e22d978d3f222bc039b467e2f880)(@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=31a04e3d011e8a286dfca9f0c8cc95119ee07e497420b9b58a2990945d3b842d)(@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/protocols': specifier: ^1.0.6-rc.1 version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) @@ -4101,7 +4101,7 @@ importers: version: 9.1.1(react@19.2.5) '@y/prosemirror': specifier: ^2.0.0-2 - version: 2.0.0-2(patch_hash=f5850be55fb1ba9d76c899bd8f6454e88773e22d978d3f222bc039b467e2f880)(@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=31a04e3d011e8a286dfca9f0c8cc95119ee07e497420b9b58a2990945d3b842d)(@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/protocols': specifier: ^1.0.6-rc.1 version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) @@ -4958,7 +4958,7 @@ importers: version: 3.22.4 '@y/prosemirror': specifier: ^2.0.0-2 - version: 2.0.0-2(patch_hash=f5850be55fb1ba9d76c899bd8f6454e88773e22d978d3f222bc039b467e2f880)(@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=31a04e3d011e8a286dfca9f0c8cc95119ee07e497420b9b58a2990945d3b842d)(@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/protocols': specifier: ^1.0.6-rc.1 version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) @@ -23044,7 +23044,7 @@ snapshots: dependencies: '@types/node': 20.19.39 - '@y/prosemirror@2.0.0-2(patch_hash=f5850be55fb1ba9d76c899bd8f6454e88773e22d978d3f222bc039b467e2f880)(@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=31a04e3d011e8a286dfca9f0c8cc95119ee07e497420b9b58a2990945d3b842d)(@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)': 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) From 6c77c662f9f8cc42649c1c3ba32a13e06f136c47 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 8 Jun 2026 17:21:54 +0200 Subject: [PATCH 4/6] fix: some more targeted fixes for rendering --- packages/core/src/editor/Block.css | 1 - .../src/y/extensions/YSuggestions.test.ts | 35 ++++++++- .../core/src/y/extensions/YSuggestions.ts | 74 +++++++++++++------ 3 files changed, 83 insertions(+), 27 deletions(-) diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index 10b096bcf2..2bdbdcb1ed 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -768,7 +768,6 @@ div[data-type="modification"] { border-radius: 2px; } [data-diff-type="block-delete"] { - display: block; padding: 2px 4px; margin: 2px 0; } diff --git a/packages/core/src/y/extensions/YSuggestions.test.ts b/packages/core/src/y/extensions/YSuggestions.test.ts index 8d4a9aea25..0aa1e7baeb 100644 --- a/packages/core/src/y/extensions/YSuggestions.test.ts +++ b/packages/core/src/y/extensions/YSuggestions.test.ts @@ -487,11 +487,38 @@ describe("defaultMapDiffToDecorations", () => { - // ── block-delete with table cells (column delete) ───────────────── - it("renders block-delete with table cells (simulating column delete)", () => { + // ── block-delete with a single table cell (real-world column delete) ── + it("renders block-delete with a single tableCell without extra wrapper", () => { + const { editor, schema, doc } = setup(); + + // Each cell in a column delete is a separate diff with one tableCell + const cell = schema.nodes.tableCell.create(null, [ + schema.nodes.tableParagraph.create(null, [schema.text("cell A")]), + ]); + const fragment = Fragment.from(cell); + + const diff: Diff = { + type: "block-delete", + from: 0, + to: 0, + content: fragment, + attribution: baseAttribution, + }; + + const mapper = defaultMapDiffToDecorations(editor); + const result = mapper({ diff, doc, schema, index: 0 }); + const el = (result as any).type.toDOM() as HTMLElement; + + // Attributes applied directly to the , no wrapper div + expect(el.tagName).toBe("TD"); + expect(el.className).toContain("pm-suggest--delete"); + expect(el.textContent).toContain("cell A"); + }); + + // ── block-delete with multiple table cells ─────────────────────────── + it("renders block-delete with multiple tableCells in a wrapper", () => { const { editor, schema, doc } = setup(); - // A column delete in a table produces tableCell fragments const cell1 = schema.nodes.tableCell.create(null, [ schema.nodes.tableParagraph.create(null, [schema.text("cell A")]), ]); @@ -512,7 +539,7 @@ describe("defaultMapDiffToDecorations", () => { const result = mapper({ diff, doc, schema, index: 0 }); const el = (result as any).type.toDOM() as HTMLElement; - console.log("Table column delete HTML:", el.outerHTML); + // Multiple cells need a wrapper expect(el.tagName).toBe("DIV"); expect(el.textContent).toContain("cell A"); expect(el.textContent).toContain("cell B"); diff --git a/packages/core/src/y/extensions/YSuggestions.ts b/packages/core/src/y/extensions/YSuggestions.ts index feb5776017..d568ffb9e6 100644 --- a/packages/core/src/y/extensions/YSuggestions.ts +++ b/packages/core/src/y/extensions/YSuggestions.ts @@ -255,6 +255,10 @@ const fragmentHasNodeView = ( * using BlockNote's `blocksToFullHTML` pipeline when possible, falling back * to `DOMSerializer`. * + * Diff attributes (class, data-diff-type, author info, etc.) are applied + * directly to the rendered root element rather than wrapping in an extra + * container, keeping the DOM flat. + * * For inline fragments the content is first wrapped in a paragraph node so * it can be converted to blocks; the rendered inline content is then extracted * from the `.bn-inline-content` wrapper so it stays inline in the document. @@ -273,20 +277,24 @@ const renderDeletedFragment = ( const tag = opts.isInline ? "span" : "div"; const diffType = opts.isInline ? "inline-delete" : "block-delete"; - const container = document.createElement(tag); - container.className = "pm-suggest pm-suggest--delete"; - container.setAttribute("data-diff-type", diffType); - if (opts.authorIds.length) { - container.setAttribute("data-diff-user-id", opts.authorIds.join(",")); - } - if (opts.color) { - container.style.setProperty("--author-color", opts.color); - } - container.setAttribute("title", opts.title); - container.contentEditable = "false"; + /** Apply diff attributes to an element in-place. */ + const applyDiffAttrs = (el: HTMLElement) => { + el.classList.add("pm-suggest", "pm-suggest--delete"); + el.setAttribute("data-diff-type", diffType); + if (opts.authorIds.length) { + el.setAttribute("data-diff-user-id", opts.authorIds.join(",")); + } + if (opts.color) { + el.style.setProperty("--author-color", opts.color); + } + el.setAttribute("title", opts.title); + el.contentEditable = "false"; + }; if (fragment.size === 0) { - return container; + const empty = document.createElement(tag); + applyDiffAttrs(empty); + return empty; } // For inline content, wrap in a paragraph so it forms a valid block tree. @@ -298,6 +306,8 @@ const renderDeletedFragment = ( blockFragment = Fragment.from(paragraphNode); } else { // Can't wrap in paragraph — fall back to DOMSerializer + const container = document.createElement(tag); + applyDiffAttrs(container); const serializer = DOMSerializer.fromSchema(schema); container.appendChild( serializer.serializeFragment(fragment, { document }), @@ -316,7 +326,7 @@ const renderDeletedFragment = ( : null; const isSubBlockContent = wrappingPath && wrappingPath.length > 3; - let rendered = false; + let rendered: HTMLElement | null = null; if (!isSubBlockContent) { const ghostDoc = wrapFragmentInDoc(blockFragment, schema); @@ -328,23 +338,31 @@ const renderDeletedFragment = ( editor.pmSchema, ); const html = editor.blocksToFullHTML(slicedBlocks.blocks); + const temp = document.createElement("div"); + temp.innerHTML = html; if (opts.isInline) { // Extract just the inline content from the block wrapper. - const temp = document.createElement("div"); - temp.innerHTML = html; const inlineContentEl = temp.querySelector(".bn-inline-content"); if (inlineContentEl) { + const span = document.createElement("span"); while (inlineContentEl.firstChild) { - container.appendChild(inlineContentEl.firstChild); + span.appendChild(inlineContentEl.firstChild); } + rendered = span; } else { - container.innerHTML = html; + // No .bn-inline-content found — use the root element + rendered = temp.firstElementChild as HTMLElement | null; } } else { - container.innerHTML = html; + // Extract the .bn-block-outer element so we don't add an extra + // bn-block-group wrapper — the widget is already inserted inside + // an existing block-group in the document. + const blockOuter = temp.querySelector( + ".bn-block-outer", + ) as HTMLElement | null; + rendered = blockOuter ?? (temp.firstElementChild as HTMLElement | null); } - rendered = true; } catch (e) { // prosemirrorSliceToSlicedBlocks doesn't support all node structures. // Fall through to DOMSerializer fallback. @@ -360,12 +378,24 @@ const renderDeletedFragment = ( // Fallback: use DOMSerializer for sub-block nodes (tableCell, etc.) // or when wrapping/conversion failed. const serializer = DOMSerializer.fromSchema(schema); - container.appendChild( - serializer.serializeFragment(fragment, { document }), + const serialized = serializer.serializeFragment(fragment, { document }); + + // If the fragment serializes to a single element, use it directly + // to avoid an extra wrapper (e.g. stays as , not
). + const children = Array.from(serialized.childNodes).filter( + (n): n is HTMLElement => n.nodeType === 1, // ELEMENT_NODE ); + if (children.length === 1 && serialized.childNodes.length === 1) { + rendered = children[0]; + } else { + const container = document.createElement(tag); + container.appendChild(serialized); + rendered = container; + } } - return container; + applyDiffAttrs(rendered); + return rendered; }; /** From 6befda305ba8d6c2e8e1f5cc545ca84102d1b5f3 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Mon, 8 Jun 2026 17:26:07 +0200 Subject: [PATCH 5/6] build: fix the build --- packages/core/src/y/extensions/RelativePositionMapping.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/y/extensions/RelativePositionMapping.ts b/packages/core/src/y/extensions/RelativePositionMapping.ts index 95b36ba63d..1af7881152 100644 --- a/packages/core/src/y/extensions/RelativePositionMapping.ts +++ b/packages/core/src/y/extensions/RelativePositionMapping.ts @@ -23,7 +23,7 @@ export const RelativePositionMappingExtension = createExtension( position + (side === "right" ? 1 : -1), ), ySyncPluginState.ytype, - ySyncPluginState.attributionManager, + ySyncPluginState.attributionManager || undefined, ); return () => { @@ -32,8 +32,8 @@ export const RelativePositionMappingExtension = createExtension( ) as typeof ySyncPluginState; const pos = posStore( editor.prosemirrorState.doc, - curYSyncPluginState.ytype, - curYSyncPluginState.attributionManager, + curYSyncPluginState.ytype || undefined, + curYSyncPluginState.attributionManager || undefined, ); // This can happen if the element is garbage collected From b668c8b898108ee75bb62191e474906cd6f55b74 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Wed, 10 Jun 2026 11:24:03 +0200 Subject: [PATCH 6/6] test: apply fixes that the decorations approach resolved --- pnpm-lock.yaml | 3 + tests/package.json | 1 + ...e-add-heading-to-empty-chromium-darwin.png | Bin 12116 -> 12718 bytes ...d-remove-delete-nested-chromium-darwin.png | Bin 8408 -> 8829 bytes ...d-remove-delete-parent-chromium-darwin.png | Bin 8503 -> 8809 bytes .../add-remove-remove-all-chromium-darwin.png | Bin 6879 -> 6839 bytes ...emove-remove-paragraph-chromium-darwin.png | Bin 11063 -> 11509 bytes .../move-paragraph-up-chromium-darwin.png | Bin 10665 -> 11208 bytes ...aragraph-with-children-chromium-darwin.png | Bin 12472 -> 13073 bytes ...current-indent-cascade-chromium-darwin.png | Bin 11842 -> 12996 bytes ...ing-change-parent-type-chromium-darwin.png | Bin 0 -> 8379 bytes .../nesting-indent-chromium-darwin.png | Bin 6026 -> 6410 bytes .../nesting-unindent-chromium-darwin.png | Bin 6342 -> 5919 bytes ...ncurrent-row-vs-column-chromium-darwin.png | Bin 0 -> 14084 bytes ...ange-list-to-paragraph-chromium-darwin.png | Bin 0 -> 7931 bytes ...e-paragraph-to-heading-chromium-darwin.png | Bin 0 -> 12347 bytes .../y-prosemirror/addRemoveBlocks.test.tsx | 55 +- .../basicText.concurrent.test.tsx | 15 +- .../browser/y-prosemirror/basicText.test.tsx | 26 +- .../fixtures/suggestionFixture.tsx | 12 +- .../browser/y-prosemirror/moveBlocks.test.tsx | 57 +- .../y-prosemirror/nesting.concurrent.test.tsx | 32 +- .../browser/y-prosemirror/nesting.test.tsx | 156 ++-- .../y-prosemirror/tables.concurrent.test.tsx | 672 ++++++++++++------ .../src/browser/y-prosemirror/tables.test.tsx | 219 ++---- .../y-prosemirror/typeChanges.test.tsx | 72 +- 26 files changed, 704 insertions(+), 616 deletions(-) create mode 100644 tests/src/browser/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-change-parent-type-chromium-darwin.png create mode 100644 tests/src/browser/y-prosemirror/__screenshots__/tables.concurrent.test.tsx/table-concurrent-row-vs-column-chromium-darwin.png create mode 100644 tests/src/browser/y-prosemirror/__screenshots__/typeChanges.test.tsx/type-change-list-to-paragraph-chromium-darwin.png create mode 100644 tests/src/browser/y-prosemirror/__screenshots__/typeChanges.test.tsx/type-change-paragraph-to-heading-chromium-darwin.png diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46d0450e09..ce117242f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6139,6 +6139,9 @@ importers: '@vitest/browser-playwright': specifier: 4.1.7 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/prosemirror': + specifier: ^2.0.0-2 + version: 2.0.0-2(patch_hash=31a04e3d011e8a286dfca9f0c8cc95119ee07e497420b9b58a2990945d3b842d)(@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/protocols': specifier: ^1.0.6-rc.1 version: 1.0.6-rc.1(@y/y@14.0.0-rc.16(patch_hash=4c87657af127f4fcf167b812054b3c34374c63cda90c4db9a831608c23b89df9)) diff --git a/tests/package.json b/tests/package.json index a276b22593..60a5a3e4dd 100644 --- a/tests/package.json +++ b/tests/package.json @@ -31,6 +31,7 @@ "@vitest/browser-playwright": "4.1.7", "@y/protocols": "^1.0.6-rc.1", "@y/y": "^14.0.0-rc.16", + "@y/prosemirror": "^2.0.0-2", "eslint": "^8.57.1", "htmlfy": "^0.6.7", "react": "^19.2.5", diff --git a/tests/src/browser/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-add-heading-to-empty-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-add-heading-to-empty-chromium-darwin.png index 49010b1ccd614a1494064f0e88246f57bf901ac3..f2df9ec38a94dd7e12502e18476ece3eb218e681 100644 GIT binary patch literal 12718 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 literal 12116 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 diff --git a/tests/src/browser/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-chromium-darwin.png index 271c80528c2d68c5ed669ca123d285b5a1966324..398f5afb9f6ae8eeba9ab226df9f1c002a008bc8 100644 GIT binary patch literal 8829 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 wyGl|4Xt-n*n} z?}du>yp@Gv!ry;$2)zPJtez?z4|5ICfL&i0Pr(DB_%u%fH9{vP%H5NIkilqx9CEQJ z5AtoAG)Nq{U5|qzg=+Y9FlE?v2CIG_*fFN#|6QHh%KV2dS?kOwlY>pzJA?DGbOZ}h8 z+L@u|Kh;+oPfkre6`a>U52_2D!uJ1dvOf_3IndVCeFR;EE*!heQWdlzh{u0E2nkuw zM}&FCpvL3dh`jOsMEHEW+==n-!#q-7iCHhikv3XjOj;-64*@QzS?J5;DR)o%*#3Ee z0KX;;D@d<|Q#Et!|8EY7VJ_-#9jdt3*wVrc7#@Y$Wdxh-eEVDW==XeUlNNCAiL&0J z;53X1?Wh1N#;qXXz-6yqzC46Z1?QT5{9yYmm?S0%Cq|TOuzJY7Ay`L~p1An<(R^PL z77f2@q41$3(RP)-QXsr1(RLsTPBAe22>@_ZMIM(ys$&vjbr{a&!{h-9CND+C3k|x& z{5CxR2Ad{-43vn$d>zpWx`f&yP42?9&k}eA5j+&CEX;(^k;LrVUZ$s`PXxi3TL0{N z?bQGA3D{W?i|TzYRXd)7U>3ktia|LFO+dxNkgx#!4|tviP{z=ckP4XljpSR;^;Jc= zck7AY{u*)3PK(bC^%Cy9}w!KGIi&smSBv z0hgin*5_(B?n?V&C-+%*I6A#uPys zsHmzw&&nc4^8pwqj46P5W&$SOzkffxISJibs3O257L@^pJ-D;6#$a01cK5CCy%lmE zTifz{JGybRw8q}Bqq0NIjib0vszP;>o;uJ6_4lNtST9I6))o&64Q*~oH?`p*16a8N zE(250?r@%I?4WzQ9P;F9PEIID;P(<_eia9*HMX_!g2u2X_|X%E9bhPpZ;a(b`NPO{ zimMvqRrT&(bbNi1f)Y%yo@uMO7=3}oBsOhlheJsD>-nM>kLGs})rQG@xEJVVG^NGX zg0x0R4TD%I_bJL@J{Z0kyH?NLD~Px{=L46a4G)RhsX|%(2gv$kJHF2 z*7XZW8R%WJxV#U4D>#tUoUpiBG<@TdcwBJl-Dy1jU7qhpKNjQ4zNW$|_L|2N3@J9w zYh-I|vX=ux$Tx@fWT1YaYp~rPK(e4=<}t9^zNEk3+HgIYFR{Nf$r{hSgJ!knZ5V1LL!o+xZ zOjLS%3(JcgICuU1wk$S?LdBA5SKV zO}l;VNyu_+|5FGkIRK03oeI3+06Oo?w^oK$M0O8`XcN8mgj!)r|16+I)ZY5<{;Ks` zA5;dh3jK`ItJR+UCPUMWKZByJFy5CyrpX{5X_ZpTgH!A-7LFu*p0Osj+n%s2bz_qc zd-b66aeJeVBo)i9(53zNmw#)^ZmT;Oj(^_%b!gx_BSM)cr%BrIVo1b)8}}{86wU5I zg&-OM)&M>)VMbQ?rj8DJe+jP|>VXhJKi5T`{-ejWU1eOGlYGx^zO3%TG`a~0dm z?ZGAUQ|60HEV&($qA|+;Tf?}xRJrD-U8n3K7_aH-ibH+$u*fXEnCW_b5-e&uhy|G0 z0O$}&C_*H9A(IfmJvkUG9btpqrhoLD@9W4hl>vi94m2)I2|oExv70StXt;gr%ClVN zE%hvq?UN^s9V?LVZ4hYJ8VL?_?nb%LJHc;m&pAo>Dg^%L;|o|F z>iI;T6NrAY{onoV|NkLE|Be1m;lKE#dk0^+v{ZYax-SfWR literal 8408 zcmdT~c{El1ziyr=p+ZFFMCLL^1DQz_y~!BTAoGwZndc0d5~Yv|(ZETW=M#FVka^BL zhfMd`z3;o$U3c9-?)u$z@4EN=VV$$jK6`(M&*z!8fV1ifJ9q5gv2NYEol1&w=hm&; zB#!s)+qU3uvHs(+b?Xi$DaoDGc3MAHyxs1CkLP@Mr^}2PyBWJD$H9BMBTn4N2$tV; z-}BjtuN=vOJAI=Un`mNu%%VuLsB_GAo-tdUO_S9$FsSdVj;yY#>N1W# za;YL!?ZLKPyUfVZM_g&Mnu0hF9=vm_H_klKsJ9~I!+eKJS$X-?>0cuTYc-$ZM65}A zSIo>BGA@b5w-&fbiI~@w4bEiS4V`OfXjpis+SoT$$ubxuP|)?W^_F=>FoVB;)!X{6 z9d9ifeU&1)!XG?Pv9#phxP{Vib$M~L<+Y!|>U4~1lFR|@NqCS&>$@WwGK(A}5~*)s zV7h1{)xGd=&lHU$8A=wJgfCyTf6Y(S&u65k2i)h?nJ%Fan7q8wTDZa(-PWd8Q&W={ zwKP$fg!j|a(|Jt9B6zGVbuPYpcdN6mz-{Re4^Ow>wPo3J}e)8nWOjLHPjBzk_0EFhwJA@ZetvI%eU-Yybw;g*Om^-K5j(`o+nc{*`}S^|O`A7clP+ZG zt6jLTPp2m%x1;CBpHJ+@zTfxQQe0fDr+oJ8?#}c0!^Sq<+tYWr^|d)wwCBdHJ@cze z6SjR-)YCIF+i{)8e$2_&n=*AfTqcgcyV>eX?oWRF*wdmh_0P=9QaDiY{q6ODz`$gK zLiaY?`s4ms`mmtV?xJ)0`gOx!U!-SdrlzL)sy~zzav0TBZM@QwV>;TJ8}{hYL1vF- z>-Tm;B~d(D;?C0?m*3lU56Z4d1noOwJlR!D$sp`a(SprBAmz}vFxC4~j}$@NwXM6< zFHYQ=ja?!1O3|J53H73C`;PokmOoLTGC$sKaqU{NM5E}%cQ+W8evYvpI^;Q(+!`n5Xf=WcQbE--K-kRQK=S zzisDE)8Q}aSn|(pp$DXN-(K6BY9Wl$#!6qr2brWWA(M*9X|jNpu5J~3m~=^z=f)P2 zE%%EH86|@=LS~=n_1zaucTi9`_k}Cai(4Ix{{HT{VgwD&h;jK;uF zLzqtHku5sa-!d*a52vRsOqNnz%r@TKn5tH4Gu@};zDPD`z{c+5*Qa-+xWivCbwbhBWy7Bh>Y}E(HLb#J$Bb;Xj!V?qg9y)eZ$BCHd&D2R%*VUQ*nJoWd z_1B2&?4JrRFTVDrkZ_tjI5adA%zS*m3QCwc%x^&7W5vG1ed(sYQ}<^6E8}gX-~&?8 zQsa4Etfzggx{B7vKR9-CKherF1GVhOk3W=}_z(%KuqtRw(`W>EBtLoLJWd)UaPGBP ztuY4Pc6%g4H&r94ZnC>nNG;N3oaBe5+$W&_DS(zcZ*6r+qH`>_lNddQIPfK1tIeVH zCX}P_DdZ!HI zx#rt!sQ@y3`X6Fxxz(-KR_1A0STs~sDO*}vR1>A+u&==xmvX1;q$aF{jvhVw&x!TN z=g(?um-$fc7xhn3iHV4dt465}br#A>NJ!{jc&S^QI@%o$q% zSa56C#r_zyDOGN3?UOK>8ti1vbUzvWx05KoREvgpH!rcVvrjomf0%7h`Q7x5oolO( z9d7fN6P*VbfXlH$CR?y3i!+0mWfbSm+57Xntk1 zk#4#{ML@{z$602#S~(0MewQc*)=aNxnM ziu&FuL622epka1dMa8*4{-^=1h*MQd?7YzJN17;DQdCn*dHy^QJZ7?e=g#tJ^oMPy+dM9& zeE$62rTGc-!McZ|&DmakeOG|p0bya>K<$!}Q>RX!&Tez+4f(J#WU#e>gwu+x0dbuRrK_jGX8km<2pAtH`+#u3}F<@v1sIyv>*2F_o35&L|RyI zGOJHiYs$PVVc_g6VP2nD-thc9Z=H^|wj2}$3h#=ksr6{Hkg$k|lDhhU>*C^~ZimO3 zTTV`n(`4~hhU2$IQ!Q)?mX}@m6hfI;d3l4py*J@xYjGymOTI>i*7@yv{zmk=KuO}K zp-d8|fF0m5OtHsGd@?8~sI(zni~FvZ*CLmal9Db+TfB31Whwj3)jv0==<3o(KT!y? znf;;p{P}Z1HQJts>`;l)6GH&r$X7|;Zq+UmiL(=ZYoqj^rV9`|Lgneosh!M55x;!XrlK$d_2`UJb z)-&H!xO$(Q?Y?sROvwK7TCvVx&clZTPkL?1M%g<|bP#JkC1u^SX{MLguxQ;41H*i@x3 zWy1xkeNR&xr;?6*dkuf zV`Yx-(!1cmz#W+6Y1n``L8A@1o$gXwshF>fe9Peb7)Mm`w{+MKgU$`^_d~tuM9P$ z$SEj@fZ(F}FV|p0C!M69rlwB6Fj(0ECUlwjSXtsj;S30llXe!?)YL>r=NOf4ky)A5 zBF4YkZ?Nh^@Oa^JzW^bG=?q-Mw=z53A=*lG9_B{`Oo+~dT_`Indu3R>AwE7{kkV_{ z0m;8VSV=y7bE72!B>MTw7jGkayC0uD|NhW8M2O!WUq;I=0gpurfL&iXok5@HR_22s zN>G1%L@kCL9J9(R$193 z=ww2mfFV4VT05)AWOfiK>X9hDeSV8a3(U`-Kc67w7&?+w$V>*pDXOYI1#3e9KnExr z8Xg1-C6h305GKI?HzH)be;@C)mCB~wzKLLSMa6CI=5-0Iz^UHKu*m3WUziq{34+o< zW8K}|t_I2~DoToqUB+M&d3kxe(dKZlD*=7~@l-u!d;6pP{rxzF+OO2pz}i{}7FqnE z`Z8z~rOLs{NsT3t4TO7o_3GZ!ryLtLZd^PDPzwkRt?0MWJbU&8Ru=1`7g#^oOBN{P zK%qi9SQo%<<8>9Xj4*0_f`z`A&F@zTZ0=vL<^<1w-q!w49F$lLC)d`dym_+++xqBo zzQfbBG+IW+Al$S{45s!!{bsp$)2D^~vc~ATe#q5Ln>L-*)GQ{P6zT<=!hzm74VE>Y z?yH_1ZHesaG9>J6Qhh^;iiB0yy7lYVYin!20(}uC7G@YKWBDF_5Q`o;?(&mET%4Xz zv~jeLp(BF#AKg(16P0xFwb`E^T^s=z3y*>DGHc7W*gkxmu7B}gIz;2@BKvI3>uep(y_x8LHTLkFep1s=8*qBWq7-$i7QcWhCz%AO1d<)6R5zcx2 zy5;S4I{0h?CZGn4>9?4gnl5)!c^D#|c&VFx7yBydIQ~gX~(rM8{m17Pj>Cc`SLR5PD_z1cDynzX-0uyPuREtE_2$$tg%}(fj#$z{%AWV7q z`MIFr03)&1y%iX41sFWrZoDl{!gjzg?zqdbCuc%Vp(sX+#~d2Je365@jRs3iRWRNX zywNNOld1@sqnCA;w#;Qz0hj}{*1Na7aMF9BdD0y%!O2*YxwF zle1&3F?o67d`KbsuwTiK9^FAu#7Fp5CJCCtgajr?3kESuCU=zarw3wT#a>%`=aFW` zGv4|@9YpRo2B0pu%D^=!sj8Mm#KtOuZ2sDE;0R%WQCE-$uL#qJ7>S0C&d;>3D*OXB z1!mx0e0(4$9Bw$fLQhO&aH>Ri=|DjO{~+}iI5|1HVAs#?c!BA|#oU;9z^j$(AT%oZn zb02_|6J5q7fPmSXwr;f@Z@9l@R`BN|;D756{BmL)o%?_0U$eQ0+(1wJB;3_##8SY3rZAn-IM7aZz8ZwVFGk5)k$%|*O zR{cY1Ndt&^iiKSltRSKCqM&_*&Fh$m=mdGf)a{Q?Q&NO5Nq!xWHWBG{m-t}budaPP zObo>xp$558L$(%4j+ZIoIRJ|_&&}3%rf44`9)bSrox821qGHnUR7o$hsOZF*P^M~N z63D{n>+|#2QN0&T15NjCU5V!c0?WzEQ`69tFv-MFX^`$XiY#O_0_i@W^fqoCvO%dTX8 zlA5}uP{+XSesVG!f5pgLj|~YfolHcPnwr#fs0Bief~1LFc3X6nDUiPHZ(_idS}@LJ znH$s->C?v|a|e4lF`Ir4-Izz;C_1&Mx~!gbI3+vVGe}rXS*4()*8#|7%&p${97zZv zC_aBKLVUWNf}*qmZ{ddH5Fy$QeCoED9dCaCZ%ITavi!uHez`tfd&3LV*L9TL2Zc&#kNt+2v0pn(Z`qxu5GGaw^NWd_*WD?eg7{Yw`aOh$!owR@#BS+Y zY3DRGJyS9>Ly;QUvSKq-V+B2d$3!L!mrRBeAgb2oMb-;;4vwgzM+fWZ_J6+{^@efV zwrxdOFW_rE&O}LGXFWF9)S);y3 zs{-`wk-$Jw+LFc#mMkIG{5W}d0`PDOcJtHC-1dqTA_aiCTb+?zqb(Ku(U3wBjje^H zVEg&?tbu~!RSJF`J2HLpr3&Ni+i#vc-+ysWW;0};X9S(UZ@ zJGS2>$YztDHzd#^*UmY6Jv!!-woN{M|17jU6p%o{!d(h5H({s&wmn3`a33KDo1oym zfxir~@vS3&cx!f^t@nwQVR?J>E9iMuV5<(EjCEUyU#;sF#3E<2ZzK&vh6R)R*${io ztSr`ha3P0+FE4lj#R7vUXNyo!o>;{Ud0qO{dWFR};W{1bcL~X~l|-zj^Y}=~vn(kE zTa1*LN4Te+J=_uT(1duHL*$1{vhE3xP=qAJv%nIQu;ac)^sYKY=m@I%usHAl4Xud? z8_@zbp4x?Cn^i=!kWWNb;33{4kFy^$$B@h88q*G~4sY6i^E-yWkjeHW8EMp$M=-+6lrgrJd zpgxWkMvO-$C$jHv74y!D+Z@OGLpOTz>7j*F#@vlzd;SfS`f&tzFeN-Dc zpvWeID#y5rhRV9F@F3M#LA}p~4jeR}0nv^HMgAp7S#Ybah(PPnPybEK4p`1Y?1n4J xWoqBJ!H2<5mkRwqc?$i1@WJ^1+K`rHP3o^@h35J3<3ASGDaotLWu7v;`)>x#;$Z*) diff --git a/tests/src/browser/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-parent-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-parent-chromium-darwin.png index f9d175699120bf5c5405604d13535d435a30649b..c1c52bab8a9da863aa01c5bdfa924fcd5d814854 100644 GIT binary patch literal 8809 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 Q6#;>=B3j|&wFkcc0E#`R0{{R3 diff --git a/tests/src/browser/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-remove-paragraph-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-remove-paragraph-chromium-darwin.png index 51b9bbeda5d86b5d278418ab0f3709d77d9fd38f..efc7c596aba3962db44141fffdee3bf9ead482b1 100644 GIT binary patch literal 11509 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 NNh_UAJM;I={{k%~sQ3T? diff --git a/tests/src/browser/y-prosemirror/__screenshots__/moveBlocks.test.tsx/move-paragraph-up-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/moveBlocks.test.tsx/move-paragraph-up-chromium-darwin.png index 8fee64373ec052a76fd7c338097fcba51e155de4..940ccb30c01bd94445596ac614c4feab863a0232 100644 GIT binary patch literal 11208 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 literal 10665 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 diff --git a/tests/src/browser/y-prosemirror/__screenshots__/moveBlocks.test.tsx/move-paragraph-with-children-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/moveBlocks.test.tsx/move-paragraph-with-children-chromium-darwin.png index 9875524537f8302e736e933e996bdd31bb7b2f22..e6c6df38f120b0258bb9f389b830aef8d6a066b7 100644 GIT binary patch literal 13073 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�xUme0`6BO$G({qvX2qIC6l&Vi!{8 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 diff --git a/tests/src/browser/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-chromium-darwin.png index 2e8a6e78ba8e9336ef485d37917791673ed6349e..dc840f0f5874d913600be658d0c307e0fad70d3c 100644 GIT binary patch literal 12996 zcmd6O2Q-%d|94BvN(yBaMMZYmR0vr`MP)=Gkv(o%l}(5eWtF5vgk(ncrYMrVBU`e0 zURU4o{MPxO=Xaj-ob%l0+gEYj*Zmpq_iKGFzNb~?DYx$4x@OH9N<{@(^)+kA@YgjD zHgClLYq&h{F9Cl=+2dzPYex&oo$Eb&RwtWh)5r63TWZ_9p3d}176xOR?u0a*5 zn$w)%@jz8T4xas;5YhOElg8}v3Ua2i_YRAIHc{-~HJ7*`IoLLK& zXPf1Q-nzVxQS+Z?mi_Bh!nw=m?^nIRHk!X)F@}5C{(4DOAN%WiS?7nh|Gs{Xd;MRR z(@b92_}ArZJ?RvGUtePUUw=SndS^GMn9Tr1y~H$yKRvHe>$@Ay%QMYpEoq)SIyu|u z=)8P-9*9dMPBcowtto9T#tlHs4$ylZ~ z)1btgdi~3xp5EShe)G%-$LWE|Dy|ev_xNC4l&GG4E^{#-ABFpLhDjqwSrBV|t#Iev zf&yulf4g>aq3^yLX{wU5vbOg5mGZ#5tkTi-(yNg)>?bE=RAZ!SOMGcB6u3%0I~fq0 z?@3NCVA{lOU|{h5UKbWExiH%W57?>@J7);*WSUsnC0 zn%j5mC`sx6mjU_sEIp>ElfhUub?kQf`ue`T_UVpEV|=RfymfNk)Tu~SPBymuqR!5k zPtWA4`ID7xzy$|nhw9X}r*o&V#cW!Bimpfv^-?%)s($utNxVwLwU;)7b^ACttcO1zbUl>#<&=WLW(-(_ ziAvV_oVGU8kt0X&)9tLRYKiAxUu(}fa;5BU_p8KYwnK;f^s26BSPw=ygoKAzBxt-K z{rn_fSzZ0iqBDP_vml1G$5t%i<;xGgqm-C6C z_wH@OWD;J#?sv|cs^T*H`sVh+w|6&`&YYo%IC>-YXf5-nPo`Jt$+eD&j#<3bi4k$R0jH0%nhsvL(8 zQ_=+MMHD)2rJ^b;=A+=IBiGm0|J>B1e(93G{PE*!S>2ZShlYm2<9)qN9y?E#(mVfZ zF=mo-W}wb&t!;XJsj{x_q--4j)-78K*(D?*7rtd$1T`1BZz5X3>DNdh7bmATX2?Xf zY5)HHadC0?u;SQN%%W)2zVYMmm&D0%X*a2v>~`~a=Iy7>o!dEsqUE>!ewst&u`K3$ z@ZiCb_S~>^<9hU7%xPWSz%X>v^g!){u&~CDXq&IO4!kVoL9Cqn_j}gVC<_P(U}9xM zwg!(mf+VIkD=I3Ev}M!gwg)y(_Lk#cdj$pWkBpc}E&tTGZfbh)z=6w+@hYD`f9|ps zGwH|^wHc@>?CfNve$iMtP#bZOm-od}W^wy_p`o7o45VFN$6Z20Ly7%pXi(oRK(Rgu zP!JpvatGxl^dKap2o>~_7yTC`wcx`pAK0bOuu)gsVQe1;hYyu?RzV%Hba!`?_>F(_P8RbAbZWyLNkDoV;*U0GC)I2uswO^tQfH7rQs zz7WVFSw(=4sw$Q4moHzE^a`Rse?F@kEm65RUQqwmq*26q4tNmxRH~S7Ld1TA0Uu5* zlnlO_wZ02ju(q+8|NbmU#JX=Yi^hRvX=y1IG=Q8+7lYuB!Ab6Z`WTU-=$TXD{l zn(w0pR!?pwColdVOQ$zDI=Zf4b=iW7RZ7|3{-`dBO&=@qIrB<66*`=cVm(dp>IV0i zgY4|?gV9b@x$SkY&LwW%vZeb+N`X%$|aO_qR@Jo}NagT3w#C>2#UZ&$u|>T(Bayy0VOZ*u%>kWZs^W3Px;7)=_dn8TYjq z#~9-MUq|ihh_4j81U!C?w%P#bt$&SN*nQ~m;eLPK{H~|oECvL`oIQJ17MlUoyE55R zD)IB93<2bTDcxA@=C6^;KxXlm7QiTc`(v7+k}MAgPnbWtc{?MclC`yfp7SKx z3$>VdEH%Lq{8!4}xGdVE=oB%=Z0u*&@~_rMB35jJD5WAsi@ZY6J!$5P=1^-UFUxGZ z1MB*Y6d(Gn&vZVO@&4XfAPvCjnVanWIo=tgc%Nf3#ckQYd5PCB?cT5b+rHC7~00m}7+p_QD+ZW%N`T8ua#y)$N-T&~? z*EB=xXDoxk(Dfqj%eL z9c+HQ&Ll(%Me!4-*|EI&fd~=>v)BXBE76HycmMK=V0K5qJ%J7oN`LfIf}B5HNoi@f zt=I)<1yuKg2M<2JcXEi0jU}{B^~1x(>;N-OJ(tiAAC4K-gckzi2^dXI4xE^<>>%}0 z%E~@|3}Gu2TT`gqDG!0$c?ikG1>JboSi2RD;s3!1Szz;lzY- zcWSAw_y}`@B9ARlCLPYxDtXB{!-oLWoqZoVbettg_5)u+F;V95s9>ggk5^m!-T`RnWn)*e2yjM}t3G}Rt3^X-? zg%Xn`v;@l%1h8=a^~KWL>o;W$#YZqmkod5?&tAUtnV%lCDc!00@wpN`<}MFGrF-d} zS@-7z4T+i08jz0c*8D^#V+gwhjriB_AW@GWV<1g4l*Zs5aRY z-S=62Axd+D(yNtdo{4a*7sPm3eSOdZ1nq>MuWw=R!jF_ePkP-CS0K@uByT=|%-~Rq zu0e-wfsptC6}=B?00qC5ocuz%5hXuAKWS+u#jXGLCYBH8nduJWx&~LSOn(3LY}06K z=HMcBQ_OBy)|-lXGc`4&Q^(UMPi}$DiTVNk?ZVVkQ&UMlzkhNE?lhj3mEADg=_aiv zF}Wee<<~B3<%|4tiRZ1lKCCs*8R8&jn`T&j;Lsrhz@)ZS=}z0B22Y588fNBzX6%xP z-S7@I$(bF6g@ry9*(%WzyBdWqyt%xV)pc&KaHq52j3|`1)98_lndT~&FYh53nv2UH z^{H|0927fOMMXtdS~X8^=jw{^>WtoMDR?13o?zbsH|Ypr3l0H+ke4qH5W*c&R`Jv+ zgQ>ns!ZLuuWo1`rDO}_6W5S|H&bAfj*!>_aj%E%ur?6s{%8!M7LqfI_0Ir205ip3_ zbDrz=9{HBxKRkS0x$)|+kru-5`1lAN#S}=NzkE5PM<@$oPLL~oW4Yr-_{6J5M$oe! z?dF3hVZ-KR9a(w#UYhOO@6;MS1GF+g&Qw-b=FN6E_4V~_+rFI;`jE_}?0kIp4r;`2 zqoplYGfdU86pJE($|;r3K1_?W(=Gl{Tyu#YqcndimAec0B|ku zZZHyBO$Y?e1FI2lGBxxfhS2;jbpB+7LR6axD_HUbK~8)#O~E@7l9M^a#2!OL65ce3 zMH0@U=f(BLcuE#Xy3b)DAt$gLZ?w2!le;WM6jW3UffPIF=z93AdSrRB`?lu+wTl~` zD~Ve5Y+##OPR<_K-P6-U0*Da4)3EyC=deeQ6rqdlbEA}a-#>u)C2Cq8n-Azp*2&$z zdv_I4QxGS0zy^~~v1IEvQC-E}lfd3(fy_z<25i`lV#swSK>9v+y_a!u8-maj0>J%f zw-v`|r!i3iKARzfDr;)`K$cu^IRX=K;x^Kf7SoVgbHh z%8e!LFex8Do`&T;+A1@7ouPixd#|bLkUbxPfs4;@)9s_ z6CoUHJ_ipERnXnL6nTU`0h|IHp6C_0qWK!0$x{_|CFL9EdjxAncEHphk;$ek`wUCB6-vD%Q?+68XCnxR=Mpv=;^0j zSZ;pbOBlz+&ecFzldD&+e#F~Ba76W^xl5}@?KQNum1W{hdV=rYm$`QBfQ&cQ^ZYZ< zPM$e)<~BO?&6|5~-tZCX@#9mOmJCz+`T2PgY*lZDb^oaoGBUe_&6%K9Xy)A>#VhLQ zFtfm+#K*?ofq-Vg>P`X{O3KO_vTck5S*2C68ByX6@qes*!nm5-0&TOl4JEPEh(>Us zfI-P-K!kC9^nO%AMTG*fvxLVq&twd%_B4UTU;4Hby-X$j3A{-f%N`^w1L_=m3q?-? z0A`QpFMiFo;basx-zO~{BIgM%7~r(ml2AiPfovDD_fERrBkzor{g zyKkg~E#FELTowyMe`Aj}YT~`++>q9Kkr&u|1 zG{s4@3(UT@>wJIS(o}Wdz<@PmW@Cn_W}a%4SShnZ+i_^i?~t7^xNuadpf*%?IRySe z9-e{Ja+YV9F`=037Jix-)kmwq{<MIx0z zvLYYEvR_g%3ZiK&tCtG+vt`Q`LKhHbU}?$uA&-t!b5z^Ff9j$ zS5#K&f4H@lK$%qdAv#{&yNEv+_--}pxkXJ)S)aViP#7!ckIRtin0LL->>$Lkw$o?^ zkx0CKD+IA3f!AtF$b>y|Q7!oy8%0NF?E4IlxE1H>c zL;McttuD}GR7pwPs4=4GFd@Rqe*b>$l#-HT*X>Q;8(&ZZje0{hb@idA3Dq6ID4gav zZS)7IAUy0wQs&t0Nw1)&`1IAQt^TD&*h=4^pg~v;_C0%Up)S+(i#H+?yMvZ{KV2v8 zyfQyXfFKYnAGe*LPIKR>`2?Ydx6RaN!9={0QE_6WlPysulo ze)7=4gM0B#G%w(GUJCg8*1Y|(*Q-~rkS06@K7+xpFl_96k(5O7M+P|)C2HdZQvhY< zT~QkU_U!{e0U@PdB_tI7F)6qKsIYTdS_&vCehPQJD)?X>9Tg~#x$IM?P5~#Mz$#*- z!`1cmFJQBl!e6~Qgt={|py*o+kFP|TUu%7LI3ptilHtysJ8v$&?i(DWh0Z|L!;=xR z4yt$t*(=242@oaLp`4E*U+uz$U4O)v!{W&CUti29s;{S}nYTl3ip2RE^0h(OW! z#Qe`jHAJ%dqu(-|p`L3VA0Y?C7y12pOtHkxEM|Kd%|X;1B$_4IfS_zy+1<2Uf8J{< zJp*75m&eA|p!Hv$SNzj~2rEeS_|2PQl!nmUh}Xi3yD!g@Uc6{LZDhoOSb!wTL`6hLNVVA5RL)(zxEs;P0%QT; z`e}Y%o`n5ao1%&eB^#TxCyq=CHzAG_xpv%I~At($JXrp^N7yim64zA|si=RSF6UU6bXk#yuszH4(zp24ip2arYA-0J5~6FFAIm2wI7?DJm)=Elrj?B3-f) zVfq{v5upT*oI&7BkP*U{*jDr00Mro(D*=Z&2!l^S`b=I&{E?lVO%x=8)p0^RlhgCW zJ#A~#2EZin!qOszXJea^p>T$rP>zv`D)pz=(9-e=i-}?N(!ZRlHF1cG3p`0I${}bx z$kqu&3mSUIj;)C|QP%@H9F4$%Lwp4v1uC(wr_}!-H+Kn82ulH1L_)Ssvu)cg9|aA& z@%OXV6J5uNT6aN693HReVrXdi)$FLOtgFv$>g&KKa0)#}R94MLLC_f(OLy0OBytR9 zU>l7|6`=mn(Hpy!!gxv{U37`_4AiXz4m_$*3fQw@IN{V5GuWn{BbHsqhz4mh7sh;v zqd_T`1!6)vFjqu&3)$5D1vU6$j&q9q0{DKjL{ZD?LnyHz($$W$;+>6ZJD7yjxmM-#@L9P+oerWga#hMdwm1um~XqT6|%#mtUp<0>-bc7_6z7> z;!6)B%{keu^|Muk^FRC_%|;4t^v=(pw!5opYU19$-NBpNt_n(sH^Hs>#UDLVk-I=P z^QGx?h+W@?nJBSfCH)TcxGVX;a7xRlJIX6P20wgQ=COsA)Z@$f=N-QFvrg8vJts~V ztd7$ph9#9edza%y-QgV6938DRY^=AJZR(PU>yijgF${hm%ixH&p|NpNK}qQf;F8EV z@n&qT+2~Xd$#_g1ZZ|ROiO(0M^F3!S=@f6W$S*fltKUD9p0X|2-~BcLPBO{63br8H2$T>WO-ONN)5bgHD||R3(iM})3vw}YM`9lJUzVleigQA zO)iLMMXC}%03y@w%#^mK|8agFs!w=5>ruPmvsiWF zuFa{{`ze?liSf20*xo__f~>1{IZqg$WuF}?r`EuA6GmeVy$#y39r+qOZ5J!yZ7si_ z&AQu`;)cA(VG5$X5e5YU*>N&mKVNk~QZ*ovjwahO$HRf`>uy;MV>ww_99vTdl%CVQec_U~j}Qw-hSs|Ee0PYonip*| z>7{DeuYN(-SJTl%v~%Q}+L$jB?k?->dpqbqp%RhW89(yFZTRL zl(OalL|p|POF)R^;q(emA>2pwLR?vr=_%)xdi|5rS-|d5=%e3B@WS<7?S4}3i&NbVv!Y>(@lqegb9DR=y25r6Amb{yDe!8Sb#6k+%eM~% z$;e1}V&-Yhm9yVQfdvrZHmx@@gTun)aS%&MwsE5YgdLGUXTx8^Sja%EM!YA{m>#u= z&6n(Cxm>N;I{7l;b^mK>&FMDAlMS|0Rb^#$s2Y6M4t;r|wP#hNeA6Ea6Ajx6glvmY1jFWE=8v7tZxrhywJdJ4`Sg3IMY2bN^kZ zdxK1NUyW*vVyjDs#97OR2jw+0Ba-74>oze?4ENi+n}kf8v+;9<5xQwuFpHN!YMI4` z?1iA_E-SWP5#Q=nYTD>W%IR-_1xx26J>?=FNd){4H(l^=^zEcytVq@JxsbhqCfFvm zr#N0Us_Ol0hkrAi&H*kig2YLZU~z4m+TR{lU%0(um{RdBGfR+3PRrD}t&+JV>KAUj zNUKs!84K&RJ1~8KCo_UY$AE?=`|Gk{5p#{}h-lvVycMcY!<~6zqdW7=ncc+0mYe%A z#uuLLw(N<*`}pli67fSu)tU!?j%-&l%!sNwB>p}}FR+zQDN!s8Vw&v4j?gE`^>jClpAnVpx^M!N=c;=k+!i+KAirCc`Mrc)%om;T|jtyFxq zXY;6=)7{wt4%lV~FHwub&sWb`eKmcRZS(nBp3(d4duN&{2c~#*XoAV>zvYn8MFhmx zfg0xS{<;ynt07aG)%@FsC#H*l|2Ib*Hc2-hd7XR=YNaV(MM!4h*I=;u)-R;Iy(cwOXwPK>17AItGcMWezQlgCPZu)|k z3U|Bq^fjMB!e(j7M+B12XT@Gsa7+98Xp0m!?E&|l;Ncz|a#ju_r!mn^dVT3~YRk1@ zf?)r6{q^wX+{-luYnHfOpHdgL0<^VQa_&9n8te5q#qf zB_7TK{`J`h*ReK0}81583LAG0m;}7b+*leeC!d8sn{7uYEr~z^`pC ztowp|N^yC>bgPozZ(4$T!OLWw`md3JZ2UAS_RhlghrG|;B~t~>#OzPM*1t8-(0F>P zp>(9JfSOo8|ES*_s6b0|IX%^O=1^T}zkPbQf4$?)o~vc;mh2j<3wum6&9R<9A7f(S zRgolv=09ud=+q*Fn6%Dqhjvd#{Ey@fHj8XIC1*FDZ!#7yh}~Okpx^Ox*H>`1rAQR9 zFq?n#wlNMpkpCs7+A35x9E^qnXh;2vCnOL~M#RO@aW+jrBAL>pEaApt#ry>A((>yzr&qTcjEywRwI&2^FWp%;X3=yv?}br=(JyIe zrBhT)m0f9dIun8vd48vq5)u>HbIx%|T)%EA8e+H3Ein|3+-;EoF5`iRYS@P}<>_v3 zR%;#@d_1c+QAEzmXVS3GykOZLhm78^^M+!PT0e&0G&SibBs`Zpz0DBEeq5s)a+KT7 z*|&xM`QNGfK3cOAZy50n6yoC+fBICTX^!F6>Cv_CoHLk%LZ$>(CU35D%9s2wm}@`P z6HA7Z^}QGbQAUc7`SjNz5$PLFOc`(}SY)2D8~qmAl5X7Fk0&o-^J534>g6MZ4IW!I ze$*#%JCEzVz0vMPspndk*cihh!nZAmTIzzDtNvbs0&s$iAmloz=x5G|fSAK~1(|~; zCfcou^v_aQ{mLW83(AG%1+rert3iti!2NO{D!GdX@gnK?OgcFp?{!(6?vAIV z$azQ2(jOX1P}gjSEqRo<{R!Bp-wpcPmX5uj)g}H&Bp0mQB>oV$C=sSu?LGF3MY;Tk zyIRwIJ<`L{#PdBfAMJqG?!D{>wh8WIXza${w~#YDtwou} zm;Mgm71;ZmC4M`kdBcC_6=GR^{{F3l;LZp8i3ZZ`2+qNw-6lu3pXi%v%&vR-ck|sndDb1Sad6G1zB&3;0QXxqX&6+jQD9v*r zjnY%ef86%FzxUmHt#^Izx4wU^f35#nYqQh+-1l{#*Lfc2aUACrc0xmW+t%G%*R5N( zP34%Geb>9TI;*R2!WqM~&0w8t+~UjUim6dS}#VBZ|qLj{h$^lFxDm?cJGGXsV%aXMy}sw zzr6S1XStA3n;%WZ&7N!HPRY;ABZj~EsyEs1W&iVIQ1`pipP!;z0il0>JOn6c|NPAA zQT};3`%pghpO-6DTJQSv^3c#u(H(zYPy58|Umj5ZKYf7DEuTOtmoGgsDPA9WI&#h_ zA3aJLLc==Ns;M~IqAJ8GF8&)Wt-Vpcp68dBd0L^UY~MYK6Pwk2&pK4bSBGVwu)g34UNS>Spdm*;hnPlY$e@| zn>XJT*c+NR+%6wa^=}%fizQ21G^dz#Wb4}Wmn848FQV17?tE<4^~|iYrlvJRlU49a zqq)hKo|oB%r73&tNWbKDMTSbfsjC|d<4ze!cJCXkjp3kXmc21HW=_0%OH0eK>E&eWLF9vw@x z=aQFCLiDh^heNiA~>EIQMN!%j+Ov>cH9In%IPd?AZpFr_XlimxNyU*!(%}&iUB%u~{hX4Lw-OR| zyM5_Vu|S6GLyC!6G&$V$@%7!Zbt}7s1btR}cxb`#5*JEQRyYjf-y?dvY zEPJzFs5Ggz_Sl+1Nl8hM@3-Fj4<0y;HSA*X{w(sQzJ8a4)o&r&nTNXH+ejuPBs|s2 z(GRVQ<&I5B(Xg`2~;nGN~=0$t^jAzfTV=XNm9l?#}_q@M-A(9(G95l}BdY^C2aP{g{mlyW^3P=(W zY6__e;hDCco(Alcny>GHhK79{92A;nW@bK1gCR-Mj_VOr zhx#{fi059v{;N`v`iT=e1dWW0NJo^Eb}%xAs#;s~br;xkb909PE`|}4_sB&~PEHb& ztV?J^gBmX%Usr)WE8fpzD>Zdxl`ysb>xv3TX{P7TpAR@KFIaA8lK$Po)z!6Vb*_Hl zQ)hJGCYtN<@hWG|&=Zl9b=jQ?_p1x>RqtcF59_$1W!)hk&`HBqohig+}k`;+oe%R5E_6(&J6Z z6|qK)q$GLI>$qzVb;X@L;_jlNGpDt+t3z1*YQyNb9T35S>}-BKl!u4spwgO@qM{;; z&&8R}_Ee&fe{;3JtEE=WE)} z8a6(+Z1q?pk2}8n*5@tbyDaWjm<()+&$DP2z4&&o*TO`3Tc%c5p}P<}`}szoQk|+$ zapj$ZfjUJVB7)8G^VF(wg6a3~U&++UKoM46-bh7STmBR;VnP|qrEdIV?YoDSl@+_7 zU{u!QSK$KZUN`^`dKRa;Yp!ge(<%0nNI|9FmT~5C?{VBzUS2N0wlu7kARZ>w=MmVI zXW`tcE;V?{z@TZMJh1DfGxxayyQMQmZ zGp^kH=+UEz?2?icp^~M`*yY;*;#OT0ZAA!8Mp03c>!*&{nF^X=BxGHFr$PPOyN7ql zy6i_nR%mH!??i$s9XZ10B$C@bMKr-HS2keF$wLuB(&$Wql-tsjqge=SE%yFKq&k{( z1(3DoynAm|wr-wuDj>zc_v;yjwIve+uSw3MS9b^1Q_#qlz0WnlEj?u2J~NtBM;hT> zexs)sF>hc5rZjfjmp0A}Rv^MOf`56t%iU4Ta-=V>K27u%N^u;Gyz}^R6wzKIcZmg? z)!)jsYUib-VNpZH+S=N_pK?QcM8=ewH6}=8>*a@+2X1n%*@mn&|M~S># z{9sK43pXX!X(2G$|>bW4mq%o<9y; z*2>V}+`m6`Bh^la?_WE`Osc85)DtS#zOTF-j*`=PVY3TO(X@7lf&X_`eJ+ip+PjBC zh^*v0`?%8V%?%!Y-Dg*r<=igh>inaYP!I=Co%$8Xk#F7E($=PQ{yg*HU}`o|(d(cK zPmL-l^_`rYJSNlQX1;y1m>sGr`mwemE-Bga?%hEh9mXdm%e*Kgbq$T}NM%kwzKE}% zI!*GrJW#t~QBl?X{i$dgTKf8GE-vB%0s=+zO>!gS$qdYq`xkt@yf zW|u!b8Fs_>@~|h^FhP0p$_gDleNaSdD(eC7#Y!8=yo>MHAD6Cq0fQX6UfAvl3JT&s z|LO)ni=hA^&9&|n1i=lBj5I|ZDJd(PNG85qXi?RD<-$WjNxc^^j!sNT&3LK)wus5* zty^E`faooe$uZZim0jmlWt8_2ZftDi*C$=eHL01K%t#$-DRj5*d12dq)X0bpz%U9h zt;)G`=hE}%;$sc*709B^)YKzBM7UqSetpr}`rfNo2|$Inx}c;q3QK$E$D7BA?ush$ z_)z32rX0mIc<(d#Q)Y~DW&05{QoV$g_4XbBcCoC2Rc0y1Tn>bJ~8P=Pw`)P3mU zF%c?eW}L*^8Wi0|swZ!u@EW_{NAuL{-0U!Ysn~ohj zR>mnJ5(5aCFF;8wfBmGKs^GhsifTC8f0YBQ`2xbG`i>g~L6M1VBWOuz=w@~f4&9edoEtZ8yq}&fV$s9|!Eo@x1qh@c zTPYP!n40cGe;6uR`C`)j!nTM!mKe!$AOYYBY8PEJgeX%U=)cqiYLMI!6Wnv!G?LXh3SDr2LvOTU;}SkTeY z1*+mY^St_A%2Z@un30s;g`pcGsC`l2FAOBF%+IiUck%tYg#cG-|jO-%=e zhYveA2&Z5bj_9O4ek%l5Z+H_vlqQ{$o&3^p1V8y*g<7`&4g8AoB!2#E!@rYCBO?~eyfQLkx^~N;*lY0AsZ*CTz!NS928w`rhJ={p>($lO8GZZG zd;09zVNvT2{^f7e(cR(ne2^zn(ykwWWAEFKJd2Bp+TH^yh^%1a;tHnc)4m4!Z*0t7 zRaJ$TCE8!>q$zoBRQ_w57RhzGFBzSGXn0tL74ipNqAKl3L~VV&$-A^8q{!V8;e}sb zMG)`nklSTLyzj3&cOJFq>gv+r-w76(>NB?m%kmJ7S0f=`1>1!D$*LMj(&eE1kzkGa z`8RLhX4KBsIS3k#CH=<0;C$EX>&GBffN%<+d<$Y>DoV@364h?S38vRu=&p7C{MCAF z@3pHvyL-ws)mQf@ySYgU?u-Z`FaTnOz`-q4 zREL#D4C@A_5#A8h)2Dv}j~I)$NErlvJpjIuk`H*_0zdZqnq`>lIdA?zE&h{~e^N&@{odSB5PyD}IgWr0NC%Zdj0xv#1xBT2+n#yVnX^8#`@-%_; zM%IgZ&Y2?1&YU^3kCPLYLwS&cG*eqg$J{r@16~Ae12)Ra$~rj8CZm*g$Gv%T9HP&y zkU67O#R5J4E?~H_Qc19M)d|ov(Ewg_3cBRjvCVYc8kMbx{V^>qy6qu>A3s_hI(&GS zlpPa(u}@AeF+6;$KA}0fpIg$gv9YNoN^VPR3=S<&Iez>#r=lWj0a&}3)5tOK%w1Rt zaCaXU*E82o+J6t7gR^Y1ekcfwep*_ZgXessRJ@cui(=)(yL*nA+1ZtY4i^f#v3`djT;_K1WakCue7JqrG1s*fzF6%xmG|Rw^nLH8s;u9XTYK zpBGO4@856L9<_=h(?{nzjL<0lAU8V_t3&?(`s=TZ(hhr}NU#|q4g&n= zQ9!?;$!_Q6<-KTSHH3Tgz>a80mW!L2amd!@Bfu2Ab__hZXlW?I2fQGXRWSu(oIuH7 z>Rqo<4m|t!-^tC55h`81p88{Te8Z+q^F7YZj&ma_P{bNg*I?Zo2fP-D_CbJKM@O}$ z-yEHR$KXaVFVavrAEWri9d_unM#~G63m@AfPTo6u^6Xhv$9`=JuzM%rU$FB->SMF`xY-AKikQ!lz1C2bfB-KLCT`w%P*2@MM6*!{|t z4~X@=pdc3Z(7k8{A`B277n)NJEPU>Ytzq$wM0FA>3_>U#;^*GctLz}kV29!TYl{Wq z;^M*{GZzS256w-G;4Ry>jS@D2ajrwJYbV=~IucMCy3fA!yChaMG-M2DAYhlCo)E8S z^bemr8NVCCVmwkCLpV|lhCV;{9pWSpY2-HbAqd?8Drm!o4Fl$A_lsQ?knrf$8)NG# zK}!hB5Qtda+#H>*8aFpv!z$+fMW4E&Y>FT#ss3v+#a>_2z}~>_QO#&|W5wV4DsZ`O zp83f$XR08^h-S5kh6PO28RUCDHg)w1usPRlN)J#$Y=KU`6+IY&zGD^r$k^CiXX)C_ zUnnSrT|b_dwCkxwzB7XSgI82kndjx&bcrGw0G*+%CnfB_rNu=z{6l6=4ks`civ(O8 zh9g09U||PBVbep3K{klEOq>9_3h9QU8WtW-*kMRA7T<-F*RNlH|I~;Az*2i!>C#0 zZ}UAVx(f9XUxpZ*n7f%Ip^vs zfp%OY42#<@8%={-Gv)r-CkC*=hC15Xu0T?Ojwu~II)r9tX=RlLi)fXjt+SJkg{7(g z9-J&Z3!-jqZH*)WsgrX~YW@24ON3Pc&m7gdr2G(@v~S;r!rmy5D?2zfAUdr*J!D=z!yJxk9Z|T_u7qjoU>0y?C=r}Q1Tin@^vuI9AQ4)hZrOZ zQ5#qaAp}u%<;s=4xPiHo-=FgcavxzhJv@1jaP2~)qb<W=U!0L#;{Tu{$oBd zvI)3J1L8)MJvz8BEDF?OaA<9G7)+d>pSi%Q>gr$j?1{iX-uzz;KW2V~KRGfw`c4;$ z@9WpEB;6eC#p!-cus-PAGcY0%dJijG+flSE4_+Z5ZfM>YFC>+eUZuf-pPQST$bS0t z+VZkj54zaOx9N=g_czeSJm!^>VnDWJ04D+0v!@LVG+>s%a|jL9-<-3=;Z6#P^kNu{Um9`L_?SHyJoW zh^1WPjoA03X6q-#)wTt_)o{FU?u+N5@eM_*`&{i0M#0yel4l ze(*umfGu@V1BOI|CkId41G5((Pb(%+J!$vu@$m5-R=Ur(b=x+xz9LWgZ#~XBIok4& z&#)*iWk804Y7ndaQZ%oD2?PcnHL>kh>2T{xd{>5c0tulm5ZZTUap$X%p}Ls(;E zWJyYcYGG=Et-}g{sIP9UsoC^%Qb3Xr7axG@gs9%UaYG5}%f5unl|l@bWRpFtE?z9N zk>p@+q2Co;4!NaMV8;x2BaxSuE-+GZ#cnAG4yMGlSmC2Dr9b?lSPmRVmjC_hLf?lJV6&s+J_KPFP*hA^TJpfwKT~G$yh2!n=wCfhc?e8JPFmXftoGwdN*jn$ z{BLgXh?trf{`hKm|Ni~AZ{J=z2e$*wh;-)k=?nRKVWFYFD~)tBdSpVlLUGB+$dIn? zk%}tl^P&o=IC%Il1&kV_Khoa}eSq+UKqe^);HNkal>LGlb|eN&!_`-@Usms>`KVylJ4%bjk`Z2+K64Wb`vqnCObZ|%N=%*CM&!m-wxfpa8Riz5uL;7q?0t+z=s%j z$FRzU(9y>Hs3=p#gf%8EPT2yg4|O?go~u*a*a*3b>qYA2&MVt$$))g5NtWRnXbcUb6VY%1_?WPT)>iRPyD;agPkB6h-o5mQv{SRDS^R`v z@8wW^Ch8(3@O4;1W6;(fA4rZIo}Qj0Fh^16F}1Pg)acepC4}*X)2GhW@n9w<4Q@LN zYXSNBjKieo&%1SAk@tQ}|7>LKhSv0BJG5Iv%7H{oIR`9}mgr%F*Ao-#0riBjrjdd7 zi|BfjV>q9>$H_rDnUtK9Khu!9eXym)&SY{duqW~c{0dDlnxN2)cV&Y^r zoDlUy$yuNpNC-RodaR)6dzVU+g^AZMTO5sg+^5o^_~efK55H1dRZ$jESydI2l%xj0 zPKMc==QIkx$9M-eLgdGTyEY%Rl*+q~$|j0f3YRiDZvnnwu$cjEgdwepippIv7(d#N z^&w5E+1ck5$NI%@sC$R+m+n4hA;z_BEY3u*+*Fi}oq>iz$QgD7#x5F|TBbl*5VxWw z|9BnPu(}v!#P1`=kkuQ&U4b+osTpdLo4$`Z2XugZyc!p-#?5~OZHgyPF<0m;xe00) zQfLO(T?y6qp)QIvV&vC*9*UU==+o8EP@lqghtqa-l*(DRMB!^DW{i{(I|Ynkw!od? zU}x{QTex;H`0I32!t;Bfp^P4P-@n(v=?9iuLWWWJb^%#x+h`ug=txTTQ7nZQ(`)Z9 zFGEJ_0%Bl5GO`zqqSTkm=k;Jz739Bv$Mb5L_2Hu?~a&|LDU$Mm$l`XagUHE(P6cF zSqX+WQj=NI9@YD0WMyVvf|hn7Fw{c|)NdrsbWwthAa{KgH5(L4xRjJ+=P_Rul>N@p z`17=i`y{I+g5>j`Aj4mvhk?KZTA_9$=N5D4xx=pW0hj(*_4)We_@qHY%iuuBRw`Iy zw6v2S|MCEju)nc9DCfG`+Br4rH8dlC{Ul89Z}mqbbpv>`(SnVUNJL)m;;>Q z;o-LrcuT^KC~Jk|QCDAIF$z`9B1wOC zd!xbNJXa}VWev{o@^-RxcUdow;Wgj1t(u?QSjOLG>gg{soObT#+r`BHXf%i>dS*4X z6eVDoGhwPgKWZ`4_V$Fbx+|yP_QGV`F-hJr@10pq;ghBl(aQQ z<`B+K5ymZWcHX^zABvvWh9S|JBQoP!y1G&RzwgBm=a7=pUf#A;&S~cRneA`LMdZmX zDJAn34W&FWit@4v2RujH3S0oFPqYosMWm<4&u3=y*C_72mU6%w9nzQ4Bj&%#f6ynz zZ`l&2OgK)%5UnwMidob3%tqUsowA?k>6D`%rd&Yvv%lr$y?yTSmzq3QrZFGw10I|e zLSZANGUa0q#INUs82SqoGgQOTuw6R%xT0829w;YpU`2RF)VdvtX4env)XJVX^x zBM!Ycf{>_h-<|+VVP|DE^!hegGdaYL4vd1IVj(S_YHHVe?|3J&X?=sjZMD^cPIndy zRQ>Dq$lKMdjT=LD>|`nLde(}c?lF4osu0$Bevua9hZr5rcj0^j$Ow>p5B8LQpLk^1 zz=b!>x9}qUO~PvYh29&oUW}dWdi#cK)9s&7T*K=*cdH%+?>}=cw)MbdRQ%d!tkk-<>Oayym+H4>lC@hlLI5ILau!xBh!Ov%3Qwi;WBOV$m zg#$bazCw3bZFmrsUb|ggG>V3S4JPy?X4y0K;R1&9%N%FvXg`B#lqUNBzV1L;TIM5Z z{M291AoSuw`Fesr_JEHc1r;OagSKm{WPQ)kn;S%3Y=>L2K2@UUX+euwcmQRAB@ z%!xy8EjR!54BQ-$MoFns)%AVSbKT_C6Xu58zx4h@gBG8k=rL=2)z;1L;oEhDnvHqf z^bbs-zbJVua#opuHRCa`5q#oAf8vLr*S`bCe|_$MdiKsm21E;TZ7>tv8ER!{%t4x% zePE^x{=x==-ZdqurpWwZz$9cINFa4h2-gi4ln&g-f{ho@kgm_;4H@su)-X!%2bh+=h^ z65R9AEj?|a8R5FIqi5=_#B;0D1YOelif5HlUy?ia)h zx_&Z$u&}@bn+EXW7`0ULJ?!eIic1F%U$n`tG0!6|Y47YXwzM4c(Gw7W+rE6>iN~>c z(G)$A(5!f=2lj%6Y)|e+F&rt%+OG%)PFkU)1QFo8b;}&h0Ef43e_!3tD;)(e?9isl z$=S&-(D+}CY)#{G+O8*jyFIvnUB&adR==HkX@ zxMIT;d^b6KX|T-h*xr=!_(j5lYD#18ZwCRPux`qXE$`;$23)$d_fZzFwD*G>V-Jq% zB_v+7v4PbdP+d*=@spLzE%(rYfgSM0Ep77&NsFS05uA!qW*LO#DFO3~khR3HS|yfC z#O<>VURE|QmGxcm(0&1#AR|T(E1QQI&xV*N1eq@zVa+ZQ#l}MqrQL%KL>x(|9;Zh$ zFij3Pyq{VK533+D#YHqVATanX2k&n5MI%(~`2-*`6@%ai20dksgAOwdV$H%nOAZe- zlEWcUCi*zG0<+Lr?U3C`a$0XVDd^y=9Z{;>v$SagZj|sS+}VRy#`XHZzzHvlm zf4+u&?KL*y*y!ipLWVflh@j^zgu8%K01ku`g`*To2vx*%ck1drWe2(Q`k}q%_Xd_u z_6)5oa_>g|c0-}4h4e*SUx zKP~%zeC7Y)nclx}p?|)?|Gh)Q|9?H;yW%C4&5K>CzwacDh^rjYPutBmoT&+>ep=e<0iP<=g3Zq7rTC=`nO z(nWPc6l#kCyf59p75-l+pNK}Ge!G21{k-vwP2(@OdB@*NUzi?PZ^Y~@{LOyfyh;8( z2eY{C#wXR>w69B5y~pp}>pxN=X?1KDP4!Irp0tw>Pd-H5HY~yOC~UIdagsA=&wVlO z9a~g+53UrYlww;Om3>EqcG@w6Ccl4`T0aj{!b|Sz_Klw}hPXCuy!nuHWaB3)@${CB zH-C5q|NP)u%btxlx85Dxyz%Dg8MU8(k#qdt805RLxPya(bJr7dMepHtllv0LUf5nK8zPUL%JR~HY51IQGgQdCl zK0l=3k>(~7-<&P19q07&Hb;fE(n6nCPbIOyZGT8zdb(PO$eA$@b&NkA;?!MYG~N{WP#kdX5Hhbv)`k#PnN3Yi#ugT|k&ayYrI_Q$CyyRI+P{B))}~*!I=xRj56dF*hSVL=*3*mp^7SiD zUq9{DE7cOWehZ@46BN7UHFB&RN9x2U3`1s1bf3<-Dn^5xj* z==5CziWIN!En(r|9lv6g(hV{-Mm{{d;Bl>=j7nOHAK^#m*>YVD!i#W9mWthPfySPd{u=jQ+2y7impcJrK;q? z8Vm%^`o!8DNRW02+q`-67F3z?feO8v#VMPuCr znYJq5*Z=jJ$ZBo%TnYtvCZMUGKdTau^)D?{?^iONa)$0`98$8{63Fsmcjin07kMu9CR z`7BXymCZ7`d^zLGmoLRW;})*{6}TNcc6gqUmBkVJ3Gv5u{`{@E*S$6`ER3@gc>q9x z)Nz|*r#Dq2;t*O|+AuUUG%O;b^Zwprj3?%&tJm0LVWFYX%Kw=IrdOrK7`GS(@#vC6S=Yb+O8Bs8UIBO<*66H6jseNiTcenp1@hc(zPyL zd|&JJtzj>d$wbS@7(<$MLMqySd}?=RE7yTED6_H0jwx+V?dpmhnt2`U?VSK+@ai)#T_V<~)5W1;bG-T2qu+{lo$EsZ$1yElCDZg33r~ z{PmamyHt$r>heN2pvWA<2usb#P>n;5WNC((aF&>V^ypEUD{r^}Et-0JEm606L(<27 zhpPBx+b(0MeHqst8F8B`v$!u`E?-brrxX?%#=*3-r%&}YqXcnUT47Di&9ZKN3gaJ( zG~KINaYv6Glf5~A?QstEO~heQDJgv(9v+M5*B+x#FOr=nKUK5mIs&>`bUsW;F3eRm4vk(-6T)O=Z9Iufm5n+?-J#krT?QKKx*;q-J9Ojq-CBU z=Sl-86j1gv-nNVXeK7Z71hiR@f_JuZ8^S+hBnJ2y7K`ivm3~O>+MhkoUFj<;{sIaf z=av^HyVq8iJCO|mn@MP&KOa<8UCn_i3s4{=-@SK_N?Koo*&rO%bA4W|)SC+vfZJU) z0n4!uAEq_NoEZDo7*iM;fQ1_fUIkAqw-H!?KL0OWB3&MGVh9@w^9kO4(IH=G$ov$%1i?9+{}+}2bI<)nFG zQ4!9-Aib)wh4913`{y0V55+lOHe+=M|TMTwUE?CuI+FJQN0WjU$ViiooiSmBt*` zWz+=R9Pcf~LcJy^`4$4KqqTi~%Oiy_651Cp-p4opJ@`Pt0)jD*A3vtiXoeaZ!D7JY zQK0Mia`THI_30LVeicBn2$*qf-8%aD<(;1Do2AmHPdlI`B)Z@U)_kwK3xw9<+2wnH zSlORH{{baWfg1Pw7Oe@w!VMd7NY>EFiPDmyOn>`U2jRocNd7Y!SKlQxwih{K17_dJ z1EjTS3I$|QjdDk4TQTw3Wo2fN_xgYQkwyz(b(&V}o2*_)0XiyJo~*TvOH2DrnLV7@ zQxi~GvpjM3f~F?DXE$ag4T9J-=rKHiMx&zv_0;upjUX89ukP(E@n7=1clYk_>cT*G z?XUd&V%h-avH%O#SjpOz!hH3>z#6}Wi99$|ie;%=g#{2=Q$vG(s!G6I!?7re1s+dh zv&M2?yf`PIbVI+AIB9bAYL1v|PjPg1R+f7AosJR4M1Lg|>!rI1RoG>Wv0LFMI%{3#)1i5elR@ zrub?Ata5U38JD{C+lNL(kew5HYDu*|KRz7kka0kZigqyn%%T1bNuwX0Wh(mopg;~% ztSfyA-#$LA*|~4uK1P+VmokfC#72K_O^s*12>PXvOGw3vPr~XwMAqS;(v7DrNoO(> zbUI?QpFGi#MQAfFE9-ZVdQ|v*vT8OGvdcap2QRH_#0&FIPftf@Lv2BzajxR#h3ZgS zhOokKkdLpK69)oJLH{8t6r_TUO$x35=43Vg zV_~*ll0tfWyCHLEh(@7Y$?b42xu;gYCnj4Pc{%< zV{0oGXcLAvFfc&WpQo2s35ZPqSbd{hJsi*&IU#{2S+l4LJ3u@E=(2C#iX2c5k_i8n zkk$k9EK0OaU{e?%_~*JT-JSoszju^DjS)i#wI%s1Vel0PvJjA%20&p9x~U}M6vn#p zI&9Zw_+*{lpUc#YG6BH^(la(Okq%(rPg-BCU{DM)i+yLWB2U0Ek{y^$WHzV+Zx|31 z7cFp=J^HD-au>fs8U*AbJkkl8)0+x!oI$M2Oi#A~Q8hvqnnwJpmY#R0>TtNbb{E8cGBZPyX5u^w`GQ?s;My{ zFA@|9G-QB`jHAk+0TH=~>^n8J05aXImfcwp`|l!d3^bZ&Q*BY{Gu{s8`0e}m+~Q&x zC>F%1fIAZMp9)f0U-3c^NRcoai>M>eVM}bbh@6}WQ2xx!jG>3eQ-HT)kWzJ0hyr^D zRUr|t4balo)|Mb;cN<(rezz^jy6Eb=_9n6@(?fxgt=Df^ZR!?cz#3pe3aEr;V4ovLj?kbIwLvKC-MbeAW@+i|+neE}njp(Sj*+9HqG*Vc z3@>s~3w!cDul_gGCB`XrN z#4U>TbrGRsY#fVdj`;Xo*d_vp1L%_{^}%d_wn4*~qGa%>z;9=g_4@JV=H_sSVfXLr zmjGWwG|(a@aq;14w&>}r$q@~SQ~;X79Ab!c>+odh5FpG2a*6_v^g0DGw&&NFVF^a zhZrq+3$Tk4^?(gp+S<&K5h~(g4D&2YO-i+Xb!BD69{A9}H2}l`8yqbZjVKn2H4j`I+(wxEWWBbw29ThV1V?Z#&%x1g ztZp|Z7J@J5)<*$JTxMRgO-R#ngnp7IPnuFG&40K+JRP)LPH$AMb(XG8=aFEc@En>6_fqG zM#v*BG<6@yUz=(_XRsjb$@MjNakHoAV16Uu&A*?&vi%S+1y|jBaJ}AVUcY_~i5xFT zTDgvMY0J0=$~Ifv^7+3m={HG9$?CawZT8~VDAsuCKvObkhfcaF7Md$KU8IUYt^DiA z>kJxB6ev&+(LdlWB|tc%;q*rgL1Z<(eGA6`uQCGHAPGT!&fUAa`^votXbc7ei9KS7 z)cWk~umASc!Qga0TYm%F02UeXun8F%8O9)}A3fUF={^6${KAC`J2oDM;7zTE-oO7I zY25(EjDpdk@d^s&!+xB5pSx~&TZExDo_>*cW&8SN=qOZtPq~RW8+@>x)2q8X5Htft zPX{TD*c3iV8-^rlNqeKMb<46IUB-H~QxaU18V%{#K)a$HQjQE|d~>2)6VWeF)jwNa zZUP}oyI=^6q|0_Zt;DG`_2UhK5J2Vpfur#s{jg@tnK3P3k2UhVcO;DQk0#eN*zM zJ8aOv!+84?rR=)pj@ZQH%vr})(RKfn(dA{df|%LM^a#ci*PSJmxz+r;LQtruqPZ}n zhi+j}A*-9%HEI}aH@sf8bUUHMIh{V;-%{Q4ERfzea^`8Nl9H0QaO4kcM^2Rd=+v4_ zxt9!XU9{@bUG}c!a>a$wqq}pBP^b?PNF%Xt59~}Uq1MVFU%Fohq?^U51p)DdQ??-s zxg%OhjS6|Vou*DL49tjXU^*<~uv*y!f`g!Zpl>s32g?i4LxQ+xOOlb5&XX|7 z{X0>pN~ybtuL8AN)CN`uHf3B0FTPU03wYropzb_KGm}+^ds^CBgk$6VYbB9c{9V`` z*A7sY{d`OkBVC!}W0}q92J!cf^!4MgwpZvrDMVKzEd#32iF@E-lEKHheEBO>nhS92 z^E^2*1sL%x9a*jq#`Le{Uv*RU7&!%t81J`d3uo);Fve>wrnx+qwlG% za^NTJ2upQn4py}6ar+a-qq}tH`vEn>F$fbuMEzDLh4=}QEvejcnHZ3J*seLZu?drU zg1GtiW|?X7cb?9SQ2fl!sg6It*Nw{mjwU5vtqB<}XNsIa??~?ABAa44klHBFAw#$^ za0JS)``|0?Zcql?f=XuYRnx&x#;~1>T)h(lJ4&1F)5yZFWG2u zaq%Q{=fF4f0Q`A*-yC+7R^qZAy*zRu&!Wr?vz^TSe7xx2PM(9c;1IO;P&+6XtbxIpq}ZfX7=xpI zG$d(+b0Fh0X*4b23&NiDnD@4(b)_j$61j`2a4A7)MgZw^@J<*0{Pau`ni!}we@E`g zvQ>$Jjfq5u3^Nj|Y-`O8Wgl-eBmx9Lc)U^j`>VIH40_j{B#B+5DWU`avVm!HZ@s0U zprB_xp)H$k0LzRwYApbK{d!H4Fwhto2q= z7zDbJdy5yko#jMR!f0S>-_lHnuGaE=TE0dA2~!?JQo&{mXIB_PI(pXE)v496{5e2K zY+R7Y_h;4~rrz2bR1xc((4-tsr|%Wykw7Yg`t@MNzWnxZ#YD0=@m0nvLoNw~Zn|HX=Toy3P`n<=ov?GBrPKLf@75 zO>b>UDWHfD71%T)R1_Qd`F`It{H1wU9-@dgE@?_1#Fv)7(Ge7_lkyf993D`MpMBZB z;621}E|0l6U-gOo)gPv7OqK``dWrOr`0`C3gIOPOX5?6g_VrWG z*!ZtLa74|9P~!t`>l!3qaXlzx&-&7ug00GO>c9j+KnJKYX5jRPJ=plrOI=HOquTYA@<1){sGhAU@y-csblwC$)8-Bv^iL&%a<>$Z04oJQIF#dZ%6m>y z*5j`~tiyM;5nW!*T>|Y5*I7Pxh*lzKq#jviuqU2Z@Yde&zaPj-+&eQj$LJjGtE8u- z%s)`7R^mY2N>%{0x}{h*=;6f3T5nIySux<#mZUuw@=O}Y@G z(XB{m;q||BhxgB31mcAoM9)E7c+T5ws)#v*1lYoPXuyoS`3~?Y=PakOxdChs+(*84N&r_2~6yzes+&F=^I+#^f)OH*RG|pP-F$PM$t(G+Dcjs8uEcb?1_K z_UuW6ZlK7KBj@3!45X?@>&9@!64n(?Q=eb{Z9G7ZOcfvql-6<>6IcbDx?e~L*!>?L z&Q4DoK#K%vm;d9#7rcnEgxemdpA}J%%LpRL!riFBg$wsy)z!gCx~cq6Qfng@T!zNS z#ogWAL7Z9EZ7AN;V2Y97>XCgww)`L6MgOyT{QqYq6YHA}d2esDYPc4L1o+YgJ@tIm HKW_gEN1_N` literal 0 HcmV?d00001 diff --git a/tests/src/browser/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-chromium-darwin.png index baacc1234e10c2608960334d9af2d788ad3f641c..0a6a04b15aa01c5e55e2913c09f5fe9217e8578f 100644 GIT binary patch literal 6410 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 WXKrYX&>0)#f{pY|^~!aAj{FaLCRY*w literal 6026 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 diff --git a/tests/src/browser/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-chromium-darwin.png index 5e65d096b0448b416b9ad3b7563a2f755230b53e..6a3345599c8e1f13fe9bb70b05bc31b0dd396b51 100644 GIT binary patch literal 5919 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 literal 6342 zcmd^Dc{tSjzn|(z>e8u*ByuDX+9*p>*+OvN=ly=YU$58u6QZxH z!MT%vCkBJz)V!iLufQTc+Rax<+FTH_o-PLf`7l zJ+f5gkXEU&G7ioZx^5V=%Q1;hXQ6+wXV$ zxG?^IbYZJ!d}vr$iQDky>7g2%0C#tH+yJ#?>(;F?&!1-=ZY(Xu!H*kR>VWZ%+Zk9?PA^86W=HM#~` z!s_0A`-zsYVzhe0lHU^j=5{WDY-ua%+hI4yo;)l3^6(Sv?8;=m>8)ECtDirAj(PSh&3&YS z1M`3)e6i&=Y<2#iqIV9%oW?Nje(H@VR-9u>uq$|U)_>aJxLJvM<=SvoI{ zHW$YH_FL+sM_bvpZ)X%V3L3$KWu3bv4=H+It@8WUEmhW^Rv*SYS^a&cGl*lKr?9-d zJfl1J1=k^kjIri8=cTESIC|~FZTRtchs8FP-A;@2cHG+uPew4WwLTUb8!#W8g2JU8 zCrx85e3A8?zs01tt3bZu8b{izz710De_s*@_=bdpXlZI1xVslv-y3GbVzCnd^mdq8 zHgMhF_q^bM-)vQH*`1WaqM}$R2uQNL0GAmd6D@A-+fdJ^H@}WC@w6yAv!=Ii^#LLP zUFHYdNRrR&pm~KyCXAzzvByQl#GHqJ55~2p1*3Wx@Ly<2IO~#Nl53v1U&@^RkX=yU z!=rFJCx1o*V2M`ZY^t=ey3q9JpItsR->sCGT}ITXsHmR->au*Y(SWIookYE!MxnqQ zyJmtDL}Omjd+IsBKOJ&SFRNM09mG#c4Y)TRf-D(2IgwC3e4nkcnV6XHsIXKBTz`!+ zi9l)F_>`6&d4Vjn`iZ@l>O}!^DsdWo?b}=89A{``ghDs{u}CAJ$f32ni)2*d*!~N1 zqEkatQ&Yi<0q}Vi6CY24a5%Rl$gYe=>n#GhCNJ;YwTnj76Vi!4trh@Dh;991q~hG2 z(+=SzifR+G3qE|f22)**kB{%I39NCgX4wOz#Lt{DQd3h4jflX$IO~EY_FWh!-5IKe zH7A_1eLO!u&wcP9Ak7@z$zrkoyE{kAfYfweoYmpr;7B-mZHKM? z1TvBJp1Q`$%*deE?N;W-V1|yQ+jl@!NAR7xtb|`|JJWhT9TM}db!6_?(WB}mB_;D= zVPVmI)7{D&EIl4>?(D2A_2{EI9eGxjiwsIW8F&vrSI=u_M!0Ef%OfZ#ozhA?H+WA4 zRZ&b_9P!&wpv{TbUmxL@rbu6s{d6y+=s7t#g+6^suu)YF($dl@zEQWGjh(%@b=Zww z(qrAJygs*kjnx|19sGK(;b4NCd)iT*gp8Lj5A*QyIuB62aP;3a1l#|NKZBUx>dKWX z|KQ)r$%$2Y^XARG_SLIh*#FD3QUr|T@yqepS@4Lt6djjry~Rcri& zC`)PeYaI&Z_J84fiT}%W<(HP405x_3ZH~o7N2hC#xyda)QC_`tZoo4d=j<#SuzHV& zhX;7*PN>0|kkHVMS2ya4@6Fwejf*=jFK_0zG-Z%)T~+KeYkf%GLsjqmTu}A$=UpsX zR~LP$?@rHj*-+jrqmT&VWDAP*_|FJloHng1*^a{=DbDWtq^`lOZOwTU5Mso*=@sDaX8%K z?8%0=`H*z!IBlX{3c;rF=^@$&kXIm&G4IL}&k4gr=kMv0NF>H!RVDwKA2-K|$jBH^ z_H2?hD3A=4;ug$T9S;37VNfAeX=%RzeM*3^4i66_l3HKsscD+sLF-vi#T*YU=D-H_1%m~o)i--u;++2d#NwRRQP-CvIf2*0K zPUb}qxs(lL=_#)nBT28Et)>9^x%3um1_(xK7-d~GxOcCdG5w)HW#g&x+Jqi{^fwKe z>$MnGiA$f!w?V)BQkT9gP@wFETfyyBT z&!`mOi^|trxY%&;j^pP)La0z~ULz6baQ)@(jE_ac%_5=stucFegoN@_ z)}|b&QOnEsSQP`VERb;)I1Lu)$z#dpXDN1O^ArljNQGcW5^gRWg%z0>JEm67P_+Ys zgM;T40hX(P%fDI*rx`(18I21!XeLNujf~RCWQnoS(d+_vM*_?_fS=3-ooWk?ArP8o zq58LCK560R`r|wgfC*$Sq-Syt&R*_P4}MZ z2MW~{mz2E9&Lsd*XY^MToj!fq*u*3Y#G4G#3YyxE;Pd!F#&g<{DOj7bQ0is80iC?6r z1_k|O=iuNx-CKemi#0`9(n*vv0E8lY2Hp=8N{o~5qla~atdZdO% zF?Ftc%&FkFlmptt!_Q9^R(VV>YQ4~jpnd7r?VA=N$M4$=QeE&U#m+s_v9Yo5roAb7 zdAhJK6C94Ln|xk8vN8RSKUAg6i?4|3CPo2lql8stz%4*F!7zv(IpVm!%0kxcykI1V zITFkyIrqy@HQ)O0MpiE*&C)K{-g4tV#u0MUW5?qtv?KtfnU!!%SS5IW#Q zpvjh&8}~I$uYnnVpQK2H-{x8HI-!w~rf>;obj{N%|b6f7zV0j?(u06k(>2??!Si?Z%mLs-D`=X_RI zt*nxs3M!GIMj-kmt4eQ}nQ90e>bXca@S5z_f!&%s;W{+xCTHAQD2ZqjS`yT>tXp|` zdBLt29hqU$SQnSvr$WkklX;c87>uP0*QEB=%}(ppDUr>0%nLw2dcBhW8!l8}tVW=# z;06y55S3keCzf(dPVU+^b}nP3vc6mZG9(1`166qDv%2A*F_`kR&@PCdJgE=%1UD6} zcfr8T?Ge)CprSZ8GXTLaqedr=HWhXQJlNlt5mgk;=2$vSo|;tLqQR z6bjR;qoX4xD(WS#jlY4O5;KM%X_i*v+(Ul#N*{xH=@kCbAYJ_(Upm; z8Y$M$j>N(+$Ud~^mX+!e{GztugcCM-VRQG+_g35`Hbe^L;{gEJ%abBV0d#T9 zQvvPP1yrObLo+%iA%T4N%X?D6NMj_$rLPnx2`z>$(P(p2$|TB9T%6HI}64 zqZ`5UzW~$?;k~}PJO{?W1dpfk@7lGi6Wm7Eb8yAiu06+KE_U(_Z}8d5+#SD)6cwdX z1%?y)rcLbZGWjK>q_AJ!C&5y#hi`f|+eR+bpmBQ5^qT=%pipqx#l^-jWIqPu$S<<0 z3=O50t}c2S)HwEpf`YlQuyE76cLplZqZyUC4#4g|@j}}=I(iE&7)CSG($jhN?o9*B zCyS^A)!L7p@rY9lHWG%Bhob!Rq_BSyL70=25o>z6Pv8UclOl)B8mEojZ5# zMH_#@6ditcjgZ;Yq!s-5F~I7RA<)df|0lCs#Pal$y41Ed{Vf1Fu77@fFm4OsfYYMMFvkrfKVL7g_@HB)RlKRO5juA*XIjGuR>^Z!u4dms2AqD1;*7)bg+H-aPn~* zU_%_;9~-Ns$4#O39tR++sXKa$=_PlC@*D>Tkk!!C+>FjykOu(;l7OBnQhB}HBnp|#5ZWmyUF3wczk0x~SoVxi2)v^BUQo!|3g?(&=7)4iFyl~?s` z#6(0?z#E&Yt3QTd@GDH_>HEfO+Ymqa8qb7!FzFvPApXr;gO1xXx=1SnvjM~xu2fCn zacb0!y2lf9jY4s!P%1d;FPcp|NOafvfm8qq}KGnFnNMTiJPg} z2tZ#xjf&D&Q}Y4LFG~RCFF{uUomYE%J5XP_@qhf!GzBCXUJqIhz(9C?Nc^8)VB4{= zch4SS7~RaX`Tn)#)L&_41qSB-R2Y-YB$0mked{d&xKW4uFg($cpr8$3DF^+R50>t0 zcsEI|Hc@yo5+JPP$J~G176Rsb-?KV5e-U_-uLU z%yEM%DK^oSH;*>jXV?8|y#1|_MEIkF)V3pAqy+HeEqY_QBwGC~9&YEsgsKF^>YD1l zk}4pch10OSi_zh0*000e>17*M*O|{s9Nt#u*iu@z z;@mE7b)_e}8@I0moo+Rq+?;|L&e<~};^*8=KA)6JhNYHMHy0o&c$Ja*VM#cl#y%f1 zNV7xp+20W0T4StREY4N$>n;9Xj=n(D^T2*zpRlU9?RTg3genCCYgBt)2*WSU2G(qP zoGyOb!6P8f3zI8sPCk`$s67koa6t?B^d7e zpR&$|$S7{Cm}sKIvuFR|5pW`oX+Lr&hjO+d{A0cjF={L~-5O3KS|smN1%Xx~h4XuRr=%={8H1 diff --git a/tests/src/browser/y-prosemirror/__screenshots__/tables.concurrent.test.tsx/table-concurrent-row-vs-column-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/tables.concurrent.test.tsx/table-concurrent-row-vs-column-chromium-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..f5b8e0ed0b36ef51d05df60526cd970900d95124 GIT binary patch literal 14084 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 literal 0 HcmV?d00001 diff --git a/tests/src/browser/y-prosemirror/__screenshots__/typeChanges.test.tsx/type-change-list-to-paragraph-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/typeChanges.test.tsx/type-change-list-to-paragraph-chromium-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..93872e5799f9afdb6f4bb9df556e1dbfd1f309b7 GIT binary patch literal 7931 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 LsgkdB|Ji>4oQNIT literal 0 HcmV?d00001 diff --git a/tests/src/browser/y-prosemirror/__screenshots__/typeChanges.test.tsx/type-change-paragraph-to-heading-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/typeChanges.test.tsx/type-change-paragraph-to-heading-chromium-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..0eb454c873c3708bc238f22d950907706588972b GIT binary patch literal 12347 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 Uu}TsCQ^2tY;&NhX_cUMqAM5zb#Q*>R literal 0 HcmV?d00001 diff --git a/tests/src/browser/y-prosemirror/addRemoveBlocks.test.tsx b/tests/src/browser/y-prosemirror/addRemoveBlocks.test.tsx index 51398f7712..1d055d9927 100644 --- a/tests/src/browser/y-prosemirror/addRemoveBlocks.test.tsx +++ b/tests/src/browser/y-prosemirror/addRemoveBlocks.test.tsx @@ -25,7 +25,7 @@ const IMG_SRC = // changes (see `typeChanges.test.tsx`). The variant in // "add paragraph after existing block" below uses `insertBlocks` on a // non-empty doc and works fine. Marked `test.fails` until upstream. -test.fails("suggestion mode: add heading to empty doc", async () => { +test("suggestion mode: add heading to empty doc", async () => { const { editor, screen, baseDoc, suggestionDoc, sync } = await setupSuggestionTest({ userAction: "add heading at top" }); @@ -46,7 +46,7 @@ test.fails("suggestion mode: add heading to empty doc", async () => { expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` " - + " @@ -62,9 +62,6 @@ test.fails("suggestion mode: add heading to empty doc", async () => { textColor="default" >New heading - - - " `); expect(editorHtml(editor)).toMatchInlineSnapshot(` @@ -77,12 +74,7 @@ test.fails("suggestion mode: add heading to empty doc", async () => { textAlignment="left" level="1" isToggleable="false" - > - New heading - - - - + >New heading " @@ -157,15 +149,9 @@ test("suggestion mode: add paragraph after existing block", async () => { isToggleable="false" >Title - - - - - Body text - - - - + + Body text + " `); @@ -246,11 +232,6 @@ test("suggestion mode: remove paragraph from heading+paragraph", async () => { isToggleable="false" >Title - - - Body text - - " `); @@ -297,9 +278,7 @@ test("suggestion mode: remove all blocks", async () => { " - - Only block - + " @@ -358,13 +337,6 @@ test("suggestion mode: delete nested block", async () => { Parent - - - - Child - - - " @@ -424,16 +396,7 @@ test("suggestion mode: delete parent block (with children)", async () => { " - - Parent - - - - - Child - - - + " @@ -451,7 +414,7 @@ test("suggestion mode: delete parent block (with children)", async () => { // content expression, so deltaToPSteps throws // `RangeError: Invalid content for node blockContainer`. Marked // `test.fails` until @y/y can represent deleting a sole atom block. -test.fails("suggestion mode: delete image block", async () => { +test("suggestion mode: delete image block", async () => { const { editor, sync } = await setupSuggestionTest({ userAction: "delete image", }); diff --git a/tests/src/browser/y-prosemirror/basicText.concurrent.test.tsx b/tests/src/browser/y-prosemirror/basicText.concurrent.test.tsx index f7be9b507c..44aad37308 100644 --- a/tests/src/browser/y-prosemirror/basicText.concurrent.test.tsx +++ b/tests/src/browser/y-prosemirror/basicText.concurrent.test.tsx @@ -128,12 +128,7 @@ test("concurrent: A fixes typo, B deletes the word", async () => { " - - hello - w - o - rold - + hello o " @@ -248,11 +243,9 @@ test("concurrent: A bolds the word, B italicises the word", async () => { hello - - - world - - + + world + diff --git a/tests/src/browser/y-prosemirror/basicText.test.tsx b/tests/src/browser/y-prosemirror/basicText.test.tsx index c40863b532..0caf732042 100644 --- a/tests/src/browser/y-prosemirror/basicText.test.tsx +++ b/tests/src/browser/y-prosemirror/basicText.test.tsx @@ -81,14 +81,7 @@ test("suggestion mode: 'hello world' -> 'hello universe'", async () => { " - - hello - wo - unive - r - ld - se - + hello universe " @@ -161,9 +154,7 @@ test("suggestion mode: add bold to 'world'", async () => { hello - - world - + world @@ -232,10 +223,7 @@ test("suggestion mode: remove bold from 'world'", async () => { " - - hello - world - + hello world " @@ -319,11 +307,9 @@ test("suggestion mode: add italic to already-bold 'world'", async () => { hello - - - world - - + + world + diff --git a/tests/src/browser/y-prosemirror/fixtures/suggestionFixture.tsx b/tests/src/browser/y-prosemirror/fixtures/suggestionFixture.tsx index babdfe0950..9dd89ad3d3 100644 --- a/tests/src/browser/y-prosemirror/fixtures/suggestionFixture.tsx +++ b/tests/src/browser/y-prosemirror/fixtures/suggestionFixture.tsx @@ -27,6 +27,7 @@ import { useCreateBlockNote } from "@blocknote/react"; import { Node as PMNode } from "@tiptap/pm/model"; import { Awareness } from "@y/protocols/awareness"; import * as Y from "@y/y"; +import { ySuggestionDecorationPluginKey } from "@y/prosemirror"; import { prettify } from "htmlfy"; import { expect } from "vitest"; import { page } from "vitest/browser"; @@ -142,7 +143,12 @@ export async function waitForSuggestion( editor: BlockNoteEditor, ): Promise { await expect - .poll(() => editor.prosemirrorState.doc.toString().includes("y-attributed")) + .poll( + () => + (ySuggestionDecorationPluginKey + .getState(editor.prosemirrorState) + ?.find().length || 0) > 0, + ) .toBe(true); } @@ -246,9 +252,7 @@ function opToXml(op: DeltaInsertOp): string { } /** Format a delta node's `attrs` map (e.g. block-level paragraph props). */ -function deltaAttrsToString( - attrs: DeltaJson["attrs"] | undefined, -): string { +function deltaAttrsToString(attrs: DeltaJson["attrs"] | undefined): string { if (attrs == null) { return ""; } diff --git a/tests/src/browser/y-prosemirror/moveBlocks.test.tsx b/tests/src/browser/y-prosemirror/moveBlocks.test.tsx index 22c8b61d06..49b768ffdd 100644 --- a/tests/src/browser/y-prosemirror/moveBlocks.test.tsx +++ b/tests/src/browser/y-prosemirror/moveBlocks.test.tsx @@ -69,23 +69,12 @@ test("suggestion mode: move paragraph up", async () => { expect(editorHtml(editor)).toMatchInlineSnapshot(` " - - - - - Middle - - - - + + Middle + First - - - Middle - - Last @@ -157,41 +146,17 @@ test("suggestion mode: move paragraph with children", async () => { expect(editorHtml(editor)).toMatchInlineSnapshot(` " - - - - - Parent - - - - - - - - - Child - - - - - - - - + + Parent + + + Child + + + First - - - Parent - - - Child - - - - " `); diff --git a/tests/src/browser/y-prosemirror/nesting.concurrent.test.tsx b/tests/src/browser/y-prosemirror/nesting.concurrent.test.tsx index ae2093ca1a..3f35bfdebb 100644 --- a/tests/src/browser/y-prosemirror/nesting.concurrent.test.tsx +++ b/tests/src/browser/y-prosemirror/nesting.concurrent.test.tsx @@ -129,34 +129,12 @@ test("concurrent: A indents N1, B indents N2 below N1", async () => { N0 - - - - - - - N1 - - - - - - + + + N1 + + - - - N1 - - - - - - - N2 - - - - " `); diff --git a/tests/src/browser/y-prosemirror/nesting.test.tsx b/tests/src/browser/y-prosemirror/nesting.test.tsx index 1fe3518a3d..72075524f4 100644 --- a/tests/src/browser/y-prosemirror/nesting.test.tsx +++ b/tests/src/browser/y-prosemirror/nesting.test.tsx @@ -78,25 +78,12 @@ test("suggestion mode: indent a block", async () => { N0 - - - - - - - N1 - - - - - - + + + N1 + + - - - N1 - - " `); @@ -158,23 +145,10 @@ test("suggestion mode: unindent a block", async () => { N0 - - - - N1 - - - - - - - - N1 - - - - + + N1 + " `); @@ -182,38 +156,82 @@ test("suggestion mode: unindent a block", async () => { // Change parent block's type while keeping its children. Hits the // known y-prosemirror type-change bug. -test.fails( - "suggestion mode: change block type of a block with children", - async () => { - const { editor, screen, baseDoc, suggestionDoc, sync } = - await setupSuggestionTest({ userAction: "parent → heading" }); - - editor.replaceBlocks(editor.document, [ - { - id: "n0", - type: "paragraph", - content: "N0", - children: [{ id: "n1", type: "paragraph", content: "N1" }], - }, - ]); - sync(); - await expect - .element(screen.getByTestId("editor-A").getByText("N0")) - .toBeVisible(); - - editor.getExtension(SuggestionsExtension)!.enableSuggestions(); - - const [parent] = editor.document; - editor.updateBlock(parent, { type: "heading", props: { level: 1 } }); - - await expect.poll(() => editor.document[0]?.type).toBe("heading"); - - await expect(screen.getByTestId("editor-root")).toMatchScreenshot( - "nesting-change-parent-type", - ); - - expect(ydocXml(baseDoc)).toMatchInlineSnapshot(); - expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(); - expect(editorHtml(editor)).toMatchInlineSnapshot(); - }, -); +test("suggestion mode: change block type of a block with children", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "parent → heading" }); + + editor.replaceBlocks(editor.document, [ + { + id: "n0", + type: "paragraph", + content: "N0", + children: [{ id: "n1", type: "paragraph", content: "N1" }], + }, + ]); + sync(); + await expect + .element(screen.getByTestId("editor-A").getByText("N0")) + .toBeVisible(); + + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + const [parent] = editor.document; + editor.updateBlock(parent, { type: "heading", props: { level: 1 } }); + + await expect.poll(() => editor.document[0]?.type).toBe("heading"); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "nesting-change-parent-type", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + N0 + + + N1 + + + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + N0 + + + N1 + + + + " + `); + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + N0 + + + N1 + + + + + " + `); +}); diff --git a/tests/src/browser/y-prosemirror/tables.concurrent.test.tsx b/tests/src/browser/y-prosemirror/tables.concurrent.test.tsx index 9abba3cc31..9899410cb1 100644 --- a/tests/src/browser/y-prosemirror/tables.concurrent.test.tsx +++ b/tests/src/browser/y-prosemirror/tables.concurrent.test.tsx @@ -28,7 +28,7 @@ const TABLE_2X2 = { // `applyChangesetToDelta: Unexpected case` in y-prosemirror when // these two suggestions sync, so this is marked `test.fails` until // upstream supports this interleaving. -test.fails("concurrent: A deletes a row, B adds a column", async () => { +test("concurrent: A deletes a row, B adds a column", async () => { const { userA, userB, @@ -82,11 +82,239 @@ test.fails("concurrent: A deletes a row, B adds a column", async () => { "table-concurrent-row-vs-column", ); - expect(ydocXml(baseDoc)).toMatchInlineSnapshot(); - expect(ydocXml(suggestionDocA)).toMatchInlineSnapshot(); - expect(ydocXml(suggestionDocB)).toMatchInlineSnapshot(); - expect(ydocXml(suggestionDocMerged)).toMatchInlineSnapshot(); - expect(editorHtml(merged.editor)).toMatchInlineSnapshot(); + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + + + + A1 + + + B1 + + + + + A2 + + + B2 + + +
+
+
" + `); + expect(ydocXml(suggestionDocA)).toMatchInlineSnapshot(` + " + + + + + A1 + + + B1 + + +
+
+
" + `); + expect(ydocXml(suggestionDocB)).toMatchInlineSnapshot(` + " + + + + + A1 + + + B1 + + + C1 + + + + + A2 + + + B2 + + + C2 + + +
+
+
" + `); + expect(ydocXml(suggestionDocMerged)).toMatchInlineSnapshot(` + " + + + + + A1 + + + B1 + + + C1 + + +
+
+
" + `); + expect(editorHtml(merged.editor)).toMatchInlineSnapshot(` + " + + + + + + A1 + + + B1 + + + C1 + + +
+
+
+
" + `); }); // Both users grow the table in independent directions: A adds a @@ -475,21 +703,15 @@ test("concurrent: A adds a row, B adds a column", async () => { > B1 - - - - - C1 - - - - + + C1 + { > B2 - - - - - C2 - - - - + + C2 + + + + + A3 + + + B3 + + + + - - - - - - - A3 - - - - - - - - - B3 - - - - - - - - - - - - - @@ -667,8 +865,28 @@ test("concurrent: A deletes a column, B adds a row", async () => { > B1 + + + + + + { > A1 + + + + + + { > B1 + + + + + + { > B3 + + + @@ -815,8 +1083,28 @@ test("concurrent: A deletes a column, B adds a row", async () => { > A1 + + + + + + { > A1 - - - B1 - - + + + + + + { > A2 - - - B2 - - - - - - - - - A3 - - - - - - - - - B3 - - - - - - + + + A3 + + + B3 + + @@ -1330,21 +1600,15 @@ test("concurrent: A adds a column, B adds a row", async () => { > B1 - - - - - C1 - - - - + + C1 + { > B2 - - - - - C2 - - - - + + C2 + + + + + A3 + + + B3 + + + + - - - - - - - A3 - - - - - - - - - B3 - - - - - - - - - - - - - diff --git a/tests/src/browser/y-prosemirror/tables.test.tsx b/tests/src/browser/y-prosemirror/tables.test.tsx index 723232be38..507c0ad5c2 100644 --- a/tests/src/browser/y-prosemirror/tables.test.tsx +++ b/tests/src/browser/y-prosemirror/tables.test.tsx @@ -232,40 +232,26 @@ test("suggestion mode: add row", async () => { B2 - - - - - - - A3 - - - - - - - - - B3 - - - - - - + + + A3 + + + B3 + + @@ -450,21 +436,15 @@ test("suggestion mode: add column", async () => { > B1 - - - - - C1 - - - - + + C1 + { > B2 - - - - - C2 - - - - + + C2 + @@ -640,28 +614,6 @@ test("suggestion mode: remove row", async () => { B1 - - - - A2 - - - B2 - - - @@ -793,17 +745,6 @@ test("suggestion mode: remove column", async () => { > A1 - - - B1 - - { > A2 - - - B2 - - @@ -980,10 +910,7 @@ test("suggestion mode: update text in cell", async () => { colspan="1" rowspan="1" > - - A1 - edited - + A1 edited { > B2 - - - @@ -1381,22 +1298,8 @@ test("suggestion mode: merge two cells", async () => { rowspan="1" colwidth="," > - - A1 - +B1 - + A1+B1 - - - B1 - - { > B2 - - - - - - - @@ -1591,26 +1481,17 @@ test("suggestion mode: split a merged cell", async () => { colspan="1" rowspan="1" > - - A1 - +B1 - + A1 + + + B1 - - - - - B1 - - - - { +test("suggestion mode: change list item to paragraph", async () => { const { editor, screen, baseDoc, suggestionDoc, sync } = await setupSuggestionTest({ userAction: "list → paragraph" }); @@ -50,13 +50,37 @@ test.fails("suggestion mode: change list item to paragraph", async () => { "type-change-list-to-paragraph", ); - expect(ydocXml(baseDoc)).toMatchInlineSnapshot(); - expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(); - expect(editorHtml(editor)).toMatchInlineSnapshot(); + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + hello world + + + " + `); }); // Promote a paragraph to a level-1 heading. Same inline content. -test.fails("suggestion mode: change paragraph to heading", async () => { +test("suggestion mode: change paragraph to heading", async () => { const { editor, screen, baseDoc, suggestionDoc, sync } = await setupSuggestionTest({ userAction: "paragraph → heading" }); @@ -79,7 +103,39 @@ test.fails("suggestion mode: change paragraph to heading", async () => { "type-change-paragraph-to-heading", ); - expect(ydocXml(baseDoc)).toMatchInlineSnapshot(); - expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(); - expect(editorHtml(editor)).toMatchInlineSnapshot(); + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + hello world + + + " + `); });