UNPKG

@blocknote/core

Version:

A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.

1 lines 217 kB
{"version":3,"file":"TrailingNode-DPu6X9ym.cjs","sources":["../src/api/exporters/html/util/serializeBlocksExternalHTML.ts","../src/api/exporters/html/externalHTMLExporter.ts","../src/api/getBlocksChangedByTransaction.ts","../src/extensions/BlockChange/BlockChange.ts","../src/extensions/Collaboration/YCursorPlugin.ts","../src/extensions/Collaboration/YSync.ts","../src/extensions/Collaboration/YUndo.ts","../src/extensions/Collaboration/ForkYDoc.ts","../src/extensions/Collaboration/schemaMigration/migrationRules/moveColorAttributes.ts","../src/extensions/Collaboration/schemaMigration/migrationRules/index.ts","../src/extensions/Collaboration/schemaMigration/SchemaMigration.ts","../src/extensions/DropCursor/DropCursor.ts","../src/extensions/FormattingToolbar/FormattingToolbar.ts","../src/extensions/History/History.ts","../src/extensions/LinkToolbar/LinkToolbar.ts","../src/extensions/LinkToolbar/protocols.ts","../src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboard.ts","../src/extensions/Placeholder/Placeholder.ts","../src/extensions/PreviousBlockType/PreviousBlockType.ts","../src/extensions/getDraggableBlockFromElement.ts","../src/api/exporters/markdown/util/removeUnderlinesRehypePlugin.ts","../src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts","../src/api/exporters/markdown/util/convertVideoToMarkdownRehypePlugin.ts","../src/api/exporters/markdown/markdownExporter.ts","../src/api/nodeConversions/fragmentToBlocks.ts","../src/extensions/SideMenu/MultipleNodeSelection.ts","../src/extensions/SideMenu/dragging.ts","../src/extensions/SideMenu/SideMenu.ts","../src/extensions/TableHandles/TableHandles.ts","../src/extensions/TrailingNode/TrailingNode.ts"],"sourcesContent":["import { DOMSerializer, Fragment, Node } from \"prosemirror-model\";\n\nimport { PartialBlock } from \"../../../../blocks/defaultBlocks.js\";\nimport type { BlockNoteEditor } from \"../../../../editor/BlockNoteEditor.js\";\nimport {\n BlockImplementation,\n BlockSchema,\n InlineContentSchema,\n StyleSchema,\n} from \"../../../../schema/index.js\";\nimport { UnreachableCaseError } from \"../../../../util/typescript.js\";\nimport {\n inlineContentToNodes,\n tableContentToNodes,\n} from \"../../../nodeConversions/blockToNode.js\";\nimport { nodeToCustomInlineContent } from \"../../../nodeConversions/nodeToBlock.js\";\n\nfunction addAttributesAndRemoveClasses(element: HTMLElement) {\n // Removes all BlockNote specific class names.\n const className =\n Array.from(element.classList).filter(\n (className) => !className.startsWith(\"bn-\"),\n ) || [];\n\n if (className.length > 0) {\n element.className = className.join(\" \");\n } else {\n element.removeAttribute(\"class\");\n }\n}\n\nexport function serializeInlineContentExternalHTML<\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n editor: BlockNoteEditor<any, I, S>,\n blockContent: PartialBlock<BSchema, I, S>[\"content\"],\n serializer: DOMSerializer,\n options?: { document?: Document },\n) {\n let nodes: Node[];\n\n // TODO: reuse function from nodeconversions?\n if (!blockContent) {\n throw new Error(\"blockContent is required\");\n } else if (typeof blockContent === \"string\") {\n nodes = inlineContentToNodes([blockContent], editor.pmSchema);\n } else if (Array.isArray(blockContent)) {\n nodes = inlineContentToNodes(blockContent, editor.pmSchema);\n } else if (blockContent.type === \"tableContent\") {\n nodes = tableContentToNodes(blockContent, editor.pmSchema);\n } else {\n throw new UnreachableCaseError(blockContent.type);\n }\n\n // Check if any of the nodes are custom inline content with toExternalHTML\n const doc = options?.document ?? document;\n const fragment = doc.createDocumentFragment();\n\n for (const node of nodes) {\n // Check if this is a custom inline content node with toExternalHTML\n if (\n node.type.name !== \"text\" &&\n editor.schema.inlineContentSchema[node.type.name]\n ) {\n const inlineContentImplementation =\n editor.schema.inlineContentSpecs[node.type.name].implementation;\n\n if (inlineContentImplementation) {\n // Convert the node to inline content format\n const inlineContent = nodeToCustomInlineContent(\n node,\n editor.schema.inlineContentSchema,\n editor.schema.styleSchema,\n );\n\n // Use the custom toExternalHTML method or fallback to `render`\n const output = inlineContentImplementation.toExternalHTML\n ? inlineContentImplementation.toExternalHTML(\n inlineContent as any,\n editor as any,\n )\n : inlineContentImplementation.render.call(\n {\n renderType: \"dom\",\n props: undefined,\n },\n inlineContent as any,\n () => {\n // No-op\n },\n editor as any,\n );\n\n if (output) {\n fragment.appendChild(output.dom);\n\n // If contentDOM exists, render the inline content into it\n if (output.contentDOM) {\n const contentFragment = serializer.serializeFragment(\n node.content,\n options,\n );\n output.contentDOM.dataset.editable = \"\";\n output.contentDOM.appendChild(contentFragment);\n }\n continue;\n }\n }\n } else if (node.type.name === \"text\") {\n // We serialize text nodes manually as we need to serialize the styles/\n // marks using `styleSpec.implementation.render`. When left up to\n // ProseMirror, it'll use `toDOM` which is incorrect.\n let dom: globalThis.Node | Text = document.createTextNode(\n node.textContent,\n );\n // Reverse the order of marks to maintain the correct priority.\n for (const mark of node.marks.toReversed()) {\n if (mark.type.name in editor.schema.styleSpecs) {\n const newDom = (\n editor.schema.styleSpecs[mark.type.name].implementation\n .toExternalHTML ??\n editor.schema.styleSpecs[mark.type.name].implementation.render\n )(mark.attrs[\"stringValue\"], editor);\n newDom.contentDOM!.appendChild(dom);\n dom = newDom.dom;\n } else {\n const domOutputSpec = mark.type.spec.toDOM!(mark, true);\n const newDom = DOMSerializer.renderSpec(document, domOutputSpec);\n newDom.contentDOM!.appendChild(dom);\n dom = newDom.dom;\n }\n }\n\n fragment.appendChild(dom);\n } else {\n // Fall back to default serialization for this node\n const nodeFragment = serializer.serializeFragment(\n Fragment.from([node]),\n options,\n );\n fragment.appendChild(nodeFragment);\n }\n }\n\n if (\n fragment.childNodes.length === 1 &&\n fragment.firstChild?.nodeType === 1 /* Node.ELEMENT_NODE */\n ) {\n addAttributesAndRemoveClasses(fragment.firstChild as HTMLElement);\n }\n\n return fragment;\n}\n\n/**\n * TODO: there's still quite some logic that handles getting and filtering properties,\n * we should make sure the `toExternalHTML` methods of default blocks actually handle this,\n * instead of the serializer.\n */\nfunction serializeBlock<\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n fragment: DocumentFragment,\n editor: BlockNoteEditor<BSchema, I, S>,\n block: PartialBlock<BSchema, I, S>,\n serializer: DOMSerializer,\n orderedListItemBlockTypes: Set<string>,\n unorderedListItemBlockTypes: Set<string>,\n options?: { document?: Document },\n) {\n const doc = options?.document ?? document;\n const BC_NODE = editor.pmSchema.nodes[\"blockContainer\"];\n\n // set default props in case we were passed a partial block\n const props = block.props || {};\n for (const [name, spec] of Object.entries(\n editor.schema.blockSchema[block.type as any].propSchema,\n )) {\n if (!(name in props) && spec.default !== undefined) {\n (props as any)[name] = spec.default;\n }\n }\n\n const bc = BC_NODE.spec?.toDOM?.(\n BC_NODE.create({\n id: block.id,\n ...props,\n }),\n ) as {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n };\n\n // the container node is just used as a workaround to get some block-level attributes.\n // we should change toExternalHTML so that this is not necessary\n const attrs = Array.from(bc.dom.attributes);\n\n const blockImplementation = editor.blockImplementations[block.type as any]\n .implementation as BlockImplementation;\n const ret =\n blockImplementation.toExternalHTML?.call(\n {},\n { ...block, props } as any,\n editor as any,\n ) ||\n blockImplementation.render.call(\n {},\n { ...block, props } as any,\n editor as any,\n );\n\n const elementFragment = doc.createDocumentFragment();\n\n if ((ret.dom as HTMLElement).classList.contains(\"bn-block-content\")) {\n const blockContentDataAttributes = [\n ...attrs,\n ...Array.from((ret.dom as HTMLElement).attributes),\n ].filter(\n (attr) =>\n attr.name.startsWith(\"data\") &&\n attr.name !== \"data-content-type\" &&\n attr.name !== \"data-file-block\" &&\n attr.name !== \"data-node-view-wrapper\" &&\n attr.name !== \"data-node-type\" &&\n attr.name !== \"data-id\" &&\n attr.name !== \"data-editable\",\n );\n\n // ret.dom = ret.dom.firstChild! as any;\n for (const attr of blockContentDataAttributes) {\n (ret.dom.firstChild! as HTMLElement).setAttribute(attr.name, attr.value);\n }\n\n addAttributesAndRemoveClasses(ret.dom.firstChild! as HTMLElement);\n elementFragment.append(...Array.from(ret.dom.childNodes));\n } else {\n elementFragment.append(ret.dom);\n }\n\n if (ret.contentDOM && block.content) {\n const ic = serializeInlineContentExternalHTML(\n editor,\n block.content as any, // TODO\n serializer,\n options,\n );\n\n ret.contentDOM.appendChild(ic);\n }\n\n let listType = undefined;\n if (orderedListItemBlockTypes.has(block.type!)) {\n listType = \"OL\";\n } else if (unorderedListItemBlockTypes.has(block.type!)) {\n listType = \"UL\";\n }\n\n if (listType) {\n if (fragment.lastChild?.nodeName !== listType) {\n const list = doc.createElement(listType);\n\n if (\n listType === \"OL\" &&\n \"start\" in props &&\n props.start &&\n props?.start !== 1\n ) {\n list.setAttribute(\"start\", props.start + \"\");\n }\n fragment.append(list);\n }\n fragment.lastChild!.appendChild(elementFragment);\n } else {\n fragment.append(elementFragment);\n }\n\n if (block.children && block.children.length > 0) {\n const childFragment = doc.createDocumentFragment();\n serializeBlocksToFragment(\n childFragment,\n editor,\n block.children,\n serializer,\n orderedListItemBlockTypes,\n unorderedListItemBlockTypes,\n options,\n );\n if (\n fragment.lastChild?.nodeName === \"UL\" ||\n fragment.lastChild?.nodeName === \"OL\"\n ) {\n // add nested lists to the last list item\n while (\n childFragment.firstChild?.nodeName === \"UL\" ||\n childFragment.firstChild?.nodeName === \"OL\"\n ) {\n fragment.lastChild!.lastChild!.appendChild(childFragment.firstChild!);\n }\n }\n\n if (editor.pmSchema.nodes[block.type as any].isInGroup(\"blockContent\")) {\n // default \"blockContainer\" style blocks are flattened (no \"nested block\" support) for externalHTML, so append the child fragment to the outer fragment\n fragment.append(childFragment);\n } else {\n // for columns / column lists, do use nesting\n ret.contentDOM?.append(childFragment);\n }\n }\n}\n\nconst serializeBlocksToFragment = <\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n fragment: DocumentFragment,\n editor: BlockNoteEditor<BSchema, I, S>,\n blocks: PartialBlock<BSchema, I, S>[],\n serializer: DOMSerializer,\n orderedListItemBlockTypes: Set<string>,\n unorderedListItemBlockTypes: Set<string>,\n options?: { document?: Document },\n) => {\n for (const block of blocks) {\n serializeBlock(\n fragment,\n editor,\n block,\n serializer,\n orderedListItemBlockTypes,\n unorderedListItemBlockTypes,\n options,\n );\n }\n};\n\nexport const serializeBlocksExternalHTML = <\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n editor: BlockNoteEditor<BSchema, I, S>,\n blocks: PartialBlock<BSchema, I, S>[],\n serializer: DOMSerializer,\n orderedListItemBlockTypes: Set<string>,\n unorderedListItemBlockTypes: Set<string>,\n options?: { document?: Document },\n) => {\n const doc = options?.document ?? document;\n const fragment = doc.createDocumentFragment();\n\n serializeBlocksToFragment(\n fragment,\n editor,\n blocks,\n serializer,\n orderedListItemBlockTypes,\n unorderedListItemBlockTypes,\n options,\n );\n return fragment;\n};\n","import { DOMSerializer, Schema } from \"prosemirror-model\";\n\nimport { PartialBlock } from \"../../../blocks/defaultBlocks.js\";\nimport type { BlockNoteEditor } from \"../../../editor/BlockNoteEditor.js\";\nimport {\n BlockSchema,\n InlineContent,\n InlineContentSchema,\n StyleSchema,\n} from \"../../../schema/index.js\";\nimport {\n serializeBlocksExternalHTML,\n serializeInlineContentExternalHTML,\n} from \"./util/serializeBlocksExternalHTML.js\";\n\n// Used to export BlockNote blocks and ProseMirror nodes to HTML for use outside\n// the editor. Blocks are exported using the `toExternalHTML` method in their\n// `blockSpec`, or `toInternalHTML` if `toExternalHTML` is not defined.\n//\n// The HTML created by this serializer is different to what's rendered by the\n// editor to the DOM. This also means that data is likely to be lost when\n// converting back to original blocks. The differences in the output HTML are:\n// 1. It doesn't include the `blockGroup` and `blockContainer` wrappers meaning\n// that nesting is not preserved for non-list-item blocks.\n// 2. `li` items in the output HTML are wrapped in `ul` or `ol` elements.\n// 3. While nesting for list items is preserved, other types of blocks nested\n// inside a list are un-nested and a new list is created after them.\n// 4. The HTML is wrapped in a single `div` element.\n\n// Needs to be sync because it's used in drag handler event (SideMenuPlugin)\nexport const createExternalHTMLExporter = <\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n schema: Schema,\n editor: BlockNoteEditor<BSchema, I, S>,\n) => {\n const serializer = DOMSerializer.fromSchema(schema);\n\n return {\n exportBlocks: (\n blocks: PartialBlock<BSchema, I, S>[],\n options: { document?: Document },\n ) => {\n const html = serializeBlocksExternalHTML(\n editor,\n blocks,\n serializer,\n new Set<string>([\"numberedListItem\"]),\n new Set<string>([\"bulletListItem\", \"checkListItem\", \"toggleListItem\"]),\n options,\n );\n const div = document.createElement(\"div\");\n div.append(html);\n return div.innerHTML;\n },\n\n exportInlineContent: (\n inlineContent: InlineContent<I, S>[],\n options: { document?: Document },\n ) => {\n const domFragment = serializeInlineContentExternalHTML(\n editor,\n inlineContent as any,\n serializer,\n options,\n );\n\n const parent = document.createElement(\"div\");\n parent.append(domFragment.cloneNode(true));\n\n return parent.innerHTML;\n },\n };\n};\n","import { combineTransactionSteps } from \"@tiptap/core\";\nimport deepEqual from \"fast-deep-equal\";\nimport type { Node } from \"prosemirror-model\";\nimport type { Transaction } from \"prosemirror-state\";\nimport {\n Block,\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema,\n} from \"../blocks/defaultBlocks.js\";\nimport type { BlockSchema } from \"../schema/index.js\";\nimport type { InlineContentSchema } from \"../schema/inlineContent/types.js\";\nimport type { StyleSchema } from \"../schema/styles/types.js\";\nimport { nodeToBlock } from \"./nodeConversions/nodeToBlock.js\";\nimport { isNodeBlock } from \"./nodeUtil.js\";\nimport { getPmSchema } from \"./pmUtil.js\";\n\n/**\n * Change detection utilities for BlockNote.\n *\n * High-level algorithm used by getBlocksChangedByTransaction:\n * 1) Merge appended transactions into one document change.\n * 2) Collect a snapshot of blocks before and after (flat map by id, and per-parent child order).\n * 3) Emit inserts and deletes by diffing ids between snapshots.\n * 4) For ids present in both snapshots:\n * - If parentId changed, emit a move\n * - Else if block changed (ignoring children), emit an update\n * 5) Finally, detect same-parent sibling reorders by comparing child order per parent.\n * We use an inlined O(n log n) LIS inside detectReorderedChildren to keep a\n * longest already-ordered subsequence and mark only the remaining items as moved.\n */\n/**\n * Gets the parent block of a node, if it has one.\n */\nfunction getParentBlockId(doc: Node, pos: number): string | undefined {\n if (pos === 0) {\n return undefined;\n }\n const resolvedPos = doc.resolve(pos);\n for (let i = resolvedPos.depth; i > 0; i--) {\n const parent = resolvedPos.node(i);\n if (isNodeBlock(parent)) {\n return parent.attrs.id;\n }\n }\n return undefined;\n}\n\n/**\n * This attributes the changes to a specific source.\n */\nexport type BlockChangeSource =\n | { type: \"local\" }\n | { type: \"paste\" }\n | { type: \"drop\" }\n | { type: \"undo\" | \"redo\" | \"undo-redo\" }\n | { type: \"yjs-remote\" };\n\nexport type BlocksChanged<\n BSchema extends BlockSchema = DefaultBlockSchema,\n ISchema extends InlineContentSchema = DefaultInlineContentSchema,\n SSchema extends StyleSchema = DefaultStyleSchema,\n> = Array<\n {\n /**\n * The affected block.\n */\n block: Block<BSchema, ISchema, SSchema>;\n /**\n * The source of the change.\n */\n source: BlockChangeSource;\n } & (\n | {\n type: \"insert\" | \"delete\";\n /**\n * Insert and delete changes don't have a previous block.\n */\n prevBlock: undefined;\n }\n | {\n type: \"update\";\n /**\n * The previous block.\n */\n prevBlock: Block<BSchema, ISchema, SSchema>;\n }\n | {\n type: \"move\";\n /**\n * The affected block.\n */\n block: Block<BSchema, ISchema, SSchema>;\n /**\n * The block before the move.\n */\n prevBlock: Block<BSchema, ISchema, SSchema>;\n /**\n * The previous parent block (if it existed).\n */\n prevParent?: Block<BSchema, ISchema, SSchema>;\n /**\n * The current parent block (if it exists).\n */\n currentParent?: Block<BSchema, ISchema, SSchema>;\n }\n )\n>;\n\nfunction determineChangeSource(transaction: Transaction): BlockChangeSource {\n if (transaction.getMeta(\"paste\")) {\n return { type: \"paste\" };\n }\n if (transaction.getMeta(\"uiEvent\") === \"drop\") {\n return { type: \"drop\" };\n }\n if (transaction.getMeta(\"history$\")) {\n return {\n type: transaction.getMeta(\"history$\").redo ? \"redo\" : \"undo\",\n };\n }\n if (transaction.getMeta(\"y-sync$\")) {\n if (transaction.getMeta(\"y-sync$\").isUndoRedoOperation) {\n return { type: \"undo-redo\" };\n }\n return { type: \"yjs-remote\" };\n }\n return { type: \"local\" };\n}\n\ntype BlockSnapshot<\n BSchema extends BlockSchema,\n ISchema extends InlineContentSchema,\n SSchema extends StyleSchema,\n> = {\n byId: Record<\n string,\n {\n block: Block<BSchema, ISchema, SSchema>;\n parentId: string | undefined;\n }\n >;\n childrenByParent: Record<string, string[]>;\n};\n\n/**\n * Collects a snapshot of blocks and per-parent child order in a single traversal.\n * Uses \"__root__\" to represent the root level where parentId is undefined.\n */\nfunction collectSnapshot<\n BSchema extends BlockSchema,\n ISchema extends InlineContentSchema,\n SSchema extends StyleSchema,\n>(doc: Node): BlockSnapshot<BSchema, ISchema, SSchema> {\n const ROOT_KEY = \"__root__\";\n const byId: Record<\n string,\n {\n block: Block<BSchema, ISchema, SSchema>;\n parentId: string | undefined;\n }\n > = {};\n const childrenByParent: Record<string, string[]> = {};\n const pmSchema = getPmSchema(doc);\n doc.descendants((node, pos) => {\n if (!isNodeBlock(node)) {\n return true;\n }\n const parentId = getParentBlockId(doc, pos);\n const key = parentId ?? ROOT_KEY;\n if (!childrenByParent[key]) {\n childrenByParent[key] = [];\n }\n const block = nodeToBlock(node, pmSchema);\n byId[node.attrs.id] = { block, parentId };\n childrenByParent[key].push(node.attrs.id);\n return true;\n });\n return { byId, childrenByParent };\n}\n\n/**\n * Determines which child ids have been reordered (moved) within the same parent.\n * Uses LIS to keep the longest ordered subsequence and marks the rest as moved.\n */\nfunction detectReorderedChildren(\n prevOrder: string[] | undefined,\n nextOrder: string[] | undefined,\n): Set<string> {\n const moved = new Set<string>();\n if (!prevOrder || !nextOrder) {\n return moved;\n }\n // Consider only ids present in both orders (ignore inserts/deletes handled elsewhere)\n const prevIds = new Set(prevOrder);\n const commonNext: string[] = nextOrder.filter((id) => prevIds.has(id));\n const commonPrev: string[] = prevOrder.filter((id) =>\n commonNext.includes(id),\n );\n\n if (commonPrev.length <= 1 || commonNext.length <= 1) {\n return moved;\n }\n\n // Map ids to their index in previous order\n const indexInPrev: Record<string, number> = {};\n for (let i = 0; i < commonPrev.length; i++) {\n indexInPrev[commonPrev[i]] = i;\n }\n\n // Build sequence of indices representing next order in terms of previous indices\n const sequence: number[] = commonNext.map((id) => indexInPrev[id]);\n\n // Inline O(n log n) LIS with reconstruction.\n // Why LIS? We want the smallest set of siblings to label as \"moved\".\n // Keeping the longest subsequence that is already in order achieves this,\n // so only items outside the LIS are reported as moves.\n const n = sequence.length;\n const tailsValues: number[] = [];\n const tailsEndsAtIndex: number[] = [];\n const previousIndexInLis: number[] = new Array(n).fill(-1);\n\n const lowerBound = (arr: number[], target: number): number => {\n let lo = 0;\n let hi = arr.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n if (arr[mid] < target) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n return lo;\n };\n\n for (let i = 0; i < n; i++) {\n const value = sequence[i];\n const pos = lowerBound(tailsValues, value);\n if (pos > 0) {\n previousIndexInLis[i] = tailsEndsAtIndex[pos - 1];\n }\n if (pos === tailsValues.length) {\n tailsValues.push(value);\n tailsEndsAtIndex.push(i);\n } else {\n tailsValues[pos] = value;\n tailsEndsAtIndex[pos] = i;\n }\n }\n\n const lisIndexSet = new Set<number>();\n let k = tailsEndsAtIndex[tailsEndsAtIndex.length - 1] ?? -1;\n while (k !== -1) {\n lisIndexSet.add(k);\n k = previousIndexInLis[k];\n }\n\n // Items not part of LIS are considered moved\n for (let i = 0; i < commonNext.length; i++) {\n if (!lisIndexSet.has(i)) {\n moved.add(commonNext[i]);\n }\n }\n return moved;\n}\n\n/**\n * Get the blocks that were changed by a transaction.\n */\nexport function getBlocksChangedByTransaction<\n BSchema extends BlockSchema = DefaultBlockSchema,\n ISchema extends InlineContentSchema = DefaultInlineContentSchema,\n SSchema extends StyleSchema = DefaultStyleSchema,\n>(\n transaction: Transaction,\n appendedTransactions: Transaction[] = [],\n): BlocksChanged<BSchema, ISchema, SSchema> {\n const source = determineChangeSource(transaction);\n const combinedTransaction = combineTransactionSteps(transaction.before, [\n transaction,\n ...appendedTransactions,\n ]);\n\n const prevSnap = collectSnapshot<BSchema, ISchema, SSchema>(\n combinedTransaction.before,\n );\n const nextSnap = collectSnapshot<BSchema, ISchema, SSchema>(\n combinedTransaction.doc,\n );\n\n const changes: BlocksChanged<BSchema, ISchema, SSchema> = [];\n const changedIds = new Set<string>();\n\n // Handle inserted blocks\n Object.keys(nextSnap.byId)\n .filter((id) => !(id in prevSnap.byId))\n .forEach((id) => {\n changes.push({\n type: \"insert\",\n block: nextSnap.byId[id].block,\n source,\n prevBlock: undefined,\n });\n changedIds.add(id);\n });\n\n // Handle deleted blocks\n Object.keys(prevSnap.byId)\n .filter((id) => !(id in nextSnap.byId))\n .forEach((id) => {\n changes.push({\n type: \"delete\",\n block: prevSnap.byId[id].block,\n source,\n prevBlock: undefined,\n });\n changedIds.add(id);\n });\n\n // Handle updated, moved to different parent, indented, outdented blocks\n Object.keys(nextSnap.byId)\n .filter((id) => id in prevSnap.byId)\n .forEach((id) => {\n const prev = prevSnap.byId[id];\n const next = nextSnap.byId[id];\n const isParentDifferent = prev.parentId !== next.parentId;\n\n if (isParentDifferent) {\n changes.push({\n type: \"move\",\n block: next.block,\n prevBlock: prev.block,\n source,\n prevParent: prev.parentId\n ? prevSnap.byId[prev.parentId]?.block\n : undefined,\n currentParent: next.parentId\n ? nextSnap.byId[next.parentId]?.block\n : undefined,\n });\n changedIds.add(id);\n } else if (\n // Compare blocks while ignoring children to avoid reporting a parent\n // update when only descendants changed.\n !deepEqual(\n { ...prev.block, children: undefined } as any,\n { ...next.block, children: undefined } as any,\n )\n ) {\n changes.push({\n type: \"update\",\n block: next.block,\n prevBlock: prev.block,\n source,\n });\n changedIds.add(id);\n }\n });\n\n // Handle sibling reorders (parent unchanged but relative order changed)\n const prevOrderByParent = prevSnap.childrenByParent;\n const nextOrderByParent = nextSnap.childrenByParent;\n\n // Use a special key for root-level siblings\n const ROOT_KEY = \"__root__\";\n const parents = new Set<string>([\n ...Object.keys(prevOrderByParent),\n ...Object.keys(nextOrderByParent),\n ]);\n\n const addedMoveForId = new Set<string>();\n\n parents.forEach((parentKey) => {\n const movedWithinParent = detectReorderedChildren(\n prevOrderByParent[parentKey],\n nextOrderByParent[parentKey],\n );\n if (movedWithinParent.size === 0) {\n return;\n }\n movedWithinParent.forEach((id) => {\n // Only consider ids that exist in both snapshots and whose parent truly did not change\n const prev = prevSnap.byId[id];\n const next = nextSnap.byId[id];\n if (!prev || !next) {\n return;\n }\n if (prev.parentId !== next.parentId) {\n return;\n }\n // Skip if already accounted for by insert/delete/update/parent move\n if (changedIds.has(id)) {\n return;\n }\n // Verify we're addressing the right parent bucket\n const bucketKey = prev.parentId ?? ROOT_KEY;\n if (bucketKey !== parentKey) {\n return;\n }\n if (addedMoveForId.has(id)) {\n return;\n }\n addedMoveForId.add(id);\n changes.push({\n type: \"move\",\n block: next.block,\n prevBlock: prev.block,\n source,\n prevParent: prev.parentId\n ? prevSnap.byId[prev.parentId]?.block\n : undefined,\n currentParent: next.parentId\n ? nextSnap.byId[next.parentId]?.block\n : undefined,\n });\n changedIds.add(id);\n });\n });\n\n return changes;\n}\n","import { Plugin, PluginKey, Transaction } from \"prosemirror-state\";\nimport {\n BlocksChanged,\n getBlocksChangedByTransaction,\n} from \"../../api/getBlocksChangedByTransaction.js\";\nimport { createExtension } from \"../../editor/BlockNoteExtension.js\";\n\n/**\n * This plugin can filter transactions before they are applied to the editor, but with a higher-level API than `filterTransaction` from prosemirror.\n */\nexport const BlockChangeExtension = createExtension(() => {\n const beforeChangeCallbacks: ((context: {\n getChanges: () => BlocksChanged<any, any, any>;\n tr: Transaction;\n }) => boolean | void)[] = [];\n return {\n key: \"blockChange\",\n prosemirrorPlugins: [\n new Plugin({\n key: new PluginKey(\"blockChange\"),\n filterTransaction: (tr) => {\n let changes:\n | ReturnType<typeof getBlocksChangedByTransaction<any, any, any>>\n | undefined = undefined;\n\n return beforeChangeCallbacks.reduce((acc, cb) => {\n if (acc === false) {\n // We only care that we hit a `false` result, so we can stop iterating.\n return acc;\n }\n return (\n cb({\n getChanges() {\n if (changes) {\n return changes;\n }\n changes = getBlocksChangedByTransaction<any, any, any>(tr);\n return changes;\n },\n tr,\n }) !== false\n );\n }, true);\n },\n }),\n ],\n\n /**\n * Subscribe to the block change events.\n */\n subscribe(\n callback: (context: {\n getChanges: () => BlocksChanged<any, any, any>;\n tr: Transaction;\n }) => boolean | void,\n ) {\n beforeChangeCallbacks.push(callback);\n\n return () => {\n beforeChangeCallbacks.splice(\n beforeChangeCallbacks.indexOf(callback),\n 1,\n );\n };\n },\n } as const;\n});\n","import { defaultSelectionBuilder, yCursorPlugin } from \"y-prosemirror\";\nimport {\n createExtension,\n ExtensionOptions,\n} from \"../../editor/BlockNoteExtension.js\";\nimport { CollaborationOptions } from \"./Collaboration.js\";\n\nexport type CollaborationUser = {\n name: string;\n color: string;\n [key: string]: string;\n};\n\n/**\n * Determine whether the foreground color should be white or black based on a provided background color\n * Inspired by: https://stackoverflow.com/a/3943023\n */\nfunction isDarkColor(bgColor: string): boolean {\n const color = bgColor.charAt(0) === \"#\" ? bgColor.substring(1, 7) : bgColor;\n const r = parseInt(color.substring(0, 2), 16); // hexToR\n const g = parseInt(color.substring(2, 4), 16); // hexToG\n const b = parseInt(color.substring(4, 6), 16); // hexToB\n const uicolors = [r / 255, g / 255, b / 255];\n const c = uicolors.map((col) => {\n if (col <= 0.03928) {\n return col / 12.92;\n }\n return Math.pow((col + 0.055) / 1.055, 2.4);\n });\n const L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];\n return L <= 0.179;\n}\n\nfunction defaultCursorRender(user: CollaborationUser) {\n const cursorElement = document.createElement(\"span\");\n\n cursorElement.classList.add(\"bn-collaboration-cursor__base\");\n\n const caretElement = document.createElement(\"span\");\n caretElement.setAttribute(\"contentedEditable\", \"false\");\n caretElement.classList.add(\"bn-collaboration-cursor__caret\");\n caretElement.setAttribute(\n \"style\",\n `background-color: ${user.color}; color: ${\n isDarkColor(user.color) ? \"white\" : \"black\"\n }`,\n );\n\n const labelElement = document.createElement(\"span\");\n\n labelElement.classList.add(\"bn-collaboration-cursor__label\");\n labelElement.setAttribute(\n \"style\",\n `background-color: ${user.color}; color: ${\n isDarkColor(user.color) ? \"white\" : \"black\"\n }`,\n );\n labelElement.insertBefore(document.createTextNode(user.name), null);\n\n caretElement.insertBefore(labelElement, null);\n\n cursorElement.insertBefore(document.createTextNode(\"\\u2060\"), null); // Non-breaking space\n cursorElement.insertBefore(caretElement, null);\n cursorElement.insertBefore(document.createTextNode(\"\\u2060\"), null); // Non-breaking space\n\n return cursorElement;\n}\n\nexport const YCursorExtension = createExtension(\n ({ options }: ExtensionOptions<CollaborationOptions>) => {\n const recentlyUpdatedCursors = new Map();\n const awareness =\n options.provider &&\n \"awareness\" in options.provider &&\n typeof options.provider.awareness === \"object\"\n ? options.provider.awareness\n : undefined;\n if (awareness) {\n if (\n \"setLocalStateField\" in awareness &&\n typeof awareness.setLocalStateField === \"function\"\n ) {\n awareness.setLocalStateField(\"user\", options.user);\n }\n if (\"on\" in awareness && typeof awareness.on === \"function\") {\n if (options.showCursorLabels !== \"always\") {\n awareness.on(\n \"change\",\n ({\n updated,\n }: {\n added: Array<number>;\n updated: Array<number>;\n removed: Array<number>;\n }) => {\n for (const clientID of updated) {\n const cursor = recentlyUpdatedCursors.get(clientID);\n\n if (cursor) {\n cursor.element.setAttribute(\"data-active\", \"\");\n\n if (cursor.hideTimeout) {\n clearTimeout(cursor.hideTimeout);\n }\n\n recentlyUpdatedCursors.set(clientID, {\n element: cursor.element,\n hideTimeout: setTimeout(() => {\n cursor.element.removeAttribute(\"data-active\");\n }, 2000),\n });\n }\n }\n },\n );\n }\n }\n }\n\n return {\n key: \"yCursor\",\n prosemirrorPlugins: [\n awareness\n ? yCursorPlugin(awareness, {\n selectionBuilder: defaultSelectionBuilder,\n cursorBuilder(user: CollaborationUser, clientID: number) {\n let cursorData = recentlyUpdatedCursors.get(clientID);\n\n if (!cursorData) {\n const cursorElement = (\n options.renderCursor ?? defaultCursorRender\n )(user);\n\n if (options.showCursorLabels !== \"always\") {\n cursorElement.addEventListener(\"mouseenter\", () => {\n const cursor = recentlyUpdatedCursors.get(clientID)!;\n cursor.element.setAttribute(\"data-active\", \"\");\n\n if (cursor.hideTimeout) {\n clearTimeout(cursor.hideTimeout);\n recentlyUpdatedCursors.set(clientID, {\n element: cursor.element,\n hideTimeout: undefined,\n });\n }\n });\n\n cursorElement.addEventListener(\"mouseleave\", () => {\n const cursor = recentlyUpdatedCursors.get(clientID)!;\n\n recentlyUpdatedCursors.set(clientID, {\n element: cursor.element,\n hideTimeout: setTimeout(() => {\n cursor.element.removeAttribute(\"data-active\");\n }, 2000),\n });\n });\n }\n\n cursorData = {\n element: cursorElement,\n hideTimeout: undefined,\n };\n\n recentlyUpdatedCursors.set(clientID, cursorData);\n }\n\n return cursorData.element;\n },\n })\n : undefined,\n ].filter(Boolean),\n dependsOn: [\"ySync\"],\n updateUser(user: { name: string; color: string; [key: string]: string }) {\n awareness?.setLocalStateField(\"user\", user);\n },\n } as const;\n },\n);\n","import { ySyncPlugin } from \"y-prosemirror\";\nimport {\n ExtensionOptions,\n createExtension,\n} from \"../../editor/BlockNoteExtension.js\";\nimport { CollaborationOptions } from \"./Collaboration.js\";\n\nexport const YSyncExtension = createExtension(\n ({ options }: ExtensionOptions<Pick<CollaborationOptions, \"fragment\">>) => {\n return {\n key: \"ySync\",\n prosemirrorPlugins: [ySyncPlugin(options.fragment)],\n runsBefore: [\"default\"],\n } as const;\n },\n);\n","import { redoCommand, undoCommand, yUndoPlugin } from \"y-prosemirror\";\nimport { createExtension } from \"../../editor/BlockNoteExtension.js\";\n\nexport const YUndoExtension = createExtension(() => {\n return {\n key: \"yUndo\",\n prosemirrorPlugins: [yUndoPlugin()],\n dependsOn: [\"yCursor\", \"ySync\"],\n undoCommand: undoCommand,\n redoCommand: redoCommand,\n } as const;\n});\n","import { yUndoPluginKey } from \"y-prosemirror\";\nimport * as Y from \"yjs\";\nimport {\n createExtension,\n createStore,\n ExtensionOptions,\n} from \"../../editor/BlockNoteExtension.js\";\nimport { CollaborationOptions } from \"./Collaboration.js\";\nimport { YCursorExtension } from \"./YCursorPlugin.js\";\nimport { YSyncExtension } from \"./YSync.js\";\nimport { YUndoExtension } from \"./YUndo.js\";\n\n/**\n * To find a fragment in another ydoc, we need to search for it.\n */\nfunction findTypeInOtherYdoc<T extends Y.AbstractType<any>>(\n ytype: T,\n otherYdoc: Y.Doc,\n): T {\n const ydoc = ytype.doc!;\n if (ytype._item === null) {\n /**\n * If is a root type, we need to find the root key in the original ydoc\n * and use it to get the type in the other ydoc.\n */\n const rootKey = Array.from(ydoc.share.keys()).find(\n (key) => ydoc.share.get(key) === ytype,\n );\n if (rootKey == null) {\n throw new Error(\"type does not exist in other ydoc\");\n }\n return otherYdoc.get(rootKey, ytype.constructor as new () => T) as T;\n } else {\n /**\n * If it is a sub type, we use the item id to find the history type.\n */\n const ytypeItem = ytype._item;\n const otherStructs = otherYdoc.store.clients.get(ytypeItem.id.client) ?? [];\n const itemIndex = Y.findIndexSS(otherStructs, ytypeItem.id.clock);\n const otherItem = otherStructs[itemIndex] as Y.Item;\n const otherContent = otherItem.content as Y.ContentType;\n return otherContent.type as T;\n }\n}\n\nexport const ForkYDocExtension = createExtension(\n ({ editor, options }: ExtensionOptions<CollaborationOptions>) => {\n let forkedState:\n | {\n originalFragment: Y.XmlFragment;\n undoStack: Y.UndoManager[\"undoStack\"];\n forkedFragment: Y.XmlFragment;\n }\n | undefined = undefined;\n\n const store = createStore({ isForked: false });\n\n return {\n key: \"yForkDoc\",\n store,\n /**\n * Fork the Y.js document from syncing to the remote,\n * allowing modifications to the document without affecting the remote.\n * These changes can later be rolled back or applied to the remote.\n */\n fork() {\n if (forkedState) {\n return;\n }\n\n const originalFragment = options.fragment;\n\n if (!originalFragment) {\n throw new Error(\"No fragment to fork from\");\n }\n\n const doc = new Y.Doc();\n // Copy the original document to a new Yjs document\n Y.applyUpdate(doc, Y.encodeStateAsUpdate(originalFragment.doc!));\n\n // Find the forked fragment in the new Yjs document\n const forkedFragment = findTypeInOtherYdoc(originalFragment, doc);\n\n forkedState = {\n undoStack: yUndoPluginKey.getState(editor.prosemirrorState)!\n .undoManager.undoStack,\n originalFragment,\n forkedFragment,\n };\n\n // Need to reset all the yjs plugins\n editor.unregisterExtension([\n YUndoExtension,\n YCursorExtension,\n YSyncExtension,\n ]);\n const newOptions = {\n ...options,\n fragment: forkedFragment,\n };\n // Register them again, based on the new forked fragment\n editor.registerExtension([\n YSyncExtension(newOptions),\n // No need to register the cursor plugin again, it's a local fork\n YUndoExtension(),\n ]);\n\n // Tell the store that the editor is now forked\n store.setState({ isForked: true });\n },\n\n /**\n * Resume syncing the Y.js document to the remote\n * If `keepChanges` is true, any changes that have been made to the forked document will be applied to the original document.\n * Otherwise, the original document will be restored and the changes will be discarded.\n */\n merge({ keepChanges }: { keepChanges: boolean }) {\n if (!forkedState) {\n return;\n }\n // Remove the forked fragment's plugins\n editor.unregisterExtension([\"ySync\", \"yCursor\", \"yUndo\"]);\n\n const { originalFragment, forkedFragment, undoStack } = forkedState;\n // Register the plugins again, based on the original fragment (which is still in the original options)\n editor.registerExtension([\n YSyncExtension(options),\n YCursorExtension(options),\n YUndoExtension(),\n ]);\n\n // Reset the undo stack to the original undo stack\n yUndoPluginKey.getState(\n editor.prosemirrorState,\n )!.undoManager.undoStack = undoStack;\n\n if (keepChanges) {\n // Apply any changes that have been made to the fork, onto the original doc\n const update = Y.encodeStateAsUpdate(\n forkedFragment.doc!,\n Y.encodeStateVector(originalFragment.doc!),\n );\n // Applying this change will add to the undo stack, allowing it to be undone normally\n Y.applyUpdate(originalFragment.doc!, update, editor);\n }\n // Reset the forked state\n forkedState = undefined;\n // Tell the store that the editor is no longer forked\n store.setState({ isForked: false });\n },\n } as const;\n },\n);\n","import * as Y from \"yjs\";\n\nimport { MigrationRule } from \"./migrationRule.js\";\nimport { defaultProps } from \"../../../../blocks/defaultProps.js\";\n\n// Helper function to recursively traverse a `Y.XMLElement` and its descendant\n// elements.\nconst traverseElement = (\n rootElement: Y.XmlElement,\n cb: (element: Y.XmlElement) => void,\n) => {\n cb(rootElement);\n rootElement.forEach((element) => {\n if (element instanceof Y.XmlElement) {\n traverseElement(element, cb);\n }\n });\n};\n\n// Moves `textColor` and `backgroundColor` attributes from `blockContainer`\n// nodes to their child `blockContent` nodes. This is due to a schema change\n// introduced in PR #TODO.\nexport const moveColorAttributes: MigrationRule = (fragment, tr) => {\n // Stores necessary info for all `blockContainer` nodes which still have\n // `textColor` or `backgroundColor` attributes that need to be moved.\n const targetBlockContainers: Map<\n string,\n {\n textColor: string | undefined;\n backgroundColor: string | undefined;\n }\n > = new Map();\n // Finds all elements which still have `textColor` or `backgroundColor`\n // attributes in the current Yjs fragment.\n fragment.forEach((element) => {\n if (element instanceof Y.XmlElement) {\n traverseElement(element, (element) => {\n if (\n element.nodeName === \"blockContainer\" &&\n element.hasAttribute(\"id\")\n ) {\n const textColor = element.getAttribute(\"textColor\");\n const backgroundColor = element.getAttribute(\"backgroundColor\");\n\n const colors = {\n textColor:\n textColor === defaultProps.textColor.default\n ? undefined\n : textColor,\n backgroundColor:\n backgroundColor === defaultProps.backgroundColor.default\n ? undefined\n : backgroundColor,\n };\n\n if (colors.textColor || colors.backgroundColor) {\n targetBlockContainers.set(element.getAttribute(\"id\")!, colors);\n }\n }\n });\n }\n });\n\n if (targetBlockContainers.size === 0) {\n return false;\n }\n\n // Appends transactions to add the `textColor` and `backgroundColor`\n // attributes found on each `blockContainer` node to move them to the child\n // `blockContent` node.\n tr.doc.descendants((node, pos) => {\n if (\n node.type.name === \"blockContainer\" &&\n targetBlockContainers.has(node.attrs.id)\n ) {\n const el = tr.doc.nodeAt(pos + 1);\n if (!el) {\n throw new Error(\"No element found\");\n }\n\n tr.setNodeMarkup(pos + 1, undefined, {\n // preserve existing attributes\n ...el.attrs,\n // add the textColor and backgroundColor attributes\n ...targetBlockContainers.get(node.attrs.id),\n });\n }\n });\n\n return true;\n};\n","import { MigrationRule } from \"./migrationRule.js\";\nimport { moveColorAttributes } from \"./moveColorAttributes.js\";\n\nexport default [moveColorAttributes] as MigrationRule[];\n","import { Plugin, PluginKey } from \"@tiptap/pm/state\";\nimport * as Y from \"yjs\";\n\nimport {\n createExtension,\n ExtensionOptions,\n} from \"../../../editor/BlockNoteExtension.js\";\nimport migrationRules from \"./migrationRules/index.js\";\n\n// This plugin allows us to update collaboration YDocs whenever BlockNote's\n// underlying ProseMirror schema changes. The plugin reads the current Yjs\n// fragment and dispatches additional transactions to the ProseMirror state, in\n// case things are found in the fragment that don't adhere to the editor schema\n// and need to be fixed. These fixes are defined as `MigrationRule`s within the\n// `migrationRules` directory.\nexport const SchemaMigration = createExtension(\n ({ options }: ExtensionOptions<{ fragment: Y.XmlFragment }>) => {\n let migrationDone = false;\n const pluginKey = new PluginKey(\"schemaMigration\");\n\n return {\n key: \"schemaMigration\",\n prosemirrorPlugins: [\n new Plugin({\n key: pluginKey,\n appendTransaction: (transactions, _oldState, newState) => {\n if (migrationDone) {\n return undefined;\n }\n\n if (\n // If any of the transactions are not due to a yjs sync, we don't need to run the migration\n !transactions.some((tr) => tr.getMeta(\"y-sync$\")) ||\n // If none of the transactions result in a document change, we don't need to run the migration\n transactions.every((tr) => !tr.docChanged) ||\n // If the fragment is still empty, we can't run the migration (since it has not yet been applied to the Y.Doc)\n !options.fragment.firstChild\n ) {\n return undefined;\n }\n\n const tr = newState.tr;\n for (const migrationRule of migrationRules) {\n migrationRule(options.fragment, tr);\n }\n\n migrationDone = true;\n\n if (!tr.docChanged) {\n return undefined;\n }\n\n return tr;\n },\n }),\n ],\n } as const;\n },\n);\n","import { dropCursor } from \"prosemirror-dropcursor\";\nimport {\n createExtension,\n ExtensionOptions,\n} from \"../../editor/BlockNoteExtension.js\";\nimport { BlockNoteEditorOptions } from \"../../editor/BlockNoteEditor.js\";\n\nexport const DropCursorExtension = createExtension(\n ({\n editor,\n options,\n }: ExtensionOptions<\n Pick<BlockNoteEditorOptions<any, any, any>, \"dropCursor\">\n >) => {\n return {\n key: \"dropCursor\",\n prosemirrorPlugins: [\n (options.dropCursor ?? dropCursor)({\n width: 5,\n color: \"#ddeeff\",\n editor: editor,\n }),\n ],\n } as const;\n },\n);\n","import { NodeSelection, TextSelection } from \"prosemirror-state\";\n\nimport {\n createExtension,\n createStore,\n} from \"../../editor/BlockNoteExtension.js\";\n\nexport const FormattingToolbarExtension = createExtension(({ editor }) => {\n const store = createStore(false);\n\n const shouldShow = () => {\n return editor.transact((tr) => {\n // Don't show if the selection is empty, or is a text selection with no\n // text.\n if (tr.selection.empty) {\n return false;\n }\n\n // Don't show if a block with inline content is selected.\n if (\n tr.selection instanceof NodeSelecti