UNPKG

@blocknote/core

Version:

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

1 lines 416 kB
{"version":3,"file":"BlockNoteSchema-COA0fsXW.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/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/styles/internal.ts","../src/schema/styles/createSpec.ts","../src/util/topo-sort.ts","../src/schema/schema.ts","../src/api/blockManipulation/tables/tables.ts","../src/api/nodeConversions/blockToNode.ts","../src/api/nodeUtil.ts","../src/api/blockManipulation/commands/updateBlock/updateBlock.ts","../src/editor/defaultColors.ts","../src/blocks/defaultProps.ts","../src/blocks/File/helpers/parse/parseFigureElement.ts","../src/blocks/File/helpers/render/createAddFileButton.ts","../src/blocks/File/helpers/render/createFileNameWithIcon.ts","../src/blocks/File/helpers/render/createFileBlockWrapper.ts","../src/blocks/File/helpers/toExternalHTML/createFigureWithCaption.ts","../src/blocks/File/helpers/toExternalHTML/createLinkWithCaption.ts","../src/blocks/Audio/parseAudioElement.ts","../src/blocks/Audio/block.ts","../src/util/EventEmitter.ts","../src/editor/BlockNoteExtension.ts","../src/blocks/Code/shiki.ts","../src/blocks/Code/block.ts","../src/blocks/Divider/block.ts","../src/blocks/File/helpers/parse/parseEmbedElement.ts","../src/blocks/File/block.ts","../src/blocks/ToggleWrapper/createToggleWrapper.ts","../src/blocks/Heading/block.ts","../src/blocks/File/helpers/render/createResizableFileBlockWrapper.ts","../src/blocks/Image/parseImageElement.ts","../src/blocks/Image/block.ts","../src/api/blockManipulation/commands/splitBlock/splitBlock.ts","../src/blocks/utils/listItemEnterHandler.ts","../src/blocks/ListItem/getListItemContent.ts","../src/blocks/ListItem/BulletListItem/block.ts","../src/blocks/ListItem/CheckListItem/block.ts","../src/blocks/ListItem/NumberedListItem/IndexingPlugin.ts","../src/blocks/ListItem/NumberedListItem/block.ts","../src/blocks/ListItem/ToggleListItem/block.ts","../src/blocks/PageBreak/block.ts","../src/blocks/Paragraph/block.ts","../src/blocks/Quote/block.ts","../src/blocks/Table/TableExtension.ts","../src/blocks/Table/block.ts","../src/blocks/Video/parseVideoElement.ts","../src/blocks/Video/block.ts","../src/blocks/File/helpers/uploadToTmpFilesDotOrg_DEV_ONLY.ts","../src/blocks/defaultBlockTypeGuards.ts","../src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts","../src/blocks/PageBreak/getPageBreakSlashMenuItems.ts","../src/blocks/defaultBlocks.ts","../src/blocks/BlockNoteSchema.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 };\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","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 [\n // Converts to & from set to remove duplicates.\n ...new Set(\n classes\n .filter((c) => c)\n // Ensures that if multiple classes are passed as a single string, they\n // are split.\n .join(\" \")\n .split(\" \"),\n ),\n ].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, separator = \"<br>\") {\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 += separator + paragraph.innerHTML;\n paragraph.remove();\n }\n }\n}\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\nexport function isVideoUrl(url: string) {\n const videoExtensions = [\n \"mp4\",\n \"webm\",\n \"ogg\",\n \"mov\",\n \"mkv\",\n \"flv\",\n \"avi\",\n \"wmv\",\n \"m4v\",\n ];\n try {\n const pathname = new URL(url).pathname;\n const ext = pathname.split(\".\").pop()?.toLowerCase() || \"\";\n return videoExtensions.includes(ext);\n } catch (_) {\n return false;\n }\n}\n","import { Attribute, Attributes, Editor, Node } from \"@tiptap/core\";\nimport { defaultBlockToHTML } from \"../../blocks/defaultBlockHelpers.js\";\nimport type { BlockNoteEditor } from \"../../editor/BlockNoteEditor.js\";\nimport { BlockNoteExtension } from \"../../editor/BlockNoteExtension.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 BlockSchemaWithBlock,\n LooseBlockSpec,\n SpecificBlock,\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).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 | undefined,\n editor: BlockNoteEditor<BSchema, I, S>,\n tipTapEditor: Editor,\n type: BType,\n) {\n const pos = getPos();\n // Gets position of the node\n if (pos === undefined) {\n throw new Error(\"Cannot find node position\");\n }\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 | DocumentFragment;\n contentDOM?: HTMLElement;\n destroy?: () => void;\n },\n blockType: BType,\n blockProps: Partial<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 (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) {\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\nexport function createBlockSpecFromTiptapNode<\n const T extends {\n node: Node;\n type: string;\n content: \"inline\" | \"table\" | \"none\";\n },\n P extends PropSchema,\n>(\n config: T,\n propSchema: P,\n extensions?: BlockNoteExtension<any>[],\n): LooseBlockSpec<T[\"type\"], P, T[\"content\"]> {\n return {\n config: {\n type: config.type as T[\"type\"],\n content: config.content,\n propSchema,\n },\n implementation: {\n node: config.node,\n render: defaultBlockToHTML,\n toExternalHTML: defaultBlockToHTML,\n },\n extensions,\n };\n}\n","import { Editor, Node } from \"@tiptap/core\";\nimport { DOMParser, Fragment, TagParseRule } from \"@tiptap/pm/model\";\nimport { NodeView } from \"@tiptap/pm/view\";\nimport { mergeParagraphs } from \"../../blocks/defaultBlockHelpers.js\";\nimport { BlockNoteExtension } from \"../../editor/BlockNoteExtension.js\";\nimport { PropSchema } from \"../propTypes.js\";\nimport {\n getBlockFromPos,\n propsToAttributes,\n wrapInBlockStructure,\n} from \"./internal.js\";\nimport {\n BlockConfig,\n BlockImplementation,\n BlockSpec,\n LooseBlockSpec,\n} from \"./types.js\";\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 TName extends string,\n TProps extends PropSchema,\n TContent extends \"inline\" | \"none\" | \"table\",\n>(\n config: BlockConfig<TName, TProps, TContent>,\n implementation: BlockImplementation<TName, TProps, TContent>,\n) {\n const rules: TagParseRule[] = [\n {\n tag: \"[data-content-type=\" + config.type + \"]\",\n contentElement: \".bn-inline-content\",\n },\n ];\n\n if (implementation.parse) {\n rules.push({\n tag: \"*\",\n getAttrs(node: string | HTMLElement) {\n if (typeof node === \"string\") {\n return false;\n }\n\n const props = implementation.parse?.(node);\n\n if (props === undefined) {\n return false;\n }\n\n return props;\n },\n getContent:\n config.content === \"inline\" || config.content === \"none\"\n ? (node, schema) => {\n if (implementation.parseContent) {\n return implementation.parseContent({\n el: node as HTMLElement,\n schema,\n });\n }\n\n if (config.content === \"inline\") {\n // Parse the inline content if it exists\n const element = node as HTMLElement;\n\n // Clone to avoid modifying the original\n const clone = element.cloneNode(true) as HTMLElement;\n\n // Merge multiple paragraphs into one with line breaks\n mergeParagraphs(\n clone,\n implementation.meta?.code ? \"\\n\" : \"<br>\",\n );\n\n // Parse the content directly as a paragraph to extract inline content\n const parser = DOMParser.fromSchema(schema);\n const parsed = parser.parse(clone, {\n topNode: schema.nodes.paragraph.create(),\n });\n\n return parsed.content;\n }\n return Fragment.empty;\n }\n : undefined,\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 addNodeAndExtensionsToSpec<\n TName extends string,\n TProps extends PropSchema,\n TContent extends \"inline\" | \"none\" | \"table\",\n>(\n blockConfig: BlockConfig<TName, TProps, TContent>,\n blockImplementation: BlockImplementation<TName, TProps, TContent>,\n extensions?: BlockNoteExtension<any>[],\n priority?: number,\n): LooseBlockSpec<TName, TProps, TContent> {\n const node =\n ((blockImplementation as any).node as Node) ||\n Node.create({\n name: blockConfig.type,\n content: (blockConfig.content === \"inline\"\n ? \"inline*\"\n : blockConfig.content === \"none\"\n ? \"\"\n : blockConfig.content) as TContent extends \"inline\" ? \"inline*\" : \"\",\n group: \"blockContent\",\n selectable: blockImplementation.meta?.selectable ?? true,\n isolating: blockImplementation.meta?.isolating ?? true,\n code: blockImplementation.meta?.code ?? false,\n defining: blockImplementation.meta?.defining ?? true,\n priority,\n addAttributes() {\n return propsToAttributes(blockConfig.propSchema);\n },\n\n parseHTML() {\n return getParseRules(blockConfig, blockImplementation);\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 blockImplementation.meta?.fileBlockAccept !== undefined,\n HTMLAttributes,\n );\n },\n\n addNodeView() {\n return (props) => {\n // Gets the BlockNote editor instance\n const editor = this.options.editor;\n // Gets the block\n const block = getBlockFromPos(\n props.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 nodeView = blockImplementation.render.call(\n { blockContentDOMAttributes, props, renderType: \"nodeView\" },\n block as any,\n editor as any,\n );\n\n if (blockImplementation.meta?.selectable === false) {\n applyNonSelectableBlockFix(nodeView, this.editor);\n }\n\n // See explanation for why `update` is not implemented for NodeViews\n // https://github.com/TypeCellOS/BlockNote/pull/1904#discussion_r2313461464\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 {\n config: blockConfig,\n implementation: {\n ...blockImplementation,\n node,\n render(block, editor) {\n const blockContentDOMAttributes =\n node.options.domAttributes?.blockContent || {};\n\n return blockImplementation.render.call(\n {\n blockContentDOMAttributes,\n props: undefined,\n renderType: \"dom\",\n },\n block as any,\n editor as any,\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 return (\n blockImplementation.toExternalHTML?.call(\n { blockContentDOMAttributes },\n block as any,\n editor as any,\n ) ??\n blockImplementation.render.call(\n { blockContentDOMAttributes, renderType: \"dom\", props: undefined },\n block as any,\n editor as any,\n )\n );\n },\n },\n extensions,\n };\n}\n\n/**\n * Helper function to create a block config.\n */\nexport function createBlockConfig<\n TCallback extends (\n options: Partial<Record<string, any>>,\n ) => BlockConfig<any, any, any>,\n TOptions extends Parameters<TCallback>[0],\n TName extends ReturnType<TCallback>[\"type\"],\n TProps extends ReturnType<TCallback>[\"propSchema\"],\n TContent extends ReturnType<TCallback>[\"content\"],\n>(\n callback: TCallback,\n): TOptions extends undefined\n ? () => BlockConfig<TName, TProps, TContent>\n : (options: TOptions) => BlockConfig<TName, TProps, TContent> {\n return callback as any;\n}\n\n/**\n * Helper function to create a block definition.\n * Can accept either functions that return the required objects, or the objects directly.\n */\nexport function createBlockSpec<\n const TName extends string,\n const TProps extends PropSchema,\n const TContent extends \"inline\" | \"none\",\n const TOptions extends Partial<Record<string, any>> | undefined = undefined,\n>(\n blockConfigOrCreator: BlockConfig<TName, TProps, TContent>,\n blockImplementationOrCreator:\n | BlockImplementation<TName, TProps, TContent>\n | (TOptions extends undefined\n ? () => BlockImplementation<TName, TProps, TContent>\n : (\n options: Partial<TOptions>,\n ) => BlockImplementation<TName, TProps, TContent>),\n extensionsOrCreator?:\n | BlockNoteExtension<any>[]\n | (TOptions extends undefined\n ? () => BlockNoteExtension<any>[]\n : (options: Partial<TOptions>) => BlockNoteExtension<any>[]),\n): (options?: Partial<TOptions>) => BlockSpec<TName, TProps, TContent>;\nexport function createBlockSpec<\n const TName extends string,\n const TProps extends PropSchema,\n const TContent extends \"inline\" | \"none\",\n const BlockConf extends BlockConfig<TName, TProps, TContent>,\n const TOptions extends Partial<Record<string, any>>,\n>(\n blockCreator: (options: Partial<TOptions>) => BlockConf,\n blockImplementationOrCreator:\n | BlockImplementation<\n BlockConf[\"type\"],\n BlockConf[\"propSchema\"],\n BlockConf[\"content\"]\n >\n | (TOptions extends undefined\n ? () => BlockImplementation<\n BlockConf[\"type\"],\n BlockConf[\"propSchema\"],\n BlockConf[\"content\"]\n >\n : (\n options: Partial<TOptions>,\n ) => BlockImplementation<\n BlockConf[\"type\"],\n BlockConf[\"propSchema\"],\n BlockConf[\"content\"]\n >),\n extensionsOrCreator?:\n | BlockNoteExtension<any>[]\n | (TOptions extends undefined\n ? () => BlockNoteExtension<any>[]\n : (options: Partial<TOptions>) => BlockNoteExtension<any>[]),\n): (\n options?: Partial<TOptions>,\n) => BlockSpec<\n BlockConf[\"type\"],\n BlockConf[\"propSchema\"],\n BlockConf[\"content\"]\n>;\nexport function createBlockSpec<\n const TName extends string,\n const TProps extends PropSchema,\n const TContent extends \"inline\" | \"none\",\n const TOptions extends Partial<Record<string, any>> | undefined = undefined,\n>(\n blockConfigOrCreator:\n | BlockConfig<TName, TProps, TContent>\n | (TOptions extends undefined\n ? () => BlockConfig<TName, TProps, TContent>\n : (options: Partial<TOptions>) => BlockConfig<TName, TProps, TContent>),\n blockImplementationOrCreator:\n | BlockImplementation<TName, TProps, TContent>\n | (TOptions extends undefined\n ? () => BlockImplementation<TName, TProps, TContent>\n : (\n options: Partial<TOptions>,\n ) => BlockImplementation<TName, TProps, TContent>),\n extensionsOrCreator?:\n | BlockNoteExtension<any>[]\n | (TOptions extends undefined\n ? () => BlockNoteExtension<any>[]\n : (options: Partial<TOptions>) => BlockNoteExtension<any>[]),\n): (options?: Partial<TOptions>) => BlockSpec<TName, TProps, TContent> {\n return (options = {} as TOptions) => {\n const blockConfig =\n typeof blockConfigOrCreator === \"function\"\n ? blockConfigOrCreator(options as any)\n : blockConfigOrCreator;\n\n const blockImplementation =\n typeof blockImplementationOrCreator === \"function\"\n ? blockImplementationOrCreator(options as any)\n : blockImplementationOrCreator;\n\n const extensions = extensionsOrCreator\n ? typeof extensionsOrCreator === \"function\"\n ? extensionsOrCreator(options as any)\n : extensionsOrCreator\n : undefined;\n\n return {\n config: blockConfig,\n implementation: {\n ...blockImplementation,\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 output = blockImplementation.toExternalHTML?.call(\n { blockContentDOMAttributes: this.blockContentDOMAttributes },\n block as any,\n editor as any,\n );\n\n if (output === undefined) {\n return undefined;\n }\n\n return wrapInBlockStructure(\n output,\n block.type,\n block.props,\n blockConfig.propSchema,\n blockImplementation.meta?.fileBlockAccept !== undefined,\n );\n },\n render(block, editor) {\n const output = blockImplementation.render.call(\n {\n blockContentDOMAttributes: this.blockContentDOMAttributes,\n renderType: this.renderType,\n props: this.props as any,\n },\n block as any,\n editor as any,\n );\n\n const nodeView = wrapInBlockStructure(\n output,\n bloc