UNPKG

@blocknote/core

Version:

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

1 lines 913 kB
{"version":3,"file":"blocknote.cjs","sources":["../src/extensions/UniqueID/UniqueID.ts","../src/schema/inlineContent/types.ts","../src/util/table.ts","../src/util/typescript.ts","../src/util/browser.ts","../src/blocks/defaultBlockHelpers.ts","../src/blocks/defaultProps.ts","../src/util/string.ts","../src/schema/blocks/internal.ts","../src/schema/blocks/createSpec.ts","../src/api/getBlockInfoFromPos.ts","../src/api/pmUtil.ts","../src/api/nodeConversions/nodeToBlock.ts","../src/schema/inlineContent/internal.ts","../src/schema/inlineContent/createSpec.ts","../src/schema/styles/internal.ts","../src/schema/styles/createSpec.ts","../src/api/blockManipulation/tables/tables.ts","../src/api/nodeConversions/blockToNode.ts","../src/api/nodeUtil.ts","../src/api/blockManipulation/commands/insertBlocks/insertBlocks.ts","../src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts","../src/api/blockManipulation/commands/updateBlock/updateBlock.ts","../src/api/exporters/html/util/serializeBlocksExternalHTML.ts","../src/api/exporters/html/externalHTMLExporter.ts","../src/api/exporters/html/util/serializeBlocksInternalHTML.ts","../src/api/exporters/html/internalHTMLSerializer.ts","../src/blocks/FileBlockContent/helpers/parse/parseFigureElement.ts","../src/blocks/FileBlockContent/helpers/render/createAddFileButton.ts","../src/blocks/FileBlockContent/helpers/render/createFileNameWithIcon.ts","../src/blocks/FileBlockContent/helpers/render/createFileBlockWrapper.ts","../src/blocks/FileBlockContent/helpers/toExternalHTML/createFigureWithCaption.ts","../src/blocks/FileBlockContent/helpers/toExternalHTML/createLinkWithCaption.ts","../src/blocks/AudioBlockContent/parseAudioElement.ts","../src/blocks/AudioBlockContent/AudioBlockContent.ts","../src/blocks/CodeBlockContent/CodeBlockContent.ts","../src/extensions/BackgroundColor/BackgroundColorMark.ts","../src/extensions/TextColor/TextColorMark.ts","../src/blocks/FileBlockContent/helpers/parse/parseEmbedElement.ts","../src/blocks/FileBlockContent/FileBlockContent.ts","../src/blocks/HeadingBlockContent/HeadingBlockContent.ts","../src/blocks/FileBlockContent/helpers/render/createResizableFileBlockWrapper.ts","../src/blocks/ImageBlockContent/parseImageElement.ts","../src/blocks/ImageBlockContent/ImageBlockContent.ts","../src/blocks/ListItemBlockContent/getListItemContent.ts","../src/api/blockManipulation/commands/splitBlock/splitBlock.ts","../src/blocks/ListItemBlockContent/ListItemKeyboardShortcuts.ts","../src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts","../src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts","../src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListIndexingPlugin.ts","../src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts","../src/blocks/ParagraphBlockContent/ParagraphBlockContent.ts","../src/blocks/QuoteBlockContent/QuoteBlockContent.ts","../src/blocks/TableBlockContent/TableExtension.ts","../src/blocks/TableBlockContent/TableBlockContent.ts","../src/blocks/VideoBlockContent/parseVideoElement.ts","../src/blocks/VideoBlockContent/VideoBlockContent.ts","../src/blocks/defaultBlocks.ts","../src/blocks/defaultBlockTypeGuards.ts","../src/blocks/FileBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts","../src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts","../src/editor/BlockNoteSchema.ts","../src/blocks/PageBreakBlockContent/PageBreakBlockContent.ts","../src/blocks/PageBreakBlockContent/schema.ts","../src/blocks/PageBreakBlockContent/getPageBreakSlashMenuItems.ts","../src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts","../src/api/blockManipulation/commands/nestBlock/nestBlock.ts","../src/api/blockManipulation/getBlock/getBlock.ts","../src/api/blockManipulation/insertContentAt.ts","../src/api/blockManipulation/selections/selection.ts","../src/api/blockManipulation/selections/textCursorPosition.ts","../src/util/esmDependencies.ts","../src/api/exporters/markdown/removeUnderlinesRehypePlugin.ts","../src/api/exporters/markdown/util/addSpacesToCheckboxesRehypePlugin.ts","../src/api/exporters/markdown/markdownExporter.ts","../src/api/parsers/html/util/nestedLists.ts","../src/api/parsers/html/parseHTML.ts","../src/api/parsers/markdown/parseMarkdown.ts","../src/api/clipboard/fromClipboard/acceptedMIMETypes.ts","../src/api/clipboard/fromClipboard/handleFileInsertion.ts","../src/api/clipboard/fromClipboard/fileDropExtension.ts","../src/api/parsers/markdown/detectMarkdown.ts","../src/api/clipboard/fromClipboard/handleVSCodePaste.ts","../src/api/clipboard/fromClipboard/pasteExtension.ts","../src/api/nodeConversions/fragmentToBlocks.ts","../src/api/clipboard/toClipboard/copyExtension.ts","../src/extensions/BackgroundColor/BackgroundColorExtension.ts","../src/util/EventEmitter.ts","../src/editor/BlockNoteExtension.ts","../src/extensions/Collaboration/CursorPlugin.ts","../src/extensions/Collaboration/SyncPlugin.ts","../src/extensions/Collaboration/UndoPlugin.ts","../src/extensions/Comments/CommentMark.ts","../src/extensions/Comments/userstore/UserStore.ts","../src/extensions/Comments/CommentsPlugin.ts","../src/extensions/FilePanel/FilePanelPlugin.ts","../src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts","../src/extensions/HardBreak/HardBreak.ts","../src/api/blockManipulation/commands/mergeBlocks/mergeBlocks.ts","../src/extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts","../src/extensions/LinkToolbar/LinkToolbarPlugin.ts","../src/extensions/LinkToolbar/protocols.ts","../src/extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.ts","../src/extensions/Placeholder/PlaceholderPlugin.ts","../src/extensions/PreviousBlockType/PreviousBlockTypePlugin.ts","../src/extensions/ShowSelection/ShowSelectionPlugin.ts","../src/extensions/getDraggableBlockFromElement.ts","../src/extensions/SideMenu/MultipleNodeSelection.ts","../src/extensions/SideMenu/dragging.ts","../src/extensions/SideMenu/SideMenuPlugin.ts","../src/api/positionMapping.ts","../src/extensions/SuggestionMenu/SuggestionPlugin.ts","../src/extensions/Suggestions/SuggestionMarks.ts","../src/extensions/TableHandles/TableHandlesPlugin.ts","../src/extensions/TextAlignment/TextAlignmentExtension.ts","../src/extensions/TextColor/TextColorExtension.ts","../src/extensions/TrailingNode/TrailingNodeExtension.ts","../src/pm-nodes/BlockContainer.ts","../src/pm-nodes/BlockGroup.ts","../src/pm-nodes/Doc.ts","../src/extensions/Collaboration/ForkYDocPlugin.ts","../src/editor/BlockNoteExtensions.ts","../src/editor/transformPasted.ts","../src/editor/BlockNoteTipTapEditor.ts","../src/editor/BlockNoteEditor.ts","../src/editor/defaultColors.ts","../src/exporter/Exporter.ts","../src/exporter/mapping.ts","../src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts","../src/util/combineByGroup.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 // console.log(\"appendTransaction\");\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) => {\n let _a, _b;\n return !((_b = (_a = this.options).filterTransaction) === null ||\n _b === void 0\n ? void 0\n : _b.call(_a, tr));\n });\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 newNodes.forEach(({ node, pos }) => {\n let _a;\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 =\n (_a = tr.doc.nodeAt(pos)) === null || _a === void 0\n ? void 0\n : _a.attrs[attributeName];\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 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\";\n\nexport type CustomInlineContentConfig = {\n type: string;\n content: \"styled\" | \"none\"; // | \"plain\"\n readonly propSchema: PropSchema;\n // content: \"inline\" | \"none\" | \"table\";\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 node: Node;\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","export const isAppleOS = () =>\n typeof navigator !== \"undefined\" &&\n (/Mac/.test(navigator.platform) ||\n (/AppleWebKit/.test(navigator.userAgent) &&\n /Mobile\\/\\w+/.test(navigator.userAgent)));\n\nexport function formatKeyboardShortcut(shortcut: string, ctrlText = \"Ctrl\") {\n if (isAppleOS()) {\n return shortcut.replace(\"Mod\", \"⌘\");\n } else {\n return shortcut.replace(\"Mod\", ctrlText);\n }\n}\n\nexport function mergeCSSClasses(...classes: (string | false | undefined)[]) {\n return classes.filter((c) => c).join(\" \");\n}\n\nexport const isSafari = () =>\n /^((?!chrome|android).)*safari/i.test(navigator.userAgent);\n","import { blockToNode } from \"../api/nodeConversions/blockToNode.js\";\nimport type { BlockNoteEditor } from \"../editor/BlockNoteEditor.js\";\nimport type {\n BlockNoDefaults,\n BlockSchema,\n InlineContentSchema,\n StyleSchema,\n} from \"../schema/index.js\";\nimport { mergeCSSClasses } from \"../util/browser.js\";\n\n// Function that creates a ProseMirror `DOMOutputSpec` for a default block.\n// Since all default blocks have the same structure (`blockContent` div with a\n// `inlineContent` element inside), this function only needs the block's name\n// for the `data-content-type` attribute of the `blockContent` element and the\n// HTML tag of the `inlineContent` element, as well as any HTML attributes to\n// add to those.\nexport function createDefaultBlockDOMOutputSpec(\n blockName: string,\n htmlTag: string,\n blockContentHTMLAttributes: Record<string, string>,\n inlineContentHTMLAttributes: Record<string, string>,\n) {\n const blockContent = document.createElement(\"div\");\n blockContent.className = mergeCSSClasses(\n \"bn-block-content\",\n blockContentHTMLAttributes.class,\n );\n blockContent.setAttribute(\"data-content-type\", blockName);\n for (const [attribute, value] of Object.entries(blockContentHTMLAttributes)) {\n if (attribute !== \"class\") {\n blockContent.setAttribute(attribute, value);\n }\n }\n\n const inlineContent = document.createElement(htmlTag);\n inlineContent.className = mergeCSSClasses(\n \"bn-inline-content\",\n inlineContentHTMLAttributes.class,\n );\n for (const [attribute, value] of Object.entries(\n inlineContentHTMLAttributes,\n )) {\n if (attribute !== \"class\") {\n inlineContent.setAttribute(attribute, value);\n }\n }\n\n blockContent.appendChild(inlineContent);\n\n return {\n dom: blockContent,\n contentDOM: inlineContent,\n };\n}\n\n// Function used to convert default blocks to HTML. It uses the corresponding\n// node's `renderHTML` method to do the conversion by using a default\n// `DOMSerializer`.\nexport const defaultBlockToHTML = <\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n block: BlockNoDefaults<BSchema, I, S>,\n editor: BlockNoteEditor<BSchema, I, S>,\n): {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n} => {\n let node = blockToNode(block, editor.pmSchema);\n\n if (node.type.name === \"blockContainer\") {\n // for regular blocks, get the toDOM spec from the blockContent node\n node = node.firstChild!;\n }\n\n const toDOM = editor.pmSchema.nodes[node.type.name].spec.toDOM;\n\n if (toDOM === undefined) {\n throw new Error(\n \"This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`.\",\n );\n }\n\n const renderSpec = toDOM(node);\n\n if (typeof renderSpec !== \"object\" || !(\"dom\" in renderSpec)) {\n throw new Error(\n \"Cannot use this block's default HTML serialization as its corresponding TipTap node's `renderHTML` function does not return an object with the `dom` property.\",\n );\n }\n\n return renderSpec as {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n };\n};\n\n// Function that merges all paragraphs into a single one separated by line breaks.\n// This is used when parsing blocks like list items and table cells, as they may\n// contain multiple paragraphs that ProseMirror will not be able to handle\n// properly.\nexport function mergeParagraphs(element: HTMLElement) {\n const paragraphs = element.querySelectorAll(\"p\");\n if (paragraphs.length > 1) {\n const firstParagraph = paragraphs[0];\n for (let i = 1; i < paragraphs.length; i++) {\n const paragraph = paragraphs[i];\n firstParagraph.innerHTML += \"<br>\" + paragraph.innerHTML;\n paragraph.remove();\n }\n }\n}\n","import type { Props, PropSchema } from \"../schema/index.js\";\n\n// TODO: this system should probably be moved / refactored.\n// The dependency from schema on this file doesn't make sense\n\nexport const defaultProps = {\n backgroundColor: {\n default: \"default\" as const,\n },\n textColor: {\n default: \"default\" as const,\n },\n textAlignment: {\n default: \"left\" as const,\n values: [\"left\", \"center\", \"right\", \"justify\"] as const,\n },\n} satisfies PropSchema;\n\nexport type DefaultProps = Props<typeof defaultProps>;\n\n// Default props which are set on `blockContainer` nodes rather than\n// `blockContent` nodes. Ensures that they are not redundantly added to\n// a custom block's TipTap node attributes.\nexport const inheritedProps = [\"backgroundColor\", \"textColor\"];\n","export function camelToDataKebab(str: string): string {\n return \"data-\" + str.replace(/([a-z])([A-Z])/g, \"$1-$2\").toLowerCase();\n}\n\nexport function filenameFromURL(url: string): string {\n const parts = url.split(\"/\");\n if (\n !parts.length || // invalid?\n parts[parts.length - 1] === \"\" // for example, URL ends in a directory-like trailing slash\n ) {\n // in this case just return the original url\n return url;\n }\n return parts[parts.length - 1];\n}\n","import {\n Attribute,\n Attributes,\n Editor,\n Extension,\n Node,\n NodeConfig,\n} from \"@tiptap/core\";\nimport { defaultBlockToHTML } from \"../../blocks/defaultBlockHelpers.js\";\nimport { inheritedProps } from \"../../blocks/defaultProps.js\";\nimport type { BlockNoteEditor } from \"../../editor/BlockNoteEditor.js\";\nimport { mergeCSSClasses } from \"../../util/browser.js\";\nimport { camelToDataKebab } from \"../../util/string.js\";\nimport { InlineContentSchema } from \"../inlineContent/types.js\";\nimport { PropSchema, Props } from \"../propTypes.js\";\nimport { StyleSchema } from \"../styles/types.js\";\nimport {\n BlockConfig,\n BlockSchemaFromSpecs,\n BlockSchemaWithBlock,\n BlockSpec,\n BlockSpecs,\n SpecificBlock,\n TiptapBlockImplementation,\n} from \"./types.js\";\n\n// Function that uses the 'propSchema' of a blockConfig to create a TipTap\n// node's `addAttributes` property.\n// TODO: extract function\nexport function propsToAttributes(propSchema: PropSchema): Attributes {\n const tiptapAttributes: Record<string, Attribute> = {};\n\n Object.entries(propSchema)\n .filter(([name, _spec]) => !inheritedProps.includes(name))\n .forEach(([name, spec]) => {\n tiptapAttributes[name] = {\n default: spec.default,\n keepOnSplit: true,\n // Props are displayed in kebab-case as HTML attributes. If a prop's\n // value is the same as its default, we don't display an HTML\n // attribute for it.\n parseHTML: (element) => {\n const value = element.getAttribute(camelToDataKebab(name));\n\n if (value === null) {\n return null;\n }\n\n if (\n (spec.default === undefined && spec.type === \"boolean\") ||\n (spec.default !== undefined && typeof spec.default === \"boolean\")\n ) {\n if (value === \"true\") {\n return true;\n }\n\n if (value === \"false\") {\n return false;\n }\n\n return null;\n }\n\n if (\n (spec.default === undefined && spec.type === \"number\") ||\n (spec.default !== undefined && typeof spec.default === \"number\")\n ) {\n const asNumber = parseFloat(value);\n const isNumeric =\n !Number.isNaN(asNumber) && Number.isFinite(asNumber);\n\n if (isNumeric) {\n return asNumber;\n }\n\n return null;\n }\n\n return value;\n },\n renderHTML: (attributes) => {\n // don't render to html if the value is the same as the default\n return attributes[name] !== spec.default\n ? {\n [camelToDataKebab(name)]: attributes[name],\n }\n : {};\n },\n };\n });\n\n return tiptapAttributes;\n}\n\n// Used to figure out which block should be rendered. This block is then used to\n// create the node view.\nexport function getBlockFromPos<\n BType extends string,\n Config extends BlockConfig,\n BSchema extends BlockSchemaWithBlock<BType, Config>,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n getPos: (() => number) | boolean,\n editor: BlockNoteEditor<BSchema, I, S>,\n tipTapEditor: Editor,\n type: BType,\n) {\n // Gets position of the node\n if (typeof getPos === \"boolean\") {\n throw new Error(\n \"Cannot find node position as getPos is a boolean, not a function.\",\n );\n }\n const pos = getPos();\n // Gets parent blockContainer node\n const blockContainer = tipTapEditor.state.doc.resolve(pos!).node();\n // Gets block identifier\n const blockIdentifier = blockContainer.attrs.id;\n\n if (!blockIdentifier) {\n throw new Error(\"Block doesn't have id\");\n }\n\n // Gets the block\n const block = editor.getBlock(blockIdentifier)! as SpecificBlock<\n BSchema,\n BType,\n I,\n S\n >;\n if (block.type !== type) {\n throw new Error(\"Block type does not match\");\n }\n\n return block;\n}\n\n// Function that wraps the `dom` element returned from 'blockConfig.render' in a\n// `blockContent` div, which contains the block type and props as HTML\n// attributes. If `blockConfig.render` also returns a `contentDOM`, it also adds\n// an `inlineContent` class to it.\nexport function wrapInBlockStructure<\n BType extends string,\n PSchema extends PropSchema,\n>(\n element: {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n destroy?: () => void;\n },\n blockType: BType,\n blockProps: Props<PSchema>,\n propSchema: PSchema,\n isFileBlock = false,\n domAttributes?: Record<string, string>,\n): {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n destroy?: () => void;\n} {\n // Creates `blockContent` element\n const blockContent = document.createElement(\"div\");\n\n // Adds custom HTML attributes\n if (domAttributes !== undefined) {\n for (const [attr, value] of Object.entries(domAttributes)) {\n if (attr !== \"class\") {\n blockContent.setAttribute(attr, value);\n }\n }\n }\n // Sets blockContent class\n blockContent.className = mergeCSSClasses(\n \"bn-block-content\",\n domAttributes?.class || \"\",\n );\n // Sets content type attribute\n blockContent.setAttribute(\"data-content-type\", blockType);\n // Adds props as HTML attributes in kebab-case with \"data-\" prefix. Skips props\n // which are already added as HTML attributes to the parent `blockContent`\n // element (inheritedProps) and props set to their default values.\n for (const [prop, value] of Object.entries(blockProps)) {\n const spec = propSchema[prop];\n const defaultValue = spec.default;\n if (!inheritedProps.includes(prop) && value !== defaultValue) {\n blockContent.setAttribute(camelToDataKebab(prop), value);\n }\n }\n // Adds file block attribute\n if (isFileBlock) {\n blockContent.setAttribute(\"data-file-block\", \"\");\n }\n\n blockContent.appendChild(element.dom);\n\n if (element.contentDOM !== undefined) {\n element.contentDOM.className = mergeCSSClasses(\n \"bn-inline-content\",\n element.contentDOM.className,\n );\n }\n\n return {\n ...element,\n dom: blockContent,\n };\n}\n\n// Helper type to keep track of the `name` and `content` properties after calling Node.create.\ntype StronglyTypedTipTapNode<\n Name extends string,\n Content extends\n | \"inline*\"\n | \"tableRow+\"\n | \"blockContainer+\"\n | \"column column+\"\n | \"\",\n> = Node & { name: Name; config: { content: Content } };\n\nexport function createStronglyTypedTiptapNode<\n Name extends string,\n Content extends\n | \"inline*\"\n | \"tableRow+\"\n | \"blockContainer+\"\n | \"column column+\"\n | \"\",\n>(config: NodeConfig & { name: Name; content: Content }) {\n return Node.create(config) as StronglyTypedTipTapNode<Name, Content>; // force re-typing (should be safe as it's type-checked from the config)\n}\n\n// This helper function helps to instantiate a blockspec with a\n// config and implementation that conform to the type of Config\nexport function createInternalBlockSpec<T extends BlockConfig>(\n config: T,\n implementation: TiptapBlockImplementation<\n T,\n any,\n InlineContentSchema,\n StyleSchema\n >,\n) {\n return {\n config,\n implementation,\n } satisfies BlockSpec<T, any, InlineContentSchema, StyleSchema>;\n}\n\nexport function createBlockSpecFromStronglyTypedTiptapNode<\n T extends Node,\n P extends PropSchema,\n>(node: T, propSchema: P, requiredExtensions?: Array<Extension | Node>) {\n return createInternalBlockSpec(\n {\n type: node.name as T[\"name\"],\n content: (node.config.content === \"inline*\"\n ? \"inline\"\n : node.config.content === \"tableRow+\"\n ? \"table\"\n : \"none\") as T[\"config\"][\"content\"] extends \"inline*\"\n ? \"inline\"\n : T[\"config\"][\"content\"] extends \"tableRow+\"\n ? \"table\"\n : \"none\",\n propSchema,\n },\n {\n node,\n requiredExtensions,\n toInternalHTML: defaultBlockToHTML,\n toExternalHTML: defaultBlockToHTML,\n // parse: () => undefined, // parse rules are in node already\n },\n );\n}\n\nexport function getBlockSchemaFromSpecs<T extends BlockSpecs>(specs: T) {\n return Object.fromEntries(\n Object.entries(specs).map(([key, value]) => [key, value.config]),\n ) as BlockSchemaFromSpecs<T>;\n}\n","import { Editor } from \"@tiptap/core\";\nimport { TagParseRule } from \"@tiptap/pm/model\";\nimport { NodeView } from \"@tiptap/pm/view\";\nimport type { BlockNoteEditor } from \"../../editor/BlockNoteEditor.js\";\nimport { InlineContentSchema } from \"../inlineContent/types.js\";\nimport { StyleSchema } from \"../styles/types.js\";\nimport {\n createInternalBlockSpec,\n createStronglyTypedTiptapNode,\n getBlockFromPos,\n propsToAttributes,\n wrapInBlockStructure,\n} from \"./internal.js\";\nimport {\n BlockConfig,\n BlockFromConfig,\n BlockSchemaWithBlock,\n PartialBlockFromConfig,\n} from \"./types.js\";\n\n// restrict content to \"inline\" and \"none\" only\nexport type CustomBlockConfig = BlockConfig & {\n content: \"inline\" | \"none\";\n};\n\nexport type CustomBlockImplementation<\n T extends CustomBlockConfig,\n I extends InlineContentSchema,\n S extends StyleSchema,\n> = {\n render: (\n /**\n * The custom block to render\n */\n block: BlockFromConfig<T, I, S>,\n /**\n * The BlockNote editor instance\n * This is typed generically. If you want an editor with your custom schema, you need to\n * cast it manually, e.g.: `const e = editor as BlockNoteEditor<typeof mySchema>;`\n */\n editor: BlockNoteEditor<BlockSchemaWithBlock<T[\"type\"], T>, I, S>,\n // (note) if we want to fix the manual cast, we need to prevent circular references and separate block definition and render implementations\n // or allow manually passing <BSchema>, but that's not possible without passing the other generics because Typescript doesn't support partial inferred generics\n ) => {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n destroy?: () => void;\n };\n // Exports block to external HTML. If not defined, the output will be the same\n // as `render(...).dom`. Used to create clipboard data when pasting outside\n // BlockNote.\n // TODO: Maybe can return undefined to ignore when serializing?\n toExternalHTML?: (\n block: BlockFromConfig<T, I, S>,\n editor: BlockNoteEditor<BlockSchemaWithBlock<T[\"type\"], T>, I, S>,\n ) => {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n };\n\n parse?: (\n el: HTMLElement,\n ) => PartialBlockFromConfig<T, I, S>[\"props\"] | undefined;\n};\n\n// Function that causes events within non-selectable blocks to be handled by the\n// browser instead of the editor.\nexport function applyNonSelectableBlockFix(nodeView: NodeView, editor: Editor) {\n nodeView.stopEvent = (event) => {\n // Blurs the editor on mouse down as the block is non-selectable. This is\n // mainly done to prevent UI elements like the formatting toolbar from being\n // visible while content within a non-selectable block is selected.\n if (event.type === \"mousedown\") {\n setTimeout(() => {\n editor.view.dom.blur();\n }, 10);\n }\n\n return true;\n };\n}\n\n// Function that uses the 'parse' function of a blockConfig to create a\n// TipTap node's `parseHTML` property. This is only used for parsing content\n// from the clipboard.\nexport function getParseRules(\n config: BlockConfig,\n customParseFunction: CustomBlockImplementation<any, any, any>[\"parse\"],\n) {\n const rules: TagParseRule[] = [\n {\n tag: \"[data-content-type=\" + config.type + \"]\",\n contentElement: \".bn-inline-content\",\n },\n ];\n\n if (customParseFunction) {\n rules.push({\n tag: \"*\",\n getAttrs(node: string | HTMLElement) {\n if (typeof node === \"string\") {\n return false;\n }\n\n const props = customParseFunction?.(node);\n\n if (props === undefined) {\n return false;\n }\n\n return props;\n },\n });\n }\n // getContent(node, schema) {\n // const block = blockConfig.parse?.(node as HTMLElement);\n //\n // if (block !== undefined && block.content !== undefined) {\n // return Fragment.from(\n // typeof block.content === \"string\"\n // ? schema.text(block.content)\n // : inlineContentToNodes(block.content, schema)\n // );\n // }\n //\n // return Fragment.empty;\n // },\n // });\n // }\n\n return rules;\n}\n\n// A function to create custom block for API consumers\n// we want to hide the tiptap node from API consumers and provide a simpler API surface instead\nexport function createBlockSpec<\n T extends CustomBlockConfig,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n blockConfig: T,\n blockImplementation: CustomBlockImplementation<NoInfer<T>, I, S>,\n) {\n const node = createStronglyTypedTiptapNode({\n name: blockConfig.type as T[\"type\"],\n content: (blockConfig.content === \"inline\"\n ? \"inline*\"\n : \"\") as T[\"content\"] extends \"inline\" ? \"inline*\" : \"\",\n group: \"blockContent\",\n selectable: blockConfig.isSelectable ?? true,\n isolating: true,\n addAttributes() {\n return propsToAttributes(blockConfig.propSchema);\n },\n\n parseHTML() {\n return getParseRules(blockConfig, blockImplementation.parse);\n },\n\n renderHTML({ HTMLAttributes }) {\n // renderHTML is used for copy/pasting content from the editor back into\n // the editor, so we need to make sure the `blockContent` element is\n // structured correctly as this is what's used for parsing blocks. We\n // just render a placeholder div inside as the `blockContent` element\n // already has all the information needed for proper parsing.\n const div = document.createElement(\"div\");\n return wrapInBlockStructure(\n {\n dom: div,\n contentDOM: blockConfig.content === \"inline\" ? div : undefined,\n },\n blockConfig.type,\n {},\n blockConfig.propSchema,\n blockConfig.isFileBlock,\n HTMLAttributes,\n );\n },\n\n addNodeView() {\n return ({ getPos }) => {\n // Gets the BlockNote editor instance\n const editor = this.options.editor;\n // Gets the block\n const block = getBlockFromPos(\n getPos,\n editor,\n this.editor,\n blockConfig.type,\n );\n // Gets the custom HTML attributes for `blockContent` nodes\n const blockContentDOMAttributes =\n this.options.domAttributes?.blockContent || {};\n\n const output = blockImplementation.render(block as any, editor);\n\n const nodeView: NodeView = wrapInBlockStructure(\n output,\n block.type,\n block.props,\n blockConfig.propSchema,\n blockContentDOMAttributes,\n );\n\n if (blockConfig.isSelectable === false) {\n applyNonSelectableBlockFix(nodeView, this.editor);\n }\n\n return nodeView;\n };\n },\n });\n\n if (node.name !== blockConfig.type) {\n throw new Error(\n \"Node name does not match block type. This is a bug in BlockNote.\",\n );\n }\n\n return createInternalBlockSpec(blockConfig, {\n node,\n toInternalHTML: (block, editor) => {\n const blockContentDOMAttributes =\n node.options.domAttributes?.blockContent || {};\n\n const output = blockImplementation.render(block as any, editor as any);\n\n return wrapInBlockStructure(\n output,\n block.type,\n block.props,\n blockConfig.propSchema,\n blockConfig.isFileBlock,\n blockContentDOMAttributes,\n );\n },\n // TODO: this should not have wrapInBlockStructure and generally be a lot simpler\n // post-processing in externalHTMLExporter should not be necessary\n toExternalHTML: (block, editor) => {\n const blockContentDOMAttributes =\n node.options.domAttributes?.blockContent || {};\n\n let output = blockImplementation.toExternalHTML?.(\n block as any,\n editor as any,\n );\n if (output === undefined) {\n output = blockImplementation.render(block as any, editor as any);\n }\n return wrapInBlockStructure(\n output,\n block.type,\n block.props,\n blockConfig.propSchema,\n blockContentDOMAttributes,\n );\n },\n });\n}\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