UNPKG

@blocknote/core

Version:

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

1 lines 122 kB
{"version":3,"file":"blockToNode-CumVjgem.cjs","sources":["../src/extensions/tiptap-extensions/UniqueID/UniqueID.ts","../src/schema/inlineContent/types.ts","../src/util/table.ts","../src/util/typescript.ts","../src/api/getBlockInfoFromPos.ts","../src/api/pmUtil.ts","../src/api/nodeConversions/nodeToBlock.ts","../src/api/blockManipulation/tables/tables.ts","../src/api/nodeConversions/blockToNode.ts"],"sourcesContent":["import {\n combineTransactionSteps,\n Extension,\n findChildrenInRange,\n getChangedRanges,\n} from \"@tiptap/core\";\nimport { Fragment, Slice } from \"prosemirror-model\";\nimport { Plugin, PluginKey } from \"prosemirror-state\";\nimport { v4 } from \"uuid\";\n\n/**\n * Code from Tiptap UniqueID extension (https://tiptap.dev/api/extensions/unique-id)\n * This extension is licensed under MIT (even though it's part of Tiptap pro).\n *\n * If you're a user of BlockNote, we still recommend to support their awesome work and become a sponsor!\n * https://tiptap.dev/pro\n */\n\n/**\n * Removes duplicated values within an array.\n * Supports numbers, strings and objects.\n */\nfunction removeDuplicates(array: any, by = JSON.stringify) {\n const seen: any = {};\n return array.filter((item: any) => {\n const key = by(item);\n return Object.prototype.hasOwnProperty.call(seen, key)\n ? false\n : (seen[key] = true);\n });\n}\n\n/**\n * Returns a list of duplicated items within an array.\n */\nfunction findDuplicates(items: any) {\n const filtered = items.filter(\n (el: any, index: number) => items.indexOf(el) !== index,\n );\n const duplicates = removeDuplicates(filtered);\n return duplicates;\n}\n\nconst UniqueID = Extension.create({\n name: \"uniqueID\",\n // we’ll set a very high priority to make sure this runs first\n // and is compatible with `appendTransaction` hooks of other extensions\n priority: 10000,\n addOptions() {\n return {\n attributeName: \"id\",\n types: [],\n setIdAttribute: false,\n generateID: () => {\n // Use mock ID if tests are running.\n if (typeof window !== \"undefined\" && (window as any).__TEST_OPTIONS) {\n const testOptions = (window as any).__TEST_OPTIONS;\n if (testOptions.mockID === undefined) {\n testOptions.mockID = 0;\n } else {\n testOptions.mockID++;\n }\n\n return testOptions.mockID.toString() as string;\n }\n\n return v4();\n },\n filterTransaction: null,\n };\n },\n addGlobalAttributes() {\n return [\n {\n types: this.options.types,\n attributes: {\n [this.options.attributeName]: {\n default: null,\n parseHTML: (element) =>\n element.getAttribute(`data-${this.options.attributeName}`),\n renderHTML: (attributes) => {\n const defaultIdAttributes = {\n [`data-${this.options.attributeName}`]:\n attributes[this.options.attributeName],\n };\n if (this.options.setIdAttribute) {\n return {\n ...defaultIdAttributes,\n id: attributes[this.options.attributeName],\n };\n } else {\n return defaultIdAttributes;\n }\n },\n },\n },\n },\n ];\n },\n // check initial content for missing ids\n // onCreate() {\n // // Don’t do this when the collaboration extension is active\n // // because this may update the content, so Y.js tries to merge these changes.\n // // This leads to empty block nodes.\n // // See: https://github.com/ueberdosis/tiptap/issues/2400\n // if (\n // this.editor.extensionManager.extensions.find(\n // (extension) => extension.name === \"collaboration\"\n // )\n // ) {\n // return;\n // }\n // const { view, state } = this.editor;\n // const { tr, doc } = state;\n // const { types, attributeName, generateID } = this.options;\n // const nodesWithoutId = findChildren(doc, (node) => {\n // return (\n // types.includes(node.type.name) && node.attrs[attributeName] === null\n // );\n // });\n // nodesWithoutId.forEach(({ node, pos }) => {\n // tr.setNodeMarkup(pos, undefined, {\n // ...node.attrs,\n // [attributeName]: generateID(),\n // });\n // });\n // tr.setMeta(\"addToHistory\", false);\n // view.dispatch(tr);\n // },\n addProseMirrorPlugins() {\n let dragSourceElement: any = null;\n let transformPasted = false;\n return [\n new Plugin({\n key: new PluginKey(\"uniqueID\"),\n appendTransaction: (transactions, oldState, newState) => {\n const docChanges =\n transactions.some((transaction) => transaction.docChanged) &&\n !oldState.doc.eq(newState.doc);\n const filterTransactions =\n this.options.filterTransaction &&\n transactions.some((tr) => !this.options.filterTransaction?.(tr));\n if (!docChanges || filterTransactions) {\n return;\n }\n const { tr } = newState;\n const { types, attributeName, generateID } = this.options;\n const transform = combineTransactionSteps(\n oldState.doc,\n transactions as any,\n );\n const { mapping } = transform;\n // get changed ranges based on the old state\n const changes = getChangedRanges(transform);\n\n changes.forEach(({ newRange }) => {\n const newNodes = findChildrenInRange(\n newState.doc,\n newRange,\n (node) => {\n return types.includes(node.type.name);\n },\n );\n const newIds = newNodes\n .map(({ node }) => node.attrs[attributeName])\n .filter((id) => id !== null);\n const duplicatedNewIds = findDuplicates(newIds);\n\n newNodes.forEach(({ node, pos }) => {\n // instead of checking `node.attrs[attributeName]` directly\n // we look at the current state of the node within `tr.doc`.\n // this helps to prevent adding new ids to the same node\n // if the node changed multiple times within one transaction\n const id = tr.doc.nodeAt(pos)?.attrs[attributeName];\n\n if (id === null) {\n // edge case, when using collaboration, yjs will set the id to null in `_forceRerender`\n // when loading the editor\n // this checks for this case and keeps it at initialBlockId so there will be no change\n const initialDoc = oldState.doc.type.createAndFill()!.content;\n const wasInitial =\n oldState.doc.content.findDiffStart(initialDoc) === null;\n\n if (wasInitial) {\n // the old state was the \"initial content\"\n const jsonNode = JSON.parse(\n JSON.stringify(newState.doc.toJSON()),\n );\n jsonNode.content[0].content[0].attrs.id = \"initialBlockId\";\n // would the new state with the fix also be the \"initial content\"?\n if (\n JSON.stringify(jsonNode.content) ===\n JSON.stringify(initialDoc.toJSON())\n ) {\n // yes, apply the fix\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: \"initialBlockId\",\n });\n return;\n }\n }\n\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID(),\n });\n return;\n }\n // check if the node doesn’t exist in the old state\n const { deleted } = mapping.invert().mapResult(pos);\n const newNode = deleted && duplicatedNewIds.includes(id);\n if (newNode) {\n tr.setNodeMarkup(pos, undefined, {\n ...node.attrs,\n [attributeName]: generateID(),\n });\n }\n });\n });\n if (!tr.steps.length) {\n return;\n }\n // mark the transaction as having been processed by the uniqueID plugin\n tr.setMeta(\"uniqueID\", true);\n return tr;\n },\n // we register a global drag handler to track the current drag source element\n view(view) {\n const handleDragstart = (event: any) => {\n let _a;\n dragSourceElement = (\n (_a = view.dom.parentElement) === null || _a === void 0\n ? void 0\n : _a.contains(event.target)\n )\n ? view.dom.parentElement\n : null;\n };\n window.addEventListener(\"dragstart\", handleDragstart);\n return {\n destroy() {\n window.removeEventListener(\"dragstart\", handleDragstart);\n },\n };\n },\n props: {\n // `handleDOMEvents` is called before `transformPasted` so we can do\n // some checks before. However, `transformPasted` only runs when\n // editor content is pasted - not external content.\n handleDOMEvents: {\n // only create new ids for dropped content while holding `alt`\n // or content is dragged from another editor\n drop: (view, event: any) => {\n let _a;\n if (\n dragSourceElement !== view.dom.parentElement ||\n ((_a = event.dataTransfer) === null || _a === void 0\n ? void 0\n : _a.effectAllowed) === \"copy\"\n ) {\n transformPasted = true;\n } else {\n transformPasted = false;\n }\n\n dragSourceElement = null;\n\n return false;\n },\n // always create new ids on pasted content\n paste: () => {\n transformPasted = true;\n return false;\n },\n },\n // we’ll remove ids for every pasted node\n // so we can create a new one within `appendTransaction`\n transformPasted: (slice) => {\n if (!transformPasted) {\n return slice;\n }\n const { types, attributeName } = this.options;\n const removeId = (fragment: any) => {\n const list: any[] = [];\n fragment.forEach((node: any) => {\n // don’t touch text nodes\n if (node.isText) {\n list.push(node);\n return;\n }\n // check for any other child nodes\n if (!types.includes(node.type.name)) {\n list.push(node.copy(removeId(node.content)));\n return;\n }\n // remove id\n const nodeWithoutId = node.type.create(\n {\n ...node.attrs,\n [attributeName]: null,\n },\n removeId(node.content),\n node.marks,\n );\n list.push(nodeWithoutId);\n });\n return Fragment.from(list);\n };\n // reset check\n transformPasted = false;\n return new Slice(\n removeId(slice.content),\n slice.openStart,\n slice.openEnd,\n );\n },\n },\n }),\n ];\n },\n});\n\nexport { UniqueID as default, UniqueID };\n","import { Node } from \"@tiptap/core\";\nimport { PropSchema, Props } from \"../propTypes.js\";\nimport { StyleSchema, Styles } from \"../styles/types.js\";\nimport { BlockNoteEditor } from \"../../editor/BlockNoteEditor.js\";\nimport { ViewMutationRecord } from \"prosemirror-view\";\n\nexport type CustomInlineContentConfig = {\n type: string;\n content: \"styled\" | \"none\"; // | \"plain\"\n readonly propSchema: PropSchema;\n};\n// InlineContentConfig contains the \"schema\" info about an InlineContent type\n// i.e. what props it supports, what content it supports, etc.\nexport type InlineContentConfig = CustomInlineContentConfig | \"text\" | \"link\";\n\n// InlineContentImplementation contains the \"implementation\" info about an InlineContent element\n// such as the functions / Nodes required to render and / or serialize it\n// @ts-ignore\nexport type InlineContentImplementation<T extends InlineContentConfig> =\n T extends \"link\" | \"text\"\n ? undefined\n : {\n meta?: {\n draggable?: boolean;\n };\n node: Node;\n toExternalHTML?: (\n inlineContent: any,\n editor: BlockNoteEditor<any, any, any>,\n ) =>\n | {\n dom: HTMLElement | DocumentFragment;\n contentDOM?: HTMLElement;\n }\n | undefined;\n render: (\n inlineContent: any,\n updateInlineContent: (update: any) => void,\n editor: BlockNoteEditor<any, any, any>,\n ) => {\n dom: HTMLElement | DocumentFragment;\n contentDOM?: HTMLElement;\n ignoreMutation?: (mutation: ViewMutationRecord) => boolean;\n destroy?: () => void;\n };\n runsBefore?: string[];\n };\n\nexport type InlineContentSchemaWithInlineContent<\n IType extends string,\n C extends InlineContentConfig,\n> = {\n [k in IType]: C;\n};\n\n// Container for both the config and implementation of InlineContent,\n// and the type of `implementation` is based on that of the config\nexport type InlineContentSpec<T extends InlineContentConfig> = {\n config: T;\n implementation: InlineContentImplementation<T>;\n};\n\n// A Schema contains all the types (Configs) supported in an editor\n// The keys are the \"type\" of InlineContent elements\nexport type InlineContentSchema = Record<string, InlineContentConfig>;\n\nexport type InlineContentSpecs = {\n text: { config: \"text\"; implementation: undefined };\n link: { config: \"link\"; implementation: undefined };\n} & Record<string, InlineContentSpec<InlineContentConfig>>;\n\nexport type InlineContentSchemaFromSpecs<T extends InlineContentSpecs> = {\n [K in keyof T]: T[K][\"config\"];\n};\n\nexport type CustomInlineContentFromConfig<\n I extends CustomInlineContentConfig,\n S extends StyleSchema,\n> = {\n type: I[\"type\"];\n props: Props<I[\"propSchema\"]>;\n content: I[\"content\"] extends \"styled\"\n ? StyledText<S>[]\n : I[\"content\"] extends \"plain\"\n ? string\n : I[\"content\"] extends \"none\"\n ? undefined\n : never;\n};\n\nexport type InlineContentFromConfig<\n I extends InlineContentConfig,\n S extends StyleSchema,\n> = I extends \"text\"\n ? StyledText<S>\n : I extends \"link\"\n ? Link<S>\n : I extends CustomInlineContentConfig\n ? CustomInlineContentFromConfig<I, S>\n : never;\n\nexport type PartialCustomInlineContentFromConfig<\n I extends CustomInlineContentConfig,\n S extends StyleSchema,\n> = {\n type: I[\"type\"];\n props?: Props<I[\"propSchema\"]>;\n content?: I[\"content\"] extends \"styled\"\n ? StyledText<S>[] | string\n : I[\"content\"] extends \"plain\"\n ? string\n : I[\"content\"] extends \"none\"\n ? undefined\n : never;\n};\n\nexport type PartialInlineContentFromConfig<\n I extends InlineContentConfig,\n S extends StyleSchema,\n> = I extends \"text\"\n ? string | StyledText<S>\n : I extends \"link\"\n ? PartialLink<S>\n : I extends CustomInlineContentConfig\n ? PartialCustomInlineContentFromConfig<I, S>\n : never;\n\nexport type StyledText<T extends StyleSchema> = {\n type: \"text\";\n text: string;\n styles: Styles<T>;\n};\n\nexport type Link<T extends StyleSchema> = {\n type: \"link\";\n href: string;\n content: StyledText<T>[];\n};\n\nexport type PartialLink<T extends StyleSchema> = Omit<Link<T>, \"content\"> & {\n content: string | Link<T>[\"content\"];\n};\n\nexport type InlineContent<\n I extends InlineContentSchema,\n T extends StyleSchema,\n> = InlineContentFromConfig<I[keyof I], T>;\n\ntype PartialInlineContentElement<\n I extends InlineContentSchema,\n T extends StyleSchema,\n> = PartialInlineContentFromConfig<I[keyof I], T>;\n\nexport type PartialInlineContent<\n I extends InlineContentSchema,\n T extends StyleSchema,\n> = PartialInlineContentElement<I, T>[] | string;\n\nexport function isLinkInlineContent<T extends StyleSchema>(\n content: InlineContent<any, T>,\n): content is Link<T> {\n return content.type === \"link\";\n}\n\nexport function isPartialLinkInlineContent<T extends StyleSchema>(\n content: PartialInlineContentElement<any, T>,\n): content is PartialLink<T> {\n return typeof content !== \"string\" && content.type === \"link\";\n}\n\nexport function isStyledTextInlineContent<T extends StyleSchema>(\n content: PartialInlineContentElement<any, T>,\n): content is StyledText<T> {\n return typeof content !== \"string\" && content.type === \"text\";\n}\n","import type {\n InlineContentSchema,\n StyleSchema,\n PartialInlineContent,\n InlineContent,\n} from \"../schema\";\nimport { PartialTableCell, TableCell } from \"../schema/blocks/types.js\";\n\n/**\n * This will map a table cell to a TableCell object.\n * This is useful for when we want to get the full table cell object from a partial table cell.\n * It is guaranteed to return a new TableCell object.\n */\nexport function mapTableCell<\n T extends InlineContentSchema,\n S extends StyleSchema,\n>(\n content:\n | PartialInlineContent<T, S>\n | PartialTableCell<T, S>\n | TableCell<T, S>,\n): TableCell<T, S> {\n return isTableCell(content)\n ? { ...content }\n : isPartialTableCell(content)\n ? {\n type: \"tableCell\",\n content: ([] as InlineContent<T, S>[]).concat(content.content as any),\n props: {\n backgroundColor: content.props?.backgroundColor ?? \"default\",\n textColor: content.props?.textColor ?? \"default\",\n textAlignment: content.props?.textAlignment ?? \"left\",\n colspan: content.props?.colspan ?? 1,\n rowspan: content.props?.rowspan ?? 1,\n },\n }\n : {\n type: \"tableCell\",\n content: ([] as InlineContent<T, S>[]).concat(content as any),\n props: {\n backgroundColor: \"default\",\n textColor: \"default\",\n textAlignment: \"left\",\n colspan: 1,\n rowspan: 1,\n },\n };\n}\n\nexport function isPartialTableCell<\n T extends InlineContentSchema,\n S extends StyleSchema,\n>(\n content:\n | TableCell<T, S>\n | PartialInlineContent<T, S>\n | PartialTableCell<T, S>\n | undefined\n | null,\n): content is PartialTableCell<T, S> {\n return (\n content !== undefined &&\n content !== null &&\n typeof content !== \"string\" &&\n !Array.isArray(content) &&\n content.type === \"tableCell\"\n );\n}\n\nexport function isTableCell<\n T extends InlineContentSchema,\n S extends StyleSchema,\n>(\n content:\n | TableCell<T, S>\n | PartialInlineContent<T, S>\n | PartialTableCell<T, S>\n | undefined\n | null,\n): content is TableCell<T, S> {\n return (\n isPartialTableCell(content) &&\n content.props !== undefined &&\n content.content !== undefined\n );\n}\n\nexport function getColspan(\n cell:\n | TableCell<any, any>\n | PartialTableCell<any, any>\n | PartialInlineContent<any, any>,\n): number {\n if (isTableCell(cell)) {\n return cell.props.colspan ?? 1;\n }\n return 1;\n}\n\nexport function getRowspan(\n cell:\n | TableCell<any, any>\n | PartialTableCell<any, any>\n | PartialInlineContent<any, any>,\n): number {\n if (isTableCell(cell)) {\n return cell.props.rowspan ?? 1;\n }\n return 1;\n}\n","export class UnreachableCaseError extends Error {\n constructor(val: never) {\n super(`Unreachable case: ${val}`);\n }\n}\n\nexport function assertEmpty(obj: Record<string, never>, throwError = true) {\n const { \"data-test\": dataTest, ...rest } = obj; // exclude data-test\n\n if (Object.keys(rest).length > 0 && throwError) {\n throw new Error(\"Object must be empty \" + JSON.stringify(obj));\n }\n}\n\n// TODO: change for built-in version of typescript 5.4 after upgrade\nexport type NoInfer<T> = [T][T extends any ? 0 : never];\n","import { Node, ResolvedPos } from \"prosemirror-model\";\nimport { EditorState, Transaction } from \"prosemirror-state\";\n\ntype SingleBlockInfo = {\n node: Node;\n beforePos: number;\n afterPos: number;\n};\n\nexport type BlockInfo = {\n /**\n * The outer node that represents a BlockNote block. This is the node that has the ID.\n * Most of the time, this will be a blockContainer node, but it could also be a Column or ColumnList\n */\n bnBlock: SingleBlockInfo;\n /**\n * The type of BlockNote block that this node represents.\n * When dealing with a blockContainer, this is retrieved from the blockContent node, otherwise it's retrieved from the bnBlock node.\n */\n blockNoteType: string;\n} & (\n | {\n // In case we're not dealing with a BlockContainer, we're dealing with a \"wrapper node\" (like a Column or ColumnList), so it will always have children\n\n /**\n * The Prosemirror node that holds block.children. For non-blockContainer, this node will be the same as bnBlock.\n */\n childContainer: SingleBlockInfo;\n isBlockContainer: false;\n }\n | {\n /**\n * The Prosemirror node that holds block.children. For blockContainers, this is the blockGroup node, if it exists.\n */\n childContainer?: SingleBlockInfo;\n /**\n * The Prosemirror node that wraps block.content and has most of the props\n */\n blockContent: SingleBlockInfo;\n /**\n * Whether bnBlock is a blockContainer node\n */\n isBlockContainer: true;\n }\n);\n\n/**\n * Retrieves the position just before the nearest block node in a ProseMirror\n * doc, relative to a position. If the position is within a block node or its\n * descendants, the position just before it is returned. If the position is not\n * within a block node or its descendants, the position just before the next\n * closest block node is returned. If the position is beyond the last block, the\n * position just before the last block is returned.\n * @param doc The ProseMirror doc.\n * @param pos An integer position in the document.\n * @returns The position just before the nearest blockContainer node.\n */\nexport function getNearestBlockPos(doc: Node, pos: number) {\n const $pos = doc.resolve(pos);\n\n // Checks if the position provided is already just before a block node, in\n // which case we return the position.\n if ($pos.nodeAfter && $pos.nodeAfter.type.isInGroup(\"bnBlock\")) {\n return {\n posBeforeNode: $pos.pos,\n node: $pos.nodeAfter,\n };\n }\n\n // Checks the node containing the position and its ancestors until a\n // block node is found and returned.\n let depth = $pos.depth;\n let node = $pos.node(depth);\n while (depth > 0) {\n if (node.type.isInGroup(\"bnBlock\")) {\n return {\n posBeforeNode: $pos.before(depth),\n node: node,\n };\n }\n\n depth--;\n node = $pos.node(depth);\n }\n\n // If the position doesn't lie within a block node, we instead find the\n // position of the next closest one. If the position is beyond the last block,\n // we return the position of the last block. While running `doc.descendants`\n // is expensive, this case should be very rarely triggered. However, it's\n // possible for the position to sometimes be beyond the last block node. This\n // is a problem specifically when using the collaboration plugin.\n const allBlockContainerPositions: number[] = [];\n doc.descendants((node, pos) => {\n if (node.type.isInGroup(\"bnBlock\")) {\n allBlockContainerPositions.push(pos);\n }\n });\n\n // eslint-disable-next-line no-console\n console.warn(`Position ${pos} is not within a blockContainer node.`);\n\n const resolvedPos = doc.resolve(\n allBlockContainerPositions.find((position) => position >= pos) ||\n allBlockContainerPositions[allBlockContainerPositions.length - 1],\n );\n return {\n posBeforeNode: resolvedPos.pos,\n node: resolvedPos.nodeAfter!,\n };\n}\n\n/**\n * Gets information regarding the ProseMirror nodes that make up a block in a\n * BlockNote document. This includes the main `blockContainer` node, the\n * `blockContent` node with the block's main body, and the optional `blockGroup`\n * node which contains the block's children. As well as the nodes, also returns\n * the ProseMirror positions just before & after each node.\n * @param node The main `blockContainer` node that the block information should\n * be retrieved from,\n * @param bnBlockBeforePosOffset the position just before the\n * `blockContainer` node in the document.\n */\nexport function getBlockInfoWithManualOffset(\n node: Node,\n bnBlockBeforePosOffset: number,\n): BlockInfo {\n if (!node.type.isInGroup(\"bnBlock\")) {\n throw new Error(\n `Attempted to get bnBlock node at position but found node of different type ${node.type.name}`,\n );\n }\n\n const bnBlockNode = node;\n const bnBlockBeforePos = bnBlockBeforePosOffset;\n const bnBlockAfterPos = bnBlockBeforePos + bnBlockNode.nodeSize;\n\n const bnBlock: SingleBlockInfo = {\n node: bnBlockNode,\n beforePos: bnBlockBeforePos,\n afterPos: bnBlockAfterPos,\n };\n\n if (bnBlockNode.type.name === \"blockContainer\") {\n let blockContent: SingleBlockInfo | undefined;\n let blockGroup: SingleBlockInfo | undefined;\n\n bnBlockNode.forEach((node, offset) => {\n if (node.type.spec.group === \"blockContent\") {\n // console.log(beforePos, offset);\n const blockContentNode = node;\n const blockContentBeforePos = bnBlockBeforePos + offset + 1;\n const blockContentAfterPos = blockContentBeforePos + node.nodeSize;\n\n blockContent = {\n node: blockContentNode,\n beforePos: blockContentBeforePos,\n afterPos: blockContentAfterPos,\n };\n } else if (node.type.name === \"blockGroup\") {\n const blockGroupNode = node;\n const blockGroupBeforePos = bnBlockBeforePos + offset + 1;\n const blockGroupAfterPos = blockGroupBeforePos + node.nodeSize;\n\n blockGroup = {\n node: blockGroupNode,\n beforePos: blockGroupBeforePos,\n afterPos: blockGroupAfterPos,\n };\n }\n });\n\n if (!blockContent) {\n throw new Error(\n `blockContainer node does not contain a blockContent node in its children: ${bnBlockNode}`,\n );\n }\n\n return {\n isBlockContainer: true,\n bnBlock,\n blockContent,\n childContainer: blockGroup,\n blockNoteType: blockContent.node.type.name,\n };\n } else {\n if (!bnBlock.node.type.isInGroup(\"childContainer\")) {\n throw new Error(\n `bnBlock node is not in the childContainer group: ${bnBlock.node}`,\n );\n }\n\n return {\n isBlockContainer: false,\n bnBlock: bnBlock,\n childContainer: bnBlock,\n blockNoteType: bnBlock.node.type.name,\n };\n }\n}\n\n/**\n * Gets information regarding the ProseMirror nodes that make up a block in a\n * BlockNote document. This includes the main `blockContainer` node, the\n * `blockContent` node with the block's main body, and the optional `blockGroup`\n * node which contains the block's children. As well as the nodes, also returns\n * the ProseMirror positions just before & after each node.\n * @param posInfo An object with the main `blockContainer` node that the block\n * information should be retrieved from, and the position just before it in the\n * document.\n */\nexport function getBlockInfo(posInfo: { posBeforeNode: number; node: Node }) {\n return getBlockInfoWithManualOffset(posInfo.node, posInfo.posBeforeNode);\n}\n\n/**\n * Gets information regarding the ProseMirror nodes that make up a block from a\n * resolved position just before the `blockContainer` node in the document that\n * corresponds to it.\n * @param resolvedPos The resolved position just before the `blockContainer`\n * node.\n */\nexport function getBlockInfoFromResolvedPos(resolvedPos: ResolvedPos) {\n if (!resolvedPos.nodeAfter) {\n throw new Error(\n `Attempted to get blockContainer node at position ${resolvedPos.pos} but a node at this position does not exist`,\n );\n }\n return getBlockInfoWithManualOffset(resolvedPos.nodeAfter, resolvedPos.pos);\n}\n\n/**\n * Gets information regarding the ProseMirror nodes that make up a block. The\n * block chosen is the one currently containing the current ProseMirror\n * selection.\n * @param state The ProseMirror editor state.\n */\nexport function getBlockInfoFromSelection(state: EditorState) {\n const posInfo = getNearestBlockPos(state.doc, state.selection.anchor);\n\n return getBlockInfo(posInfo);\n}\n\n/**\n * Gets information regarding the ProseMirror nodes that make up a block. The\n * block chosen is the one currently containing the current ProseMirror\n * selection.\n * @param tr The ProseMirror transaction.\n */\nexport function getBlockInfoFromTransaction(tr: Transaction) {\n const posInfo = getNearestBlockPos(tr.doc, tr.selection.anchor);\n\n return getBlockInfo(posInfo);\n}\n","import type { Node, Schema } from \"prosemirror-model\";\nimport { Transform } from \"prosemirror-transform\";\nimport type { BlockNoteEditor } from \"../editor/BlockNoteEditor.js\";\nimport { BlockNoteSchema } from \"../blocks/BlockNoteSchema.js\";\nimport type { BlockSchema } from \"../schema/blocks/types.js\";\nimport type { InlineContentSchema } from \"../schema/inlineContent/types.js\";\nimport type { StyleSchema } from \"../schema/styles/types.js\";\n\nexport function getPmSchema(trOrNode: Transform | Node) {\n if (\"doc\" in trOrNode) {\n return trOrNode.doc.type.schema;\n }\n return trOrNode.type.schema;\n}\n\nfunction getBlockNoteEditor<\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(schema: Schema): BlockNoteEditor<BSchema, I, S> {\n return schema.cached.blockNoteEditor as BlockNoteEditor<BSchema, I, S>;\n}\n\nexport function getBlockNoteSchema<\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(schema: Schema): BlockNoteSchema<BSchema, I, S> {\n return getBlockNoteEditor(schema).schema as unknown as BlockNoteSchema<\n BSchema,\n I,\n S\n >;\n}\n\nexport function getBlockSchema<BSchema extends BlockSchema>(\n schema: Schema,\n): BSchema {\n return getBlockNoteSchema(schema).blockSchema as BSchema;\n}\n\nexport function getInlineContentSchema<I extends InlineContentSchema>(\n schema: Schema,\n): I {\n return getBlockNoteSchema(schema).inlineContentSchema as I;\n}\n\nexport function getStyleSchema<S extends StyleSchema>(schema: Schema): S {\n return getBlockNoteSchema(schema).styleSchema as S;\n}\n\nexport function getBlockCache(schema: Schema) {\n return getBlockNoteEditor(schema).blockCache;\n}\n","import { Mark, Node, Schema, Slice } from \"@tiptap/pm/model\";\nimport type { Block } from \"../../blocks/defaultBlocks.js\";\nimport UniqueID from \"../../extensions/tiptap-extensions/UniqueID/UniqueID.js\";\nimport type {\n BlockSchema,\n CustomInlineContentConfig,\n CustomInlineContentFromConfig,\n InlineContent,\n InlineContentFromConfig,\n InlineContentSchema,\n StyleSchema,\n Styles,\n TableCell,\n TableContent,\n} from \"../../schema/index.js\";\nimport {\n isLinkInlineContent,\n isStyledTextInlineContent,\n} from \"../../schema/inlineContent/types.js\";\nimport { UnreachableCaseError } from \"../../util/typescript.js\";\nimport { getBlockInfoWithManualOffset } from \"../getBlockInfoFromPos.js\";\nimport {\n getBlockCache,\n getBlockSchema,\n getInlineContentSchema,\n getPmSchema,\n getStyleSchema,\n} from \"../pmUtil.js\";\n\n/**\n * Converts an internal (prosemirror) table node contentto a BlockNote Tablecontent\n */\nexport function contentNodeToTableContent<\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(contentNode: Node, inlineContentSchema: I, styleSchema: S) {\n const ret: TableContent<I, S> = {\n type: \"tableContent\",\n columnWidths: [],\n headerRows: undefined,\n headerCols: undefined,\n rows: [],\n };\n\n /**\n * A matrix of boolean values indicating whether a cell is a header.\n * The first index is the row index, the second index is the cell index.\n */\n const headerMatrix: boolean[][] = [];\n\n contentNode.content.forEach((rowNode, _offset, rowIndex) => {\n const row: TableContent<I, S>[\"rows\"][0] = {\n cells: [],\n };\n\n if (rowIndex === 0) {\n rowNode.content.forEach((cellNode) => {\n let colWidth = cellNode.attrs.colwidth as null | undefined | number[];\n if (colWidth === undefined || colWidth === null) {\n colWidth = new Array(cellNode.attrs.colspan ?? 1).fill(undefined);\n }\n ret.columnWidths.push(...colWidth);\n });\n }\n\n row.cells = rowNode.content.content.map((cellNode, cellIndex) => {\n if (!headerMatrix[rowIndex]) {\n headerMatrix[rowIndex] = [];\n }\n // Mark the cell as a header if it is a tableHeader node.\n headerMatrix[rowIndex][cellIndex] = cellNode.type.name === \"tableHeader\";\n // Convert cell content to inline content and merge adjacent styled text nodes\n const content = cellNode.content.content\n .map((child) =>\n contentNodeToInlineContent(child, inlineContentSchema, styleSchema),\n )\n // The reason that we merge this content is that we allow table cells to contain multiple tableParagraph nodes\n // So that we can leverage prosemirror-tables native merging\n // If the schema only allowed a single tableParagraph node, then the merging would not work and cause prosemirror to fit the content into a new cell\n .reduce(\n (acc, contentPartial) => {\n if (!acc.length) {\n return contentPartial;\n }\n\n const last = acc[acc.length - 1];\n const first = contentPartial[0];\n\n // Only merge if the last and first content are both styled text nodes and have the same styles\n if (\n first &&\n isStyledTextInlineContent(last) &&\n isStyledTextInlineContent(first) &&\n JSON.stringify(last.styles) === JSON.stringify(first.styles)\n ) {\n // Join them together if they have the same styles\n last.text += \"\\n\" + first.text;\n acc.push(...contentPartial.slice(1));\n return acc;\n }\n acc.push(...contentPartial);\n return acc;\n },\n [] as InlineContent<I, S>[],\n );\n\n return {\n type: \"tableCell\",\n content,\n props: {\n colspan: cellNode.attrs.colspan,\n rowspan: cellNode.attrs.rowspan,\n backgroundColor: cellNode.attrs.backgroundColor,\n textColor: cellNode.attrs.textColor,\n textAlignment: cellNode.attrs.textAlignment,\n },\n } satisfies TableCell<I, S>;\n });\n\n ret.rows.push(row);\n });\n\n for (let i = 0; i < headerMatrix.length; i++) {\n if (headerMatrix[i]?.every((isHeader) => isHeader)) {\n ret.headerRows = (ret.headerRows ?? 0) + 1;\n }\n }\n\n for (let i = 0; i < headerMatrix[0]?.length; i++) {\n if (headerMatrix?.every((row) => row[i])) {\n ret.headerCols = (ret.headerCols ?? 0) + 1;\n }\n }\n\n return ret;\n}\n\n/**\n * Converts an internal (prosemirror) content node to a BlockNote InlineContent array.\n */\nexport function contentNodeToInlineContent<\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(contentNode: Node, inlineContentSchema: I, styleSchema: S) {\n const content: InlineContent<any, S>[] = [];\n let currentContent: InlineContent<any, S> | undefined = undefined;\n\n // Most of the logic below is for handling links because in ProseMirror links are marks\n // while in BlockNote links are a type of inline content\n contentNode.content.forEach((node) => {\n // hardBreak nodes do not have an InlineContent equivalent, instead we\n // add a newline to the previous node.\n if (node.type.name === \"hardBreak\") {\n if (currentContent) {\n // Current content exists.\n if (isStyledTextInlineContent(currentContent)) {\n // Current content is text.\n currentContent.text += \"\\n\";\n } else if (isLinkInlineContent(currentContent)) {\n // Current content is a link.\n currentContent.content[currentContent.content.length - 1].text +=\n \"\\n\";\n } else {\n throw new Error(\"unexpected\");\n }\n } else {\n // Current content does not exist.\n currentContent = {\n type: \"text\",\n text: \"\\n\",\n styles: {},\n };\n }\n\n return;\n }\n\n if (node.type.name !== \"link\" && node.type.name !== \"text\") {\n if (!inlineContentSchema[node.type.name]) {\n // eslint-disable-next-line no-console\n console.warn(\"unrecognized inline content type\", node.type.name);\n return;\n }\n if (currentContent) {\n content.push(currentContent);\n currentContent = undefined;\n }\n\n content.push(\n nodeToCustomInlineContent(node, inlineContentSchema, styleSchema),\n );\n\n return;\n }\n\n const styles: Styles<S> = {};\n let linkMark: Mark | undefined;\n\n for (const mark of node.marks) {\n if (mark.type.name === \"link\") {\n linkMark = mark;\n } else {\n const config = styleSchema[mark.type.name];\n if (!config) {\n if (mark.type.spec.blocknoteIgnore) {\n // at this point, we don't want to show certain marks (such as comments)\n // in the BlockNote JSON output. These marks should be tagged with \"blocknoteIgnore\" in the spec\n continue;\n }\n throw new Error(`style ${mark.type.name} not found in styleSchema`);\n }\n if (config.propSchema === \"boolean\") {\n (styles as any)[config.type] = true;\n } else if (config.propSchema === \"string\") {\n (styles as any)[config.type] = mark.attrs.stringValue;\n } else {\n throw new UnreachableCaseError(config.propSchema);\n }\n }\n }\n\n // Parsing links and text.\n // Current content exists.\n if (currentContent) {\n // Current content is text.\n if (isStyledTextInlineContent(currentContent)) {\n if (!linkMark) {\n // Node is text (same type as current content).\n if (\n JSON.stringify(currentContent.styles) === JSON.stringify(styles)\n ) {\n // Styles are the same.\n currentContent.text += node.textContent;\n } else {\n // Styles are different.\n content.push(currentContent);\n currentContent = {\n type: \"text\",\n text: node.textContent,\n styles,\n };\n }\n } else {\n // Node is a link (different type to current content).\n content.push(currentContent);\n currentContent = {\n type: \"link\",\n href: linkMark.attrs.href,\n content: [\n {\n type: \"text\",\n text: node.textContent,\n styles,\n },\n ],\n };\n }\n } else if (isLinkInlineContent(currentContent)) {\n // Current content is a link.\n if (linkMark) {\n // Node is a link (same type as current content).\n // Link URLs are the same.\n if (currentContent.href === linkMark.attrs.href) {\n // Styles are the same.\n if (\n JSON.stringify(\n currentContent.content[currentContent.content.length - 1]\n .styles,\n ) === JSON.stringify(styles)\n ) {\n currentContent.content[currentContent.content.length - 1].text +=\n node.textContent;\n } else {\n // Styles are different.\n currentContent.content.push({\n type: \"text\",\n text: node.textContent,\n styles,\n });\n }\n } else {\n // Link URLs are different.\n content.push(currentContent);\n currentContent = {\n type: \"link\",\n href: linkMark.attrs.href,\n content: [\n {\n type: \"text\",\n text: node.textContent,\n styles,\n },\n ],\n };\n }\n } else {\n // Node is text (different type to current content).\n content.push(currentContent);\n currentContent = {\n type: \"text\",\n text: node.textContent,\n styles,\n };\n }\n } else {\n // TODO\n }\n }\n // Current content does not exist.\n else {\n // Node is text.\n if (!linkMark) {\n currentContent = {\n type: \"text\",\n text: node.textContent,\n styles,\n };\n }\n // Node is a link.\n else {\n currentContent = {\n type: \"link\",\n href: linkMark.attrs.href,\n content: [\n {\n type: \"text\",\n text: node.textContent,\n styles,\n },\n ],\n };\n }\n }\n });\n\n if (currentContent) {\n content.push(currentContent);\n }\n\n return content as InlineContent<I, S>[];\n}\n\nexport function nodeToCustomInlineContent<\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(node: Node, inlineContentSchema: I, styleSchema: S): InlineContent<I, S> {\n if (node.type.name === \"text\" || node.type.name === \"link\") {\n throw new Error(\"unexpected\");\n }\n const props: any = {};\n const icConfig = inlineContentSchema[\n node.type.name\n ] as CustomInlineContentConfig;\n for (const [attr, value] of Object.entries(node.attrs)) {\n if (!icConfig) {\n throw Error(\"ic node is of an unrecognized type: \" + node.type.name);\n }\n\n const propSchema = icConfig.propSchema;\n\n if (attr in propSchema) {\n props[attr] = value;\n }\n }\n\n let content: CustomInlineContentFromConfig<any, any>[\"content\"];\n\n if (icConfig.content === \"styled\") {\n content = contentNodeToInlineContent(\n node,\n inlineContentSchema,\n styleSchema,\n ) as any; // TODO: is this safe? could we have Links here that are undesired?\n } else {\n content = undefined;\n }\n\n const ic = {\n type: node.type.name,\n props,\n content,\n } as InlineContentFromConfig<I[keyof I], S>;\n return ic;\n}\n\n/**\n * Convert a Prosemirror node to a BlockNote block.\n *\n * TODO: test changes\n */\nexport function nodeToBlock<\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n node: Node,\n schema: Schema,\n blockSchema: BSchema = getBlockSchema(schema) as BSchema,\n inlineContentSchema: I = getInlineContentSchema(schema) as I,\n styleSchema: S = getStyleSchema(schema) as S,\n blockCache = getBlockCache(schema),\n): Block<BSchema, I, S> {\n if (!node.type.isInGroup(\"bnBlock\")) {\n throw Error(\"Node should be a bnBlock, but is instead: \" + node.type.name);\n }\n\n const cachedBlock = blockCache?.get(node);\n\n if (cachedBlock) {\n return cachedBlock;\n }\n\n const blockInfo = getBlockInfoWithManualOffset(node, 0);\n\n let id = blockInfo.bnBlock.node.attrs.id;\n\n // Only used for blocks converted from other formats.\n if (id === null) {\n id = UniqueID.options.generateID();\n }\n\n const blockSpec = blockSchema[blockInfo.blockNoteType];\n\n if (!blockSpec) {\n throw Error(\"Block is of an unrecognized type: \" + blockInfo.blockNoteType);\n }\n\n const props: any = {};\n for (const [attr, value] of Object.entries({\n ...node.attrs,\n ...(blockInfo.isBlockContainer ? blockInfo.blockContent.node.attrs : {}),\n })) {\n const propSchema = blockSpec.propSchema;\n\n if (\n attr in propSchema &&\n !(propSchema[attr].default === undefined && value === undefined)\n ) {\n props[attr] = value;\n }\n }\n\n const blockConfig = blockSchema[blockInfo.blockNoteType];\n\n const children: Block<BSchema, I, S>[] = [];\n blockInfo.childContainer?.node.forEach((child) => {\n children.push(\n nodeToBlock(\n child,\n schema,\n blockSchema,\n inlineContentSchema,\n styleSchema,\n blockCache,\n ),\n );\n });\n\n let content: Block<any, any, any>[\"content\"];\n\n if (blockConfig.content === \"inline\") {\n if (!blockInfo.isBlockContainer) {\n throw new Error(\"impossible\");\n }\n content = contentNodeToInlineContent(\n blockInfo.blockContent.node,\n inlineContentSchema,\n styleSchema,\n );\n } else if (blockConfig.content === \"table\") {\n if (!blockInfo.isBlockContainer) {\n throw new Error(\"impossible\");\n }\n content = contentNodeToTableContent(\n blockInfo.blockContent.node,\n inlineContentSchema,\n styleSchema,\n );\n } else if (blockConfig.content === \"none\") {\n content = undefined;\n } else {\n throw new UnreachableCaseError(blockConfig.content);\n }\n\n const block = {\n id,\n type: blockConfig.type,\n props,\n content,\n children,\n } as Block<BSchema, I, S>;\n\n blockCache?.set(node, block);\n\n return block;\n}\n\n/**\n * Convert a Prosemirror document to a BlockNote document (array of blocks)\n */\nexport function docToBlocks<\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n doc: Node,\n schema: Schema = getPmSchema(doc),\n blockSchema: BSchema = getBlockSchema(schema) as BSchema,\n inlineContentSchema: I = getInlineContentSchema(schema) as I,\n styleSchema: S = getStyleSchema(schema) as S,\n blockCache = getBlockCache(schema),\n) {\n const blocks: Block<BSchema, I, S>[] = [];\n if (doc.firstChild) {\n doc.firstChild.descendants((node) => {\n blocks.push(\n nodeToBlock(\n node,\n schema,\n blockSchema,\n inlineContentSchema,\n styleSchema,\n blockCache,\n ),\n );\n return false;\n });\n }\n return blocks;\n}\n\n/**\n *\n * Parse a Prosemirror Slice into a BlockNote selection. The prosemirror schema looks like this:\n *\n * <blockGroup>\n * <blockContainer> (main content of block)\n * <p, heading, etc.>\n * <blockGroup> (only if blocks has children)\n * <blockContainer> (child block)\n * <p, heading, etc.>\n * </blockContainer>\n * <blockContainer> (child block 2)\n * <p, heading, etc.>\n * </blockContainer>\n * </blockContainer>\n * </blockGroup>\n * </blockGroup>\n *\n */\nexport function prosemirrorSliceToSlicedBlocks<\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n slice: Slice,\n schema: Schema,\n blockSchema: BSchema = getBlockSchema(schema) as BSchema,\n inlineContentSchema: I = getInlineContentSchema(schema) as I,\n styleSchema: S = getStyleSchema(schema) as S,\n blockCache: WeakMap<Node, Block<BSchema, I, S>> = getBlockCache(schema),\n): {\n /**\n * The blocks that are included in the selection.\n */\n blocks: Block<BSchema, I, S>[];\n /**\n * If a block was \"cut\" at the start of the selection, this will be the id of the block that was cut.\n */\n blockCutAtStart: string | undefined;\n /**\n * If a block was \"cut\" at the end of the selection, this will be the id of the block that was cut.\n */\n blockCutAtEnd: string | undefined;\n} {\n // console.log(JSON.stringify(slice.toJSON()));\n function processNode(\n node: Node,\n openStart: number,\n openEnd: number,\n ): {\n blocks: Block<BSchema, I, S>[];\n blockCutAtStart: string | undefined;\n blockCutAtEnd: string | undefined;\n } {\n if (node.type.name !== \"blockGroup\") {\n throw new Error(\"unexpected\");\n }\n const blocks: Block<BSchema, I, S>[] = [];\n let blockCutAtStart: string | undefined;\n let blockCutAtEnd: string | undefined;\n\n node.forEach((blockContainer, _offset, index) => {\n if (blockContainer.type.name !== \"blockContainer\") {\n throw new Error(\"unexpected\");\n }\n if (blockContainer.childCount === 0) {\n return;\n }\n if (blockContainer.childCount === 0 || blockContainer.childCount > 2) {\n throw new Error(\n \"unexpected, blockContainer.childCount: \" + blockContainer.childCount,\n );\n