@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
1 lines • 291 kB
Source Map (JSON)
{"version":3,"file":"defaultBlocks-D1cc0lV9.cjs","sources":["../src/util/browser.ts","../src/blocks/defaultBlockHelpers.ts","../src/util/string.ts","../src/schema/blocks/internal.ts","../src/schema/blocks/createSpec.ts","../src/schema/inlineContent/internal.ts","../src/schema/styles/internal.ts","../src/schema/styles/createSpec.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/extensions/FilePanel/FilePanel.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/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/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/defaultBlockTypeGuards.ts","../src/api/positionMapping.ts","../src/extensions/SuggestionMenu/SuggestionMenu.ts","../src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts","../src/blocks/defaultBlocks.ts"],"sourcesContent":["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 type { ExtensionFactoryInstance } 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?: ExtensionFactoryInstance[],\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 {\n Extension,\n ExtensionFactoryInstance,\n} 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 // Because we do the parsing ourselves, we want to preserve whitespace for content we've parsed\n preserveWhitespace: true,\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 preserveWhitespace: true,\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?: (ExtensionFactoryInstance | Extension)[],\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 | ExtensionFactoryInstance[]\n | (TOptions extends undefined\n ? () => ExtensionFactoryInstance[]\n : (options: Partial<TOptions>) => ExtensionFactoryInstance[]),\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 | ExtensionFactoryInstance[]\n | (TOptions extends undefined\n ? () => ExtensionFactoryInstance[]\n : (options: Partial<TOptions>) => ExtensionFactoryInstance[]),\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 | ExtensionFactoryInstance[]\n | (TOptions extends undefined\n ? () => ExtensionFactoryInstance[]\n : (options: Partial<TOptions>) => ExtensionFactoryInstance[]),\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 block.type,\n block.props,\n blockConfig.propSchema,\n blockImplementation.meta?.fileBlockAccept !== undefined,\n this.blockContentDOMAttributes,\n ) satisfies NodeView;\n\n return nodeView;\n },\n },\n extensions: extensions,\n };\n };\n}\n","import { KeyboardShortcutCommand, Node } from \"@tiptap/core\";\n\nimport { camelToDataKebab } from \"../../util/string.js\";\nimport { PropSchema, Props } from \"../propTypes.js\";\nimport {\n CustomInlineContentConfig,\n InlineContentImplementation,\n InlineContentSchemaFromSpecs,\n InlineContentSpec,\n InlineContentSpecs,\n} from \"./types.js\";\n\n// Function that adds necessary classes and attributes to the `dom` element\n// returned from a custom inline content's 'render' function, to ensure no data\n// is lost on internal copy & paste.\nexport function addInlineContentAttributes<\n IType extends string,\n PSchema extends PropSchema,\n>(\n element: {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n },\n inlineContentType: IType,\n inlineContentProps: Props<PSchema>,\n propSchema: PSchema,\n): {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n} {\n // Sets content type attribute\n element.dom.setAttribute(\"data-inline-content-type\", inlineContentType);\n // Adds props as HTML attributes in kebab-case with \"data-\" prefix. Skips props\n // set to their default values.\n Object.entries(inlineContentProps)\n .filter(([prop, value]) => {\n const spec = propSchema[prop];\n return value !== spec.default;\n })\n .map(([prop, value]) => {\n return [camelToDataKebab(prop), value];\n })\n .forEach(([prop, value]) => element.dom.setAttribute(prop, value));\n\n if (element.contentDOM) {\n element.contentDOM.setAttribute(\"data-editable\", \"\");\n }\n\n return element;\n}\n\n// see https://github.com/TypeCellOS/BlockNote/pull/435\nexport function addInlineContentKeyboardShortcuts<\n T extends CustomInlineContentConfig,\n>(\n config: T,\n): {\n [p: string]: KeyboardShortcutCommand;\n} {\n return {\n Backspace: ({ editor }) => {\n const resolvedPos = editor.state.selection.$from;\n\n return (\n editor.state.selection.empty &&\n resolvedPos.node().type.name === config.type &&\n resolvedPos.parentOffset === 0\n );\n },\n };\n}\n\n// This helper function helps to instantiate a InlineContentSpec with a\n// config and implementation that conform to the type of Config\nexport function createInternalInlineContentSpec<\n const T extends CustomInlineContentConfig,\n>(\n config: T,\n implementation: InlineContentImplementation<NoInfer<T>>,\n): InlineContentSpec<T> {\n return {\n config,\n implementation,\n } as const;\n}\n\nexport function createInlineContentSpecFromTipTapNode<\n T extends Node,\n P extends PropSchema,\n>(\n node: T,\n propSchema: P,\n implementation: Omit<\n InlineContentImplementation<CustomInlineContentConfig>,\n \"node\"\n >,\n) {\n return createInternalInlineContentSpec(\n {\n type: node.name as T[\"name\"],\n propSchema,\n content: node.config.content === \"inline*\" ? \"styled\" : \"none\",\n },\n {\n ...implementation,\n node,\n },\n );\n}\n\nexport function getInlineContentSchemaFromSpecs<T extends InlineContentSpecs>(\n specs: T,\n) {\n return Object.fromEntries(\n Object.entries(specs).map(([key, value]) => [key, value.config]),\n ) as InlineContentSchemaFromSpecs<T>;\n}\n","import { Attributes, Mark } from \"@tiptap/core\";\nimport { DOMSerializer } from \"@tiptap/pm/model\";\nimport {\n StyleConfig,\n StyleImplementation,\n StylePropSchema,\n StyleSchemaFromSpecs,\n StyleSpec,\n StyleSpecs,\n} from \"./types.js\";\n\nexport function stylePropsToAttributes(\n propSchema: StylePropSchema,\n): Attributes {\n if (propSchema === \"boolean\") {\n return {};\n }\n return {\n stringValue: {\n default: undefined,\n keepOnSplit: true,\n parseHTML: (element) => element.getAttribute(\"data-value\"),\n renderHTML: (attributes) =>\n attributes.stringValue !== undefined\n ? {\n \"data-value\": attributes.stringValue,\n }\n : {},\n },\n };\n}\n\n// Function that adds necessary classes and attributes to the `dom` element\n// returned from a custom style's 'render' function, to ensure no data is lost\n// on internal copy & paste.\nexport function addStyleAttributes<\n SType extends string,\n PSchema extends StylePropSchema,\n>(\n element: {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n },\n styleType: SType,\n styleValue: PSchema extends \"boolean\" ? undefined : string,\n propSchema: PSchema,\n): {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n} {\n // Sets content type attribute\n element.dom.setAttribute(\"data-style-type\", styleType);\n // Adds style value as an HTML attribute in kebab-case with \"data-\" prefix, if\n // the style takes a string value.\n if (propSchema === \"string\") {\n element.dom.setAttribute(\"data-value\", styleValue as string);\n }\n\n if (element.contentDOM) {\n element.contentDOM.setAttribute(\"data-editable\", \"\");\n }\n\n return element;\n}\n\n// This helper function helps to instantiate a stylespec with a\n// config and implementation that conform to the type of Config\nexport function createInternalStyleSpec<T extends StyleConfig>(\n config: T,\n implementation: StyleImplementation<T>,\n) {\n return {\n config,\n implementation,\n } satisfies StyleSpec<T>;\n}\n\nexport function createStyleSpecFromTipTapMark<\n T extends Mark,\n P extends StylePropSchema,\n>(mark: T, propSchema: P) {\n return createInternalStyleSpec(\n {\n type: mark.name as T[\"name\"],\n propSchema,\n },\n {\n mark,\n render(value, editor) {\n const toDOM = editor.pmSchema.marks[mark.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 markInstance = editor.pmSchema.mark(mark.name, {\n stringValue: value,\n });\n\n const renderSpec = DOMSerializer.renderSpec(\n document,\n toDOM(markInstance, true),\n );\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 mark'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 toExternalHTML(value, editor) {\n const toDOM = editor.pmSchema.marks[mark.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 markInstance = editor.pmSchema.mark(mark.name, {\n stringValue: value,\n });\n\n const renderSpec = DOMSerializer.renderSpec(\n document,\n toDOM(markInstance, true),\n );\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 mark'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 );\n}\n\nexport function getStyleSchemaFromSpecs<T extends StyleSpecs>(specs: T) {\n return Object.fromEntries(\n Object.entries(specs).map(([key, value]) => [key, value.config]),\n ) as StyleSchemaFromSpecs<T>;\n}\n","import { Mark } from \"@tiptap/core\";\n\nimport { ParseRule, TagParseRule } from \"@tiptap/pm/model\";\nimport {\n addStyleAttributes,\n createInternalStyleSpec,\n stylePropsToAttributes,\n} from \"./internal.js\";\nimport { StyleConfig, StyleSpec } from \"./types.js\";\n\nexport type CustomStyleImplementation<T extends StyleConfig> = {\n render: (value: T[\"propSchema\"] extends \"boolean\" ? undefined : string) => {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n };\n toExternalHTML?: (\n value: T[\"propSchema\"] extends \"boolean\" ? undefined : string,\n ) => {\n dom: HTMLElement;\n contentDOM?: HTMLElement;\n };\n parse?: (\n element: HTMLElement,\n ) => (T[\"propSchema\"] extends \"boolean\" ? true : string) | undefined;\n runsBefore?: string[];\n};\n\nexport function getStyleParseRules<T extends StyleConfig>(\n config: T,\n customParseFunction?: CustomStyleImplementation<T>[\"parse\"],\n): ParseRule[] {\n const rules: TagParseRule[] = [\n {\n tag: `[data-style-type=\"${config.type}\"]`,\n contentElement: (element) => {\n const htmlElement = element as HTMLElement;\n\n if (htmlElement.matches(\"[data-editable]\")) {\n return htmlElement;\n }\n\n return htmlElement.querySelector(\"[data-editable]\") || htmlElement;\n },\n },\n ];\n\n if (customParseFunction) {\n rules.push({\n tag: \"*\",\n // By default, styles can overlap each other, so the rules should not\n // completely consume the element they parse (which can have multiple\n // styles).\n consuming: false,\n getAttrs(node: string | HTMLElement) {\n if (typeof node === \"string\") {\n return false;\n }\n\n const stringValue = customParseFunction?.(node);\n\n if (stringValue === undefined) {\n return false;\n }\n\n return { stringValue };\n },\n });\n }\n return rules;\n}\n\nexport function createStyleSpec<const T extends StyleConfig>(\n styleConfig: T,\n styleImplementation: CustomStyleImplementation<T>,\n): StyleSpec<T> {\n const mark = Mark.create({\n name: styleConfig.type,\n\n addAttributes() {\n return stylePropsToAttributes(styleConfig.propSchema);\n },\n\n parseHTML() {\n return getStyleParseRules(styleConfig, styleImplementation.parse);\n },\n\n renderHTML({ mark }) {\n const renderResult = (\n styleImplementation.toExternalHTML || styleImplementation.render\n )(mark.attrs.stringValue);\n\n return addStyleAttributes(\n renderResult,\n styleConfig.type,\n mark.attrs.stringValue,\n styleConfig.propSchema,\n );\n },\n\n addMarkView() {\n return ({ mark }) => {\n const renderResult = styleImplementation.render(mark.attrs.stringValue);\n\n return addStyleAttributes(\n renderResult,\n styleConfig.type,\n mark.attrs.stringValue,\n styleConfig.propSchema,\n );\n };\n },\n });\n\n return createInternalStyleSpec(styleConfig, {\n ...styleImplementation,\n mark,\n render: (value) => {\n const renderResult = styleImplementation.render(value as any);\n\n return addStyleAttributes(\n renderResult,\n styleConfig.type,\n value,\n styleConfig.propSchema,\n );\n },\n toExternalHTML: (value) => {\n const renderResult = (\n styleImplementation.toExternalHTML || styleImplementation.render\n )(value as any);\n\n return addStyleAttributes(\n renderResult,\n styleConfig.type,\n value,\n styleConfig.propSchema,\n );\n },\n });\n}\n","import type { Node } from \"prosemirror-model\";\n\n/**\n * Get a TipTap node by id\n */\nexport function getNodeById(\n id: string,\n doc: Node,\n): { node: Node; posBeforeNode: number } | undefined {\n let targetNode: Node | undefined = undefined;\n let posBeforeNode: number | undefined = undefined;\n\n doc.firstChild!.descendants((node, pos) => {\n // Skips traversing nodes after node with target ID has been found.\n if (targetNode) {\n return false;\n }\n\n // Keeps traversing nodes if block with target ID has not been found.\n if (!isNodeBlock(node) || node.attrs.id !== id) {\n return true;\n }\n\n targetNode = node;\n posBeforeNode = pos + 1;\n\n return false;\n });\n\n if (targetNode === undefined || posBeforeNode === undefined) {\n return undefined;\n }\n\n return {\n node: targetNode,\n posBeforeNode: posBeforeNode,\n };\n}\n\nexport function isNodeBlock(node: Node): boolean {\n return node.type.isInGroup(\"bnBlock\");\n}\n","import {\n Fragment,\n type NodeType,\n type Node as PMNode,\n Slice,\n} from \"prosemirror-model\";\nimport { TextSelection, Transaction } from \"prosemirror-state\";\nimport { TableMap } from \"prosemirror-tables\";\nimport { ReplaceStep, Transform } from \"prosemirror-transform\";\n\nimport type { Block, PartialBlock } from \"../../../../blocks/defaultBlocks.js\";\nimport type {\n BlockIdentifier,\n BlockSchema,\n} from \"../../../../schema/blocks/types.js\";\nimport type { InlineContentSchema } from \"../../../../schema/inlineContent/types.js\";\nimport type { StyleSchema } from \"../../../../schema/styles/types.js\";\nimport { UnreachableCaseError } from \"../../../../util/typescript.js\";\nimport {\n type BlockInfo,\n getBlockInfoFromResolvedPos,\n} from \"../../../getBlockInfoFromPos.js\";\nimport {\n blockToNode,\n inlineContentToNodes,\n tableContentToNodes,\n} from \"../../../nodeConversions/blockToNode.js\";\nimport { nodeToBlock } from \"../../../nodeConversions/nodeToBlock.js\";\nimport { getNodeById } from \"../../../nodeUtil.js\";\nimport { getPmSchema } from \"../../../pmUtil.js\";\n\n// for compatibility with tiptap. TODO: remove as we want to remove dependency on tiptap command interface\nexport const updateBlockCommand = <\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n posBeforeBlock: number,\n block: PartialBlock<BSchema, I, S>,\n) => {\n return ({\n tr,\n dispatch,\n }: {\n tr: Transaction;\n dispatch?: () => void;\n }): boolean => {\n if (dispatch) {\n updateBlockTr(tr, posBeforeBlock, block);\n }\n return true;\n };\n};\n\nexport function updateBlockTr<\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n tr: Transform | Transaction,\n posBeforeBlock: number,\n block: PartialBlock<BSchema, I, S>,\n replaceFromPos?: number,\n replaceToPos?: number,\n) {\n const blockInfo = getBlockInfoFromResolvedPos(tr.doc.resolve(posBeforeBlock));\n\n let cellAnchor: CellAnchor | null = null;\n if (blockInfo.blockNoteType === \"table\") {\n cellAnchor = captureCellAnchor(tr);\n }\n\n const pmSchema = getPmSchema(tr);\n\n if (\n replaceFromPos !== undefined &&\n replaceToPos !== undefined &&\n replaceFromPos > replaceToPos\n ) {\n throw new Error(\"Invalid replaceFromPos or replaceToPos\");\n }\n\n // Adds blockGroup node with child blocks if necessary.\n\n const oldNodeType = pmSchema.nodes[blockInfo.blockNoteType];\n const newNodeType = pmSchema.nodes[block.type || blockInfo.blockNoteType];\n const newBnBlockNodeType = newNodeType.isInGroup(\"bnBlock\")\n ? newNodeType\n : pmSchema.nodes[\"blockContainer\"];\n\n if (blockInfo.isBlockContainer && newNodeType.isInGroup(\"blockContent\")) {\n const replaceFromOffset =\n replaceFromPos !== undefined &&\n replaceFromPos > blockInfo.blockContent.beforePos &&\n replaceFromPos < blockInfo.blockContent.afterPos\n ? replaceFromPos - blockInfo.blockContent.beforePos - 1\n : undefined;\n\n const replaceToOffset =\n replaceToPos !== undefined &&\n replaceToPos > blockInfo.blockContent.beforePos &&\n replaceToPos < blockInfo.blockContent.afterPos\n ? replaceToPos - blockInfo.blockContent.beforePos - 1\n : undefined;\n\n updateChildren(block, tr, blockInfo);\n // The code below determines the new content of the block.\n // or \"keep\" to keep as-is\n updateBlockContentNode(\n block,\n tr,\n oldNodeType,\n newNodeType,\n blockInfo,\n replaceFromOffset,\n replaceToOffset,\n );\n } else if (!blockInfo.isBlockContainer && newNodeType.isInGroup(\"bnBlock\")) {\n updateChildren(block, tr, blockInfo);\n // old node was a bnBlock type (like column or columnList) and new block as well\n // No op, we just update the bnBlock below (at end of function) and have already updated the children\n } else {\n // switching from blockContainer to non-blockContainer or v.v.\n // currently breaking for column slash menu items converting empty block\n // to column.\n\n // currently, we calculate the new node and replace the entire node with the desired new node.\n // for this, we do a nodeToBlock on the existing block to get the children.\n // it would be cleaner to use a ReplaceAroundStep, but this is a bit simpler and it's quite an edge case\n const existingBlock = nodeToBlock(blockInfo.bnBlock.node, pmSchema);\n tr.replaceWith(\n blockInfo.bnBlock.beforePos,\n blockInfo.bnBlock.afterPos,\n blockToNode(\n {\n children: existingBlock.children, // if no children are passed in, use existing children\n ...block,\n },\n pmSchema,\n ),\n );\n\n return;\n }\n\n // Adds all provided props as attributes to the parent blockContainer node too, and also preserves existing\n // attributes.\n tr.setNodeMarkup(blockInfo.bnBlock.beforePos, newBnBlockNodeType, {\n ...blockInfo.bnBlock.node.attrs,\n ...block.props,\n });\n\n if (cellAnchor) {\n restoreCellAnchor(tr, blockInfo, cellAnchor);\n }\n}\n\nfunction updateBlockContentNode<\n BSchema extends BlockSchema,\n I extends InlineContentSchema,\n S extends StyleSchema,\n>(\n block: PartialBlock<BSchema, I, S>,\n tr: Transform,\n oldNodeType: NodeType,\n newNodeType: NodeType,\n blockInfo: {\n childContainer?:\n | { node: PMNode; beforePos: number; afterPos: number }\n | undefined;\n blockContent: { node: PMNode; beforePos: number; afterPos: number };\n },\n replaceFromOffset?: number,\n replaceToOffset?: number,\n) {\n const pmSchema = getPmSchema(tr);\n let content: PMNode[] | \"keep\" = \"keep\";\n\n // Has there been any custom content provided?\n if (block.content) {\n if (typeof block.content === \"string\") {\n // Adds a single text node with no marks to the content.\n content = inlineContentToNodes(\n [block.content],\n pmSchema,\n newNodeType.name,\n );\n } else if (Array.isArray(block.content)) {\n // Adds a text node with the provided styles converted into marks to the content,\n // for each InlineContent object.\n content = inlineContentToNodes(block.content, pmSchema, newNodeType.name);\n } else if (block.content.type === \"tableContent\") {\n content = tableContentToNodes(block.content, pmSchema);\n } else {\n throw new UnreachableCaseError(block.content.type);\n }\n } else {\n // no custom content has been provided, use existing content IF possible\n // Since some block types contain inline content and others don't,\n // we either need to call setNodeMarkup to just update type &\n // attributes, or replaceWith to replace the whole blockContent.\n if (oldNodeType.spec.content === \"\") {\n // keep old content, because it's empty anyway and should be compatible with\n // any newContentType\n } else if (newNodeType.spec.content !== oldNodeType.spec.content) {\n // the content type changed, replace the previous content\n content = [];\n } else {\n // keep old content, because the content type is the same and should be compatible\n }\n }\n\n // Now, changes the blockContent node type and adds the provided props\n // as attributes. Also preserves all existing attributes that are\n // compatible with the new type.\n //\n // Use either setNodeMarkup or replaceWith depending on whether the\n // content is being replaced or not.\n if (content === \"keep\") {\n // use setNodeMarkup to only update the type and attributes\n tr.setNodeMarkup(blockInfo.blockContent.beforePos, newNodeType, {\n ...blockInfo.blockContent.node.attrs,\n ...block.props,\n });\n } else if (replaceFromOffset !== undefined || replaceToOffset !== undefined) {\n // f