UNPKG

@blocknote/server-util

Version:

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

1 lines 14 kB
{"version":3,"file":"blocknote-server-util.cjs","sources":["../src/context/ServerBlockNoteEditor.ts"],"sourcesContent":["import {\n Block,\n BlockNoteEditor,\n BlockNoteEditorOptions,\n BlockSchema,\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema,\n InlineContentSchema,\n PartialBlock,\n StyleSchema,\n blockToNode,\n blocksToMarkdown,\n createExternalHTMLExporter,\n createInternalHTMLSerializer,\n nodeToBlock,\n} from \"@blocknote/core\";\n\nimport { BlockNoteViewRaw } from \"@blocknote/react\";\nimport { Node } from \"@tiptap/pm/model\";\nimport * as jsdom from \"jsdom\";\nimport * as React from \"react\";\nimport { createElement } from \"react\";\nimport { flushSync } from \"react-dom\";\nimport { createRoot } from \"react-dom/client\";\nimport {\n prosemirrorToYDoc,\n prosemirrorToYXmlFragment,\n yXmlFragmentToProseMirrorRootNode,\n} from \"y-prosemirror\";\nimport type * as Y from \"yjs\";\n\n/**\n * Use the ServerBlockNoteEditor to interact with BlockNote documents in a server (nodejs) environment.\n */\nexport class ServerBlockNoteEditor<\n BSchema extends BlockSchema = DefaultBlockSchema,\n ISchema extends InlineContentSchema = DefaultInlineContentSchema,\n SSchema extends StyleSchema = DefaultStyleSchema,\n> {\n /**\n * Internal BlockNoteEditor (not recommended to use directly, use the methods of this class instead)\n */\n public readonly editor: BlockNoteEditor<BSchema, ISchema, SSchema>;\n\n /**\n * We currently use a JSDOM instance to mock document and window methods\n *\n * A possible improvement could be to make this:\n * a) pluggable so other shims can be used as well\n * b) obsolete, but for this all blocks should be React based and we need to remove all references to document / window\n * from the core / react package. (and even then, it's likely some custom blocks would still use document / window methods)\n */\n private jsdom = new jsdom.JSDOM();\n\n /**\n * Calls a function with mocking window and document using JSDOM\n *\n * We could make this obsolete by passing in a document / window object to the render / serialize methods of Blocks\n */\n public async _withJSDOM<T>(fn: () => Promise<T>) {\n const prevWindow = globalThis.window;\n const prevDocument = globalThis.document;\n globalThis.document = this.jsdom.window.document;\n (globalThis as any).window = this.jsdom.window;\n (globalThis as any).window.__TEST_OPTIONS = (\n prevWindow as any\n )?.__TEST_OPTIONS;\n try {\n return await fn();\n } finally {\n globalThis.document = prevDocument;\n globalThis.window = prevWindow;\n }\n }\n\n public static create<\n BSchema extends BlockSchema = DefaultBlockSchema,\n ISchema extends InlineContentSchema = DefaultInlineContentSchema,\n SSchema extends StyleSchema = DefaultStyleSchema,\n >(options: Partial<BlockNoteEditorOptions<BSchema, ISchema, SSchema>> = {}) {\n return new ServerBlockNoteEditor(options) as ServerBlockNoteEditor<\n BSchema,\n ISchema,\n SSchema\n >;\n }\n\n protected constructor(\n options: Partial<BlockNoteEditorOptions<any, any, any>>,\n ) {\n this.editor = BlockNoteEditor.create(options) as any;\n }\n\n /** PROSEMIRROR / BLOCKNOTE conversions */\n\n /**\n * Turn Prosemirror JSON to BlockNote style JSON\n * @param json Prosemirror JSON\n * @returns BlockNote style JSON\n */\n public _prosemirrorNodeToBlocks(pmNode: Node) {\n const blocks: Block<BSchema, InlineContentSchema, StyleSchema>[] = [];\n\n // note, this code is similar to editor.document\n pmNode.firstChild!.descendants((node) => {\n blocks.push(nodeToBlock(node, this.editor.pmSchema));\n\n return false;\n });\n\n return blocks;\n }\n\n /**\n * Turn Prosemirror JSON to BlockNote style JSON\n * @param json Prosemirror JSON\n * @returns BlockNote style JSON\n */\n public _prosemirrorJSONToBlocks(json: any) {\n // note: theoretically this should also be possible without creating prosemirror nodes,\n // but this is definitely the easiest way\n const doc = this.editor.pmSchema.nodeFromJSON(json);\n return this._prosemirrorNodeToBlocks(doc);\n }\n\n /**\n * Turn BlockNote JSON to Prosemirror node / state\n * @param blocks BlockNote blocks\n * @returns Prosemirror root node\n */\n public _blocksToProsemirrorNode(\n blocks: PartialBlock<BSchema, ISchema, SSchema>[],\n ) {\n const pmSchema = this.editor.pmSchema;\n const pmNodes = blocks.map((b) => blockToNode(b, pmSchema));\n\n const doc = pmSchema.topNodeType.create(\n null,\n pmSchema.nodes[\"blockGroup\"].create(null, pmNodes),\n );\n return doc;\n }\n\n /** YJS / BLOCKNOTE conversions */\n\n /**\n * Turn a Y.XmlFragment collaborative doc into a BlockNote document (BlockNote style JSON of all blocks)\n * @returns BlockNote document (BlockNote style JSON of all blocks)\n */\n public yXmlFragmentToBlocks(xmlFragment: Y.XmlFragment) {\n const pmNode = yXmlFragmentToProseMirrorRootNode(\n xmlFragment,\n this.editor.pmSchema,\n );\n return this._prosemirrorNodeToBlocks(pmNode);\n }\n\n /**\n * Convert blocks to a Y.XmlFragment\n *\n * This can be used when importing existing content to Y.Doc for the first time,\n * note that this should not be used to rehydrate a Y.Doc from a database once\n * collaboration has begun as all history will be lost\n *\n * @param blocks the blocks to convert\n * @returns Y.XmlFragment\n */\n public blocksToYXmlFragment(\n blocks: Block<BSchema, ISchema, SSchema>[],\n xmlFragment?: Y.XmlFragment,\n ) {\n return prosemirrorToYXmlFragment(\n this._blocksToProsemirrorNode(blocks),\n xmlFragment,\n );\n }\n\n /**\n * Turn a Y.Doc collaborative doc into a BlockNote document (BlockNote style JSON of all blocks)\n * @returns BlockNote document (BlockNote style JSON of all blocks)\n */\n public yDocToBlocks(ydoc: Y.Doc, xmlFragment = \"prosemirror\") {\n return this.yXmlFragmentToBlocks(ydoc.getXmlFragment(xmlFragment));\n }\n\n /**\n * This can be used when importing existing content to Y.Doc for the first time,\n * note that this should not be used to rehydrate a Y.Doc from a database once\n * collaboration has begun as all history will be lost\n *\n * @param blocks\n */\n public blocksToYDoc(\n blocks: PartialBlock<BSchema, ISchema, SSchema>[],\n xmlFragment = \"prosemirror\",\n ) {\n return prosemirrorToYDoc(\n this._blocksToProsemirrorNode(blocks),\n xmlFragment,\n );\n }\n\n /** HTML / BLOCKNOTE conversions */\n\n /**\n * Exports blocks into a simplified HTML string. To better conform to HTML standards, children of blocks which aren't list\n * items are un-nested in the output HTML.\n *\n * @param blocks An array of blocks that should be serialized into HTML.\n * @returns The blocks, serialized as an HTML string.\n */\n public async blocksToHTMLLossy(\n blocks: PartialBlock<BSchema, ISchema, SSchema>[],\n ): Promise<string> {\n return this._withJSDOM(async () => {\n const exporter = createExternalHTMLExporter(\n this.editor.pmSchema,\n this.editor,\n );\n\n return exporter.exportBlocks(blocks, {\n document: this.jsdom.window.document,\n });\n });\n }\n\n /**\n * Serializes blocks into an HTML string in the format that would normally be rendered by the editor.\n *\n * Use this method if you want to server-side render HTML (for example, a blog post that has been edited in BlockNote)\n * and serve it to users without loading the editor on the client (i.e.: displaying the blog post)\n *\n * @param blocks An array of blocks that should be serialized into HTML.\n * @returns The blocks, serialized as an HTML string.\n */\n public async blocksToFullHTML(\n blocks: PartialBlock<BSchema, ISchema, SSchema>[],\n ): Promise<string> {\n return this._withJSDOM(async () => {\n const exporter = createInternalHTMLSerializer(\n this.editor.pmSchema,\n this.editor,\n );\n\n return exporter.serializeBlocks(blocks, {\n document: this.jsdom.window.document,\n });\n });\n }\n\n /**\n * Parses blocks from an HTML string. Tries to create `Block` objects out of any HTML block-level elements, and\n * `InlineNode` objects from any HTML inline elements, though not all element types are recognized. If BlockNote\n * doesn't recognize an HTML element's tag, it will parse it as a paragraph or plain text.\n * @param html The HTML string to parse blocks from.\n * @returns The blocks parsed from the HTML string.\n */\n public async tryParseHTMLToBlocks(\n html: string,\n ): Promise<Block<BSchema, ISchema, SSchema>[]> {\n return this._withJSDOM(async () => {\n return this.editor.tryParseHTMLToBlocks(html);\n });\n }\n\n /** MARKDOWN / BLOCKNOTE conversions */\n\n /**\n * Serializes blocks into a Markdown string. The output is simplified as Markdown does not support all features of\n * BlockNote - children of blocks which aren't list items are un-nested and certain styles are removed.\n * @param blocks An array of blocks that should be serialized into Markdown.\n * @returns The blocks, serialized as a Markdown string.\n */\n public async blocksToMarkdownLossy(\n blocks: PartialBlock<BSchema, ISchema, SSchema>[],\n ): Promise<string> {\n return this._withJSDOM(async () => {\n return blocksToMarkdown(blocks, this.editor.pmSchema, this.editor, {\n document: this.jsdom.window.document,\n });\n });\n }\n\n /**\n * Creates a list of blocks from a Markdown string. Tries to create `Block` and `InlineNode` objects based on\n * Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it\n * as text.\n * @param markdown The Markdown string to parse blocks from.\n * @returns The blocks parsed from the Markdown string.\n */\n public async tryParseMarkdownToBlocks(\n markdown: string,\n ): Promise<Block<BSchema, ISchema, SSchema>[]> {\n return this._withJSDOM(() => {\n return this.editor.tryParseMarkdownToBlocks(markdown);\n });\n }\n\n /**\n * If you're using React Context in your blocks, you can use this method to wrap editor calls for importing / exporting / block manipulation\n * with the React Context Provider.\n * \n * Example:\n * \n * ```tsx\n const html = await editor.withReactContext(\n ({ children }) => (\n <YourContext.Provider value={true}>{children}</YourContext.Provider>\n ),\n async () => editor.blocksToFullHTML(blocks)\n );\n */\n public async withReactContext<T>(comp: React.FC<any>, fn: () => Promise<T>) {\n return this._withJSDOM(async () => {\n const tmpRoot = createRoot(\n this.jsdom.window.document.createElement(\"div\"),\n );\n\n flushSync(() => {\n tmpRoot.render(\n createElement(\n comp,\n {},\n createElement(BlockNoteViewRaw<any, any, any>, {\n editor: this.editor,\n }),\n ),\n );\n });\n try {\n return await fn();\n } finally {\n tmpRoot.unmount();\n }\n });\n }\n}\n"],"names":["ServerBlockNoteEditor","options","__publicField","jsdom","BlockNoteEditor","fn","prevWindow","prevDocument","pmNode","blocks","node","nodeToBlock","json","doc","pmSchema","pmNodes","b","blockToNode","xmlFragment","yXmlFragmentToProseMirrorRootNode","prosemirrorToYXmlFragment","ydoc","prosemirrorToYDoc","createExternalHTMLExporter","createInternalHTMLSerializer","html","blocksToMarkdown","markdown","comp","tmpRoot","createRoot","flushSync","createElement","BlockNoteViewRaw"],"mappings":"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"}