From 4a8b1767084f988282e18c3488fb4d79aef0be4c Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 11 Nov 2025 11:16:03 +0100 Subject: [PATCH 1/5] feat(yjs): expose Y.js BlockNote conversion primitives #1866 --- packages/core/package.json | 5 + .../src/api/nodeConversions/nodeToBlock.ts | 31 +- packages/core/src/yjs/index.ts | 1 + packages/core/src/yjs/utils.test.ts | 1023 +++++++++++++++++ packages/core/src/yjs/utils.ts | 150 +++ packages/core/vite.config.ts | 1 + .../src/context/ServerBlockNoteEditor.test.ts | 22 - .../src/context/ServerBlockNoteEditor.ts | 59 +- packages/server-util/tsconfig.json | 2 +- 9 files changed, 1214 insertions(+), 80 deletions(-) create mode 100644 packages/core/src/yjs/index.ts create mode 100644 packages/core/src/yjs/utils.test.ts create mode 100644 packages/core/src/yjs/utils.ts diff --git a/packages/core/package.json b/packages/core/package.json index 8779f7a6b3..604f08cb0f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -66,6 +66,11 @@ "types": "./types/src/i18n/index.d.ts", "import": "./dist/locales.js", "require": "./dist/locales.cjs" + }, + "./yjs": { + "types": "./types/src/yjs/index.d.ts", + "import": "./dist/yjs.js", + "require": "./dist/yjs.cjs" } }, "scripts": { diff --git a/packages/core/src/api/nodeConversions/nodeToBlock.ts b/packages/core/src/api/nodeConversions/nodeToBlock.ts index 1f5d2c75d4..7ce9d39cf1 100644 --- a/packages/core/src/api/nodeConversions/nodeToBlock.ts +++ b/packages/core/src/api/nodeConversions/nodeToBlock.ts @@ -23,6 +23,7 @@ import { getBlockCache, getBlockSchema, getInlineContentSchema, + getPmSchema, getStyleSchema, } from "../pmUtil.js"; @@ -503,26 +504,28 @@ export function docToBlocks< S extends StyleSchema, >( doc: Node, - schema: Schema, + schema: Schema = getPmSchema(doc), blockSchema: BSchema = getBlockSchema(schema) as BSchema, inlineContentSchema: I = getInlineContentSchema(schema) as I, styleSchema: S = getStyleSchema(schema) as S, blockCache = getBlockCache(schema), ) { const blocks: Block[] = []; - doc.firstChild!.descendants((node) => { - blocks.push( - nodeToBlock( - node, - schema, - blockSchema, - inlineContentSchema, - styleSchema, - blockCache, - ), - ); - return false; - }); + if (doc.firstChild) { + doc.firstChild.descendants((node) => { + blocks.push( + nodeToBlock( + node, + schema, + blockSchema, + inlineContentSchema, + styleSchema, + blockCache, + ), + ); + return false; + }); + } return blocks; } diff --git a/packages/core/src/yjs/index.ts b/packages/core/src/yjs/index.ts new file mode 100644 index 0000000000..05c69e3c01 --- /dev/null +++ b/packages/core/src/yjs/index.ts @@ -0,0 +1 @@ +export * from "./utils.js"; diff --git a/packages/core/src/yjs/utils.test.ts b/packages/core/src/yjs/utils.test.ts new file mode 100644 index 0000000000..d7af3eb1a6 --- /dev/null +++ b/packages/core/src/yjs/utils.test.ts @@ -0,0 +1,1023 @@ +import { Block, docToBlocks } from "../index.js"; +import { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; +import { describe, expect, it } from "vitest"; +import * as Y from "yjs"; +import { + _blocksToProsemirrorNode, + blocksToYDoc, + blocksToYXmlFragment, + yDocToBlocks, + yXmlFragmentToBlocks, +} from "./utils.js"; + +describe("Test yjs utils", () => { + const editor = BlockNoteEditor.create(); + + const testConversion = (testName: string, blocks: Block[]) => { + it(`${testName} - converts to and from prosemirror (doc)`, () => { + const node = _blocksToProsemirrorNode(editor, blocks); + const blockOutput = docToBlocks(node); + expect(blockOutput).toEqual(blocks); + }); + + it(`${testName} - converts to and from yjs (doc)`, () => { + const ydoc = blocksToYDoc(editor, blocks); + const blockOutput = yDocToBlocks(editor, ydoc); + expect(blockOutput).toEqual(blocks); + }); + + it(`${testName} - converts to and from yjs (fragment)`, () => { + const doc = new Y.Doc(); + const fragment = doc.getXmlFragment("test"); + blocksToYXmlFragment(editor, blocks, fragment); + + const blockOutput = yXmlFragmentToBlocks(editor, fragment); + expect(blockOutput).toEqual(blocks); + }); + }; + + describe("Original test case", () => { + const blocks: Block[] = [ + { + id: "1", + type: "heading", + props: { + backgroundColor: "blue", + textColor: "yellow", + textAlignment: "right", + level: 2, + isToggleable: false, + }, + content: [ + { + type: "text", + text: "Heading ", + styles: { + bold: true, + underline: true, + }, + }, + { + type: "text", + text: "2", + styles: { + italic: true, + strike: true, + }, + }, + ], + children: [ + { + id: "2", + type: "paragraph", + props: { + backgroundColor: "red", + textAlignment: "left", + textColor: "default", + }, + content: [ + { + type: "text", + text: "Paragraph", + styles: {}, + }, + ], + children: [], + }, + { + id: "3", + type: "bulletListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "list item", + styles: {}, + }, + ], + children: [], + }, + ], + }, + { + id: "4", + type: "image", + props: { + backgroundColor: "default", + textAlignment: "left", + name: "Example", + url: "exampleURL", + caption: "Caption", + showPreview: true, + previewWidth: 256, + }, + content: undefined, + children: [], + }, + { + id: "5", + type: "image", + props: { + backgroundColor: "default", + textAlignment: "left", + name: "Example", + url: "exampleURL", + caption: "Caption", + showPreview: false, + previewWidth: 256, + }, + content: undefined, + children: [], + }, + ]; + + testConversion("original test case", blocks); + }); + + describe("Empty document", () => { + it("empty document - handles empty array", () => { + const blocks: Block[] = []; + const node = _blocksToProsemirrorNode(editor, blocks); + const blockOutput = docToBlocks(node); + expect(blockOutput).toEqual([]); + }); + + it("empty document - converts to and from yjs (doc)", () => { + const blocks: Block[] = []; + const ydoc = blocksToYDoc(editor, blocks); + const blockOutput = yDocToBlocks(editor, ydoc); + expect(blockOutput).toEqual([]); + }); + + it("empty document - converts to and from yjs (fragment)", () => { + const blocks: Block[] = []; + const doc = new Y.Doc(); + const fragment = doc.getXmlFragment("test"); + blocksToYXmlFragment(editor, blocks, fragment); + + const blockOutput = yXmlFragmentToBlocks(editor, fragment); + expect(blockOutput).toEqual([]); + }); + }); + + describe("Simple paragraphs", () => { + const blocks: Block[] = [ + { + id: "1", + type: "paragraph", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "First paragraph", + styles: {}, + }, + ], + children: [], + }, + { + id: "2", + type: "paragraph", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "center", + }, + content: [ + { + type: "text", + text: "Second paragraph", + styles: {}, + }, + ], + children: [], + }, + ]; + testConversion("simple paragraphs", blocks); + }); + + describe("Deeply nested lists", () => { + const blocks: Block[] = [ + { + id: "1", + type: "bulletListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Level 1", + styles: {}, + }, + ], + children: [ + { + id: "2", + type: "bulletListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Level 2", + styles: {}, + }, + ], + children: [ + { + id: "3", + type: "bulletListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Level 3", + styles: {}, + }, + ], + children: [ + { + id: "4", + type: "bulletListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Level 4", + styles: {}, + }, + ], + children: [], + }, + ], + }, + ], + }, + ], + }, + ]; + testConversion("deeply nested lists", blocks); + }); + + describe("Numbered lists", () => { + const blocks = [ + { + id: "1", + type: "numberedListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "First item", + styles: {}, + }, + ], + children: [], + }, + { + id: "2", + type: "numberedListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Second item", + styles: {}, + }, + ], + children: [ + { + id: "3", + type: "numberedListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Nested item", + styles: {}, + }, + ], + children: [], + }, + ], + }, + ] as unknown as Block[]; + testConversion("numbered lists", blocks); + }); + + describe("Checklists", () => { + const blocks: Block[] = [ + { + id: "1", + type: "checkListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + checked: true, + }, + content: [ + { + type: "text", + text: "Completed task", + styles: {}, + }, + ], + children: [], + }, + { + id: "2", + type: "checkListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + checked: false, + }, + content: [ + { + type: "text", + text: "Pending task", + styles: {}, + }, + ], + children: [ + { + id: "3", + type: "checkListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + checked: false, + }, + content: [ + { + type: "text", + text: "Subtask", + styles: {}, + }, + ], + children: [], + }, + ], + }, + ]; + testConversion("checklists", blocks); + }); + + describe("Toggle lists", () => { + const blocks: Block[] = [ + { + id: "1", + type: "toggleListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Toggle item", + styles: {}, + }, + ], + children: [ + { + id: "2", + type: "paragraph", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Hidden content", + styles: {}, + }, + ], + children: [], + }, + ], + }, + ]; + testConversion("toggle lists", blocks); + }); + + describe("Code blocks", () => { + const blocks: Block[] = [ + { + id: "1", + type: "codeBlock", + props: { + language: "javascript", + }, + content: [ + { + type: "text", + text: 'console.log("Hello, world!");', + styles: {}, + }, + ], + children: [], + }, + { + id: "2", + type: "codeBlock", + props: { + language: "typescript", + }, + content: [ + { + type: "text", + text: "const x: number = 42;", + styles: {}, + }, + ], + children: [], + }, + ]; + testConversion("code blocks", blocks); + }); + + describe("Quotes", () => { + const blocks: Block[] = [ + { + id: "1", + type: "quote", + props: { + backgroundColor: "default", + textColor: "default", + }, + content: [ + { + type: "text", + text: "This is a quote", + styles: { + italic: true, + }, + }, + ], + children: [ + { + id: "2", + type: "paragraph", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Nested in quote", + styles: {}, + }, + ], + children: [], + }, + ], + }, + ]; + testConversion("quotes", blocks); + }); + + describe("Headings with different levels", () => { + const blocks: Block[] = [ + { + id: "1", + type: "heading", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + level: 1, + isToggleable: false, + }, + content: [ + { + type: "text", + text: "Heading 1", + styles: {}, + }, + ], + children: [], + }, + { + id: "2", + type: "heading", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + level: 2, + isToggleable: false, + }, + content: [ + { + type: "text", + text: "Heading 2", + styles: {}, + }, + ], + children: [], + }, + { + id: "3", + type: "heading", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + level: 3, + isToggleable: true, + }, + content: [ + { + type: "text", + text: "Toggle Heading 3", + styles: {}, + }, + ], + children: [ + { + id: "4", + type: "paragraph", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Content under toggle heading", + styles: {}, + }, + ], + children: [], + }, + ], + }, + ]; + testConversion("headings with different levels", blocks); + }); + + describe("Inline styles and links", () => { + const blocks: Block[] = [ + { + id: "1", + type: "paragraph", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Bold ", + styles: { + bold: true, + }, + }, + { + type: "text", + text: "italic ", + styles: { + italic: true, + }, + }, + { + type: "text", + text: "underline ", + styles: { + underline: true, + }, + }, + { + type: "text", + text: "strikethrough ", + styles: { + strike: true, + }, + }, + { + type: "text", + text: "code", + styles: { + code: true, + }, + }, + ], + children: [], + }, + { + id: "2", + type: "paragraph", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "link", + href: "https://example.com", + content: [ + { + type: "text", + text: "Link text", + styles: {}, + }, + ], + }, + ], + children: [], + }, + ]; + testConversion("inline styles and links", blocks); + }); + + describe("Tables", () => { + const blocks = [ + { + id: "1", + type: "table", + props: { + textColor: "default", + }, + content: { + type: "tableContent", + columnWidths: [100, 100, 100], + headerRows: 1, + headerCols: undefined, + rows: [ + { + cells: [ + { + type: "tableCell", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + colspan: 1, + rowspan: 1, + }, + content: [ + { + type: "text", + text: "Header 1", + styles: { + bold: true, + }, + }, + ], + }, + { + type: "tableCell", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + colspan: 1, + rowspan: 1, + }, + content: [ + { + type: "text", + text: "Header 2", + styles: { + bold: true, + }, + }, + ], + }, + { + type: "tableCell", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + colspan: 1, + rowspan: 1, + }, + content: [ + { + type: "text", + text: "Header 3", + styles: { + bold: true, + }, + }, + ], + }, + ], + }, + { + cells: [ + { + type: "tableCell", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + colspan: 1, + rowspan: 1, + }, + content: [ + { + type: "text", + text: "Cell 1", + styles: {}, + }, + ], + }, + { + type: "tableCell", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + colspan: 1, + rowspan: 1, + }, + content: [ + { + type: "text", + text: "Cell 2", + styles: {}, + }, + ], + }, + { + type: "tableCell", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + colspan: 1, + rowspan: 1, + }, + content: [ + { + type: "text", + text: "Cell 3", + styles: {}, + }, + ], + }, + ], + }, + ], + }, + children: [], + }, + ] as unknown as Block[]; + testConversion("tables", blocks); + }); + + describe("Divider", () => { + const blocks = [ + { + id: "1", + type: "paragraph", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Before divider", + styles: {}, + }, + ], + children: [], + }, + { + id: "2", + type: "divider", + props: {}, + content: undefined, + children: [], + }, + { + id: "3", + type: "paragraph", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "After divider", + styles: {}, + }, + ], + children: [], + }, + ] as unknown as Block[]; + testConversion("divider", blocks); + }); + + describe("Complex mixed document", () => { + const blocks: Block[] = [ + { + id: "1", + type: "heading", + props: { + backgroundColor: "blue", + textColor: "yellow", + textAlignment: "center", + level: 1, + isToggleable: false, + }, + content: [ + { + type: "text", + text: "Main Title", + styles: { + bold: true, + }, + }, + ], + children: [], + }, + { + id: "2", + type: "paragraph", + props: { + backgroundColor: "red", + textColor: "default", + textAlignment: "right", + }, + content: [ + { + type: "text", + text: "This is a paragraph with ", + styles: {}, + }, + { + type: "text", + text: "mixed", + styles: { + bold: true, + italic: true, + }, + }, + { + type: "text", + text: " styles and a ", + styles: {}, + }, + { + type: "link", + href: "https://example.com", + content: [ + { + type: "text", + text: "link", + styles: {}, + }, + ], + }, + { + type: "text", + text: ".", + styles: {}, + }, + ], + children: [ + { + id: "3", + type: "bulletListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + }, + content: [ + { + type: "text", + text: "Nested list item", + styles: {}, + }, + ], + children: [], + }, + ], + }, + { + id: "4", + type: "quote", + props: { + backgroundColor: "default", + textColor: "default", + }, + content: [ + { + type: "text", + text: "Important quote", + styles: { + italic: true, + }, + }, + ], + children: [], + }, + { + id: "5", + type: "codeBlock", + props: { + language: "typescript", + }, + content: [ + { + type: "text", + text: "const example = () => {\n return 'code';\n};", + styles: {}, + }, + ], + children: [], + }, + { + id: "6", + type: "checkListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + checked: true, + }, + content: [ + { + type: "text", + text: "Completed checklist item", + styles: {}, + }, + ], + children: [], + }, + { + id: "7", + type: "checkListItem", + props: { + backgroundColor: "default", + textColor: "default", + textAlignment: "left", + checked: false, + }, + content: [ + { + type: "text", + text: "Pending checklist item", + styles: {}, + }, + ], + children: [], + }, + ]; + testConversion("complex mixed document", blocks); + }); +}); diff --git a/packages/core/src/yjs/utils.ts b/packages/core/src/yjs/utils.ts new file mode 100644 index 0000000000..60930a5c9e --- /dev/null +++ b/packages/core/src/yjs/utils.ts @@ -0,0 +1,150 @@ +import { + prosemirrorToYDoc, + prosemirrorToYXmlFragment, + yXmlFragmentToProseMirrorRootNode, +} from "y-prosemirror"; +import * as Y from "yjs"; + +import { + type Block, + type BlockNoteEditor, + type BlockSchema, + type InlineContentSchema, + type PartialBlock, + type StyleSchema, + blockToNode, + docToBlocks, +} from "../index.js"; + +/** + * Turn Prosemirror JSON to BlockNote style JSON + * @param editor BlockNote editor + * @param json Prosemirror JSON + * @returns BlockNote style JSON + */ +export function _prosemirrorJSONToBlocks< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>(editor: BlockNoteEditor, json: any) { + // note: theoretically this should also be possible without creating prosemirror nodes, + // but this is definitely the easiest way + const doc = editor.pmSchema.nodeFromJSON(json); + return docToBlocks(doc); +} + +/** + * Turn BlockNote JSON to Prosemirror node / state + * @param editor BlockNote editor + * @param blocks BlockNote blocks + * @returns Prosemirror root node + */ +export function _blocksToProsemirrorNode< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + blocks: PartialBlock[], +) { + const pmNodes = blocks.map((b) => blockToNode(b, editor.pmSchema)); + + const doc = editor.pmSchema.topNodeType.create( + null, + editor.pmSchema.nodes["blockGroup"].create(null, pmNodes), + ); + return doc; +} + +/** YJS / BLOCKNOTE conversions */ + +/** + * Turn a Y.XmlFragment collaborative doc into a BlockNote document (BlockNote style JSON of all blocks) + * @param editor BlockNote editor + * @param xmlFragment Y.XmlFragment + * @returns BlockNote document (BlockNote style JSON of all blocks) + */ +export function yXmlFragmentToBlocks< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + xmlFragment: Y.XmlFragment, +) { + const pmNode = yXmlFragmentToProseMirrorRootNode( + xmlFragment, + editor.pmSchema, + ); + return docToBlocks(pmNode); +} + +/** + * Convert blocks to a Y.XmlFragment + * + * This can be used when importing existing content to Y.Doc for the first time, + * note that this should not be used to rehydrate a Y.Doc from a database once + * collaboration has begun as all history will be lost + * + * @param editor BlockNote editor + * @param blocks the blocks to convert + * @param xmlFragment XML fragment name + * @returns Y.XmlFragment + */ +export function blocksToYXmlFragment< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + blocks: Block[], + xmlFragment?: Y.XmlFragment, +) { + return prosemirrorToYXmlFragment( + _blocksToProsemirrorNode(editor, blocks), + xmlFragment, + ); +} + +/** + * Turn a Y.Doc collaborative doc into a BlockNote document (BlockNote style JSON of all blocks) + * @param editor BlockNote editor + * @param ydoc Y.Doc + * @param xmlFragment XML fragment name + * @returns BlockNote document (BlockNote style JSON of all blocks) + */ +export function yDocToBlocks< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + ydoc: Y.Doc, + xmlFragment = "prosemirror", +) { + return yXmlFragmentToBlocks(editor, ydoc.getXmlFragment(xmlFragment)); +} + +/** + * This can be used when importing existing content to Y.Doc for the first time, + * note that this should not be used to rehydrate a Y.Doc from a database once + * collaboration has begun as all history will be lost + * + * @param editor BlockNote editor + * @param blocks the blocks to convert + * @param xmlFragment XML fragment name + */ +export function blocksToYDoc< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + blocks: PartialBlock[], + xmlFragment = "prosemirror", +) { + return prosemirrorToYDoc( + _blocksToProsemirrorNode(editor, blocks), + xmlFragment, + ); +} diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts index 66656bfd87..de4bd587d9 100644 --- a/packages/core/vite.config.ts +++ b/packages/core/vite.config.ts @@ -21,6 +21,7 @@ export default defineConfig({ comments: path.resolve(__dirname, "src/comments/index.ts"), blocks: path.resolve(__dirname, "src/blocks/index.ts"), locales: path.resolve(__dirname, "src/i18n/index.ts"), + yjs: path.resolve(__dirname, "src/yjs/index.ts"), }, name: "blocknote", formats: ["es", "cjs"], diff --git a/packages/server-util/src/context/ServerBlockNoteEditor.test.ts b/packages/server-util/src/context/ServerBlockNoteEditor.test.ts index cc6818227e..f5729df9eb 100644 --- a/packages/server-util/src/context/ServerBlockNoteEditor.test.ts +++ b/packages/server-util/src/context/ServerBlockNoteEditor.test.ts @@ -1,6 +1,5 @@ import { Block } from "@blocknote/core"; import { describe, expect, it } from "vitest"; -import * as Y from "yjs"; import { ServerBlockNoteEditor } from "./ServerBlockNoteEditor.js"; describe("Test ServerBlockNoteEditor", () => { @@ -104,27 +103,6 @@ describe("Test ServerBlockNoteEditor", () => { }, ]; - it("converts to and from prosemirror (doc)", async () => { - const node = await editor._blocksToProsemirrorNode(blocks); - const blockOutput = await editor._prosemirrorNodeToBlocks(node); - expect(blockOutput).toEqual(blocks); - }); - - it("converts to and from yjs (doc)", async () => { - const ydoc = await editor.blocksToYDoc(blocks); - const blockOutput = await editor.yDocToBlocks(ydoc); - expect(blockOutput).toEqual(blocks); - }); - - it("converts to and from yjs (fragment)", async () => { - const doc = new Y.Doc(); - const fragment = doc.getXmlFragment("test"); - await editor.blocksToYXmlFragment(blocks, fragment); - - const blockOutput = await editor.yXmlFragmentToBlocks(fragment); - expect(blockOutput).toEqual(blocks); - }); - it("converts to and from HTML (blocksToHTMLLossy)", async () => { const html = await editor.blocksToHTMLLossy(blocks); expect(html).toMatchSnapshot(); diff --git a/packages/server-util/src/context/ServerBlockNoteEditor.ts b/packages/server-util/src/context/ServerBlockNoteEditor.ts index 6757aad98e..dc60c3fc62 100644 --- a/packages/server-util/src/context/ServerBlockNoteEditor.ts +++ b/packages/server-util/src/context/ServerBlockNoteEditor.ts @@ -9,12 +9,19 @@ import { InlineContentSchema, PartialBlock, StyleSchema, - blockToNode, blocksToMarkdown, createExternalHTMLExporter, createInternalHTMLSerializer, - nodeToBlock, } from "@blocknote/core"; +import { + _blocksToProsemirrorNode as blocksToProsemirrorNodeUtil, + _prosemirrorJSONToBlocks as prosemirrorJSONToBlocksUtil, + _prosemirrorNodeToBlocks as prosemirrorNodeToBlocksUtil, + blocksToYDoc as blocksToYDocUtil, + blocksToYXmlFragment as blocksToYXmlFragmentUtil, + yDocToBlocks as yDocToBlocksUtil, + yXmlFragmentToBlocks as yXmlFragmentToBlocksUtil, +} from "@blocknote/core/yjs"; import { BlockNoteViewRaw } from "@blocknote/react"; import { Node } from "@tiptap/pm/model"; @@ -23,11 +30,6 @@ import * as React from "react"; import { createElement } from "react"; import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; -import { - prosemirrorToYDoc, - prosemirrorToYXmlFragment, - yXmlFragmentToProseMirrorRootNode, -} from "y-prosemirror"; import type * as Y from "yjs"; /** @@ -100,16 +102,7 @@ export class ServerBlockNoteEditor< * @returns BlockNote style JSON */ public _prosemirrorNodeToBlocks(pmNode: Node) { - const blocks: Block[] = []; - - // note, this code is similar to editor.document - pmNode.firstChild!.descendants((node) => { - blocks.push(nodeToBlock(node, this.editor.pmSchema)); - - return false; - }); - - return blocks; + return prosemirrorNodeToBlocksUtil(pmNode); } /** @@ -118,10 +111,7 @@ export class ServerBlockNoteEditor< * @returns BlockNote style JSON */ public _prosemirrorJSONToBlocks(json: any) { - // note: theoretically this should also be possible without creating prosemirror nodes, - // but this is definitely the easiest way - const doc = this.editor.pmSchema.nodeFromJSON(json); - return this._prosemirrorNodeToBlocks(doc); + return prosemirrorJSONToBlocksUtil(this.editor, json); } /** @@ -132,14 +122,7 @@ export class ServerBlockNoteEditor< public _blocksToProsemirrorNode( blocks: PartialBlock[], ) { - const pmSchema = this.editor.pmSchema; - const pmNodes = blocks.map((b) => blockToNode(b, pmSchema)); - - const doc = pmSchema.topNodeType.create( - null, - pmSchema.nodes["blockGroup"].create(null, pmNodes), - ); - return doc; + return blocksToProsemirrorNodeUtil(this.editor, blocks); } /** YJS / BLOCKNOTE conversions */ @@ -149,11 +132,7 @@ export class ServerBlockNoteEditor< * @returns BlockNote document (BlockNote style JSON of all blocks) */ public yXmlFragmentToBlocks(xmlFragment: Y.XmlFragment) { - const pmNode = yXmlFragmentToProseMirrorRootNode( - xmlFragment, - this.editor.pmSchema, - ); - return this._prosemirrorNodeToBlocks(pmNode); + return yXmlFragmentToBlocksUtil(this.editor, xmlFragment); } /** @@ -170,10 +149,7 @@ export class ServerBlockNoteEditor< blocks: Block[], xmlFragment?: Y.XmlFragment, ) { - return prosemirrorToYXmlFragment( - this._blocksToProsemirrorNode(blocks), - xmlFragment, - ); + return blocksToYXmlFragmentUtil(this.editor, blocks, xmlFragment); } /** @@ -181,7 +157,7 @@ export class ServerBlockNoteEditor< * @returns BlockNote document (BlockNote style JSON of all blocks) */ public yDocToBlocks(ydoc: Y.Doc, xmlFragment = "prosemirror") { - return this.yXmlFragmentToBlocks(ydoc.getXmlFragment(xmlFragment)); + return yDocToBlocksUtil(this.editor, ydoc, xmlFragment); } /** @@ -195,10 +171,7 @@ export class ServerBlockNoteEditor< blocks: PartialBlock[], xmlFragment = "prosemirror", ) { - return prosemirrorToYDoc( - this._blocksToProsemirrorNode(blocks), - xmlFragment, - ); + return blocksToYDocUtil(this.editor, blocks, xmlFragment); } /** HTML / BLOCKNOTE conversions */ diff --git a/packages/server-util/tsconfig.json b/packages/server-util/tsconfig.json index 2769eb7390..1e4b4e3f65 100644 --- a/packages/server-util/tsconfig.json +++ b/packages/server-util/tsconfig.json @@ -4,7 +4,7 @@ "useDefineForClassFields": true, "module": "ESNext", "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", + "moduleResolution": "Bundler", "jsx": "react-jsx", "strict": true, "sourceMap": true, From 8919d8bb4bcda62a64698c4d3e0f169eaeef607b Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 11 Nov 2025 11:26:47 +0100 Subject: [PATCH 2/5] docs: add docs for yjs utils --- docs/content/docs/reference/editor/meta.json | 2 +- .../docs/reference/editor/overview.mdx | 6 + .../docs/reference/editor/yjs-utilities.mdx | 350 ++++++++++++++++++ 3 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 docs/content/docs/reference/editor/yjs-utilities.mdx diff --git a/docs/content/docs/reference/editor/meta.json b/docs/content/docs/reference/editor/meta.json index bce5349853..a82cdf3ea4 100644 --- a/docs/content/docs/reference/editor/meta.json +++ b/docs/content/docs/reference/editor/meta.json @@ -1,4 +1,4 @@ { "title": "Editor", - "pages": ["overview", "manipulating-content", "cursor-selections", "..."] + "pages": ["overview", "manipulating-content", "cursor-selections", "yjs-utilities", "..."] } diff --git a/docs/content/docs/reference/editor/overview.mdx b/docs/content/docs/reference/editor/overview.mdx index 4cef63a606..5230aeff02 100644 --- a/docs/content/docs/reference/editor/overview.mdx +++ b/docs/content/docs/reference/editor/overview.mdx @@ -118,6 +118,12 @@ The editor can be configured with the following options when using `BlockNoteEdi name="BlockNoteEditorOptions" /> +## YJS Utilities + +BlockNote provides utilities for working with YJS collaborative documents. These utilities allow you to convert between BlockNote blocks and YJS documents programmatically. + +To read more about YJS utilities, see the [YJS Utilities](/docs/reference/editor/yjs-utilities) reference. + ## Related Documentation For more detailed information about specific areas: diff --git a/docs/content/docs/reference/editor/yjs-utilities.mdx b/docs/content/docs/reference/editor/yjs-utilities.mdx new file mode 100644 index 0000000000..64254d34e4 --- /dev/null +++ b/docs/content/docs/reference/editor/yjs-utilities.mdx @@ -0,0 +1,350 @@ +--- +title: YJS Utilities +description: Utilities for converting between BlockNote blocks and YJS collaborative documents +imageTitle: YJS Utilities +--- + +# YJS Utilities + +The `@blocknote/core/yjs` export provides utilities for converting between BlockNote blocks and YJS collaborative documents. These utilities are useful when you need to work with YJS documents outside of the standard collaboration setup, such as importing existing content or working with YJS documents programmatically. + +## Import + +```typescript +import { + blocksToYDoc, + blocksToYXmlFragment, + yDocToBlocks, + yXmlFragmentToBlocks, + _blocksToProsemirrorNode, + _prosemirrorJSONToBlocks, +} from "@blocknote/core/yjs"; +``` + +## Overview + +YJS utilities enable bidirectional conversion between: + +- **BlockNote blocks** ↔ **Y.Doc** (YJS document) +- **BlockNote blocks** ↔ **Y.XmlFragment** (YJS XML fragment) +- **BlockNote blocks** ↔ **Prosemirror nodes** (intermediate format) + +These conversions are essential for: + +- Importing existing BlockNote content into a YJS document for collaboration +- Exporting content from a YJS document back to BlockNote blocks +- Working with YJS documents programmatically without an active editor instance + +## Converting Blocks to YJS Documents + +### `blocksToYDoc` + +Converts BlockNote blocks into a Y.Doc. This is useful when importing existing content to a Y.Doc for the first time. + + + **Important:** This should not be used to rehydrate a Y.Doc from a database once collaboration has begun, as all history will be lost. + + +```typescript +function blocksToYDoc< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + blocks: PartialBlock[], + xmlFragment?: string +): Y.Doc +``` + +**Parameters:** + +- `editor` - The BlockNote editor instance +- `blocks` - Array of blocks to convert +- `xmlFragment` - Optional XML fragment name (defaults to `"prosemirror"`) + +**Returns:** A new Y.Doc containing the converted blocks + +**Example:** + +```typescript +import { BlockNoteEditor } from "@blocknote/core"; +import { blocksToYDoc } from "@blocknote/core/yjs"; +import * as Y from "yjs"; + +const editor = BlockNoteEditor.create(); + +const blocks = [ + { + type: "paragraph", + content: "Hello, world!", + }, + { + type: "heading", + props: { level: 1 }, + content: "My Document", + }, +]; + +// Convert blocks to Y.Doc +const ydoc = blocksToYDoc(editor, blocks); + +// Now you can use this Y.Doc with a YJS provider for collaboration +const provider = new WebrtcProvider("my-room", ydoc); +``` + +### `blocksToYXmlFragment` + +Converts BlockNote blocks into a Y.XmlFragment. This is useful when you want to work with a specific XML fragment within a Y.Doc. + +```typescript +function blocksToYXmlFragment< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + blocks: Block[], + xmlFragment?: Y.XmlFragment +): Y.XmlFragment +``` + +**Parameters:** + +- `editor` - The BlockNote editor instance +- `blocks` - Array of blocks to convert +- `xmlFragment` - Optional existing Y.XmlFragment to populate (creates new one if not provided) + +**Returns:** A Y.XmlFragment containing the converted blocks + +**Example:** + +```typescript +import { BlockNoteEditor } from "@blocknote/core"; +import { blocksToYXmlFragment } from "@blocknote/core/yjs"; +import * as Y from "yjs"; + +const editor = BlockNoteEditor.create(); +const doc = new Y.Doc(); +const fragment = doc.getXmlFragment("my-fragment"); + +const blocks = [ + { + type: "paragraph", + content: "Content for fragment", + }, +]; + +// Convert blocks to the XML fragment +blocksToYXmlFragment(editor, blocks, fragment); +``` + +## Converting YJS Documents to Blocks + +### `yDocToBlocks` + +Converts a Y.Doc back into BlockNote blocks. This is useful for reading content from a YJS document. + +```typescript +function yDocToBlocks< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + ydoc: Y.Doc, + xmlFragment?: string +): Block[] +``` + +**Parameters:** + +- `editor` - The BlockNote editor instance +- `ydoc` - The Y.Doc to convert +- `xmlFragment` - Optional XML fragment name (defaults to `"prosemirror"`) + +**Returns:** Array of BlockNote blocks + +**Example:** + +```typescript +import { BlockNoteEditor } from "@blocknote/core"; +import { yDocToBlocks } from "@blocknote/core/yjs"; +import * as Y from "yjs"; + +const editor = BlockNoteEditor.create(); +const ydoc = new Y.Doc(); + +// ... Y.Doc is populated through collaboration or other means ... + +// Convert Y.Doc back to blocks +const blocks = yDocToBlocks(editor, ydoc); + +console.log(blocks); // Array of BlockNote blocks +``` + +### `yXmlFragmentToBlocks` + +Converts a Y.XmlFragment back into BlockNote blocks. + +```typescript +function yXmlFragmentToBlocks< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + xmlFragment: Y.XmlFragment +): Block[] +``` + +**Parameters:** + +- `editor` - The BlockNote editor instance +- `xmlFragment` - The Y.XmlFragment to convert + +**Returns:** Array of BlockNote blocks + +**Example:** + +```typescript +import { BlockNoteEditor } from "@blocknote/core"; +import { yXmlFragmentToBlocks } from "@blocknote/core/yjs"; +import * as Y from "yjs"; + +const editor = BlockNoteEditor.create(); +const doc = new Y.Doc(); +const fragment = doc.getXmlFragment("my-fragment"); + +// ... Fragment is populated through collaboration or other means ... + +// Convert fragment back to blocks +const blocks = yXmlFragmentToBlocks(editor, fragment); +``` + +## Prosemirror Conversions + +### `_blocksToProsemirrorNode` + +Converts BlockNote blocks to a Prosemirror node. This is an intermediate conversion step used internally. + +```typescript +function _blocksToProsemirrorNode< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + blocks: PartialBlock[] +): Node +``` + +**Parameters:** + +- `editor` - The BlockNote editor instance +- `blocks` - Array of blocks to convert + +**Returns:** Prosemirror root node + +### `_prosemirrorJSONToBlocks` + +Converts Prosemirror JSON to BlockNote blocks. + +```typescript +function _prosemirrorJSONToBlocks< + BSchema extends BlockSchema, + ISchema extends InlineContentSchema, + SSchema extends StyleSchema, +>( + editor: BlockNoteEditor, + json: any +): Block[] +``` + +**Parameters:** + +- `editor` - The BlockNote editor instance +- `json` - Prosemirror JSON object + +**Returns:** Array of BlockNote blocks + +## Complete Example: Importing Existing Content + +Here's a complete example showing how to import existing BlockNote content into a YJS document for collaboration: + +```typescript +import { BlockNoteEditor } from "@blocknote/core"; +import { blocksToYDoc, yDocToBlocks } from "@blocknote/core/yjs"; +import { WebrtcProvider } from "y-webrtc"; +import * as Y from "yjs"; + +// Create editor +const editor = BlockNoteEditor.create(); + +// Existing content you want to import +const existingBlocks = [ + { + type: "heading", + props: { level: 1 }, + content: "Welcome to Collaboration", + }, + { + type: "paragraph", + content: "This content will be synced across all users.", + }, +]; + +// Convert blocks to Y.Doc +const ydoc = blocksToYDoc(editor, existingBlocks); + +// Set up collaboration provider +const provider = new WebrtcProvider("my-room", ydoc); + +// Create editor with collaboration +const collaborativeEditor = BlockNoteEditor.create({ + collaboration: { + provider, + fragment: ydoc.getXmlFragment("prosemirror"), + user: { + name: "User 1", + color: "#ff0000", + }, + }, +}); + +// Later, you can read the content back +const currentBlocks = yDocToBlocks(editor, ydoc); +console.log("Current document:", currentBlocks); +``` + +## Round-trip Conversion + +All conversion functions support round-trip conversion, meaning you can convert blocks → YJS → blocks and get back the same content: + +```typescript +import { BlockNoteEditor } from "@blocknote/core"; +import { blocksToYDoc, yDocToBlocks } from "@blocknote/core/yjs"; + +const editor = BlockNoteEditor.create(); + +const originalBlocks = [ + { + type: "paragraph", + content: "Test content", + }, +]; + +// Convert to Y.Doc and back +const ydoc = blocksToYDoc(editor, originalBlocks); +const convertedBlocks = yDocToBlocks(editor, ydoc); + +// originalBlocks and convertedBlocks are equivalent +console.log(originalBlocks); // Same structure as convertedBlocks +``` + +## Related Documentation + +- [Real-time Collaboration](/docs/features/collaboration) - Learn how to set up collaboration in BlockNote +- [Manipulating Content](/docs/reference/editor/manipulating-content) - Working with blocks and inline content +- [Server Utilities](/docs/reference/server-util) - Server-side utilities for BlockNote (uses these YJS utilities internally) + From b9bb32919288321842113bbc069a5289266391ae Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 11 Nov 2025 11:34:31 +0100 Subject: [PATCH 3/5] chore: fix import --- packages/server-util/src/context/ServerBlockNoteEditor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server-util/src/context/ServerBlockNoteEditor.ts b/packages/server-util/src/context/ServerBlockNoteEditor.ts index dc60c3fc62..3068bf2f39 100644 --- a/packages/server-util/src/context/ServerBlockNoteEditor.ts +++ b/packages/server-util/src/context/ServerBlockNoteEditor.ts @@ -12,11 +12,11 @@ import { blocksToMarkdown, createExternalHTMLExporter, createInternalHTMLSerializer, + docToBlocks, } from "@blocknote/core"; import { _blocksToProsemirrorNode as blocksToProsemirrorNodeUtil, _prosemirrorJSONToBlocks as prosemirrorJSONToBlocksUtil, - _prosemirrorNodeToBlocks as prosemirrorNodeToBlocksUtil, blocksToYDoc as blocksToYDocUtil, blocksToYXmlFragment as blocksToYXmlFragmentUtil, yDocToBlocks as yDocToBlocksUtil, @@ -102,7 +102,7 @@ export class ServerBlockNoteEditor< * @returns BlockNote style JSON */ public _prosemirrorNodeToBlocks(pmNode: Node) { - return prosemirrorNodeToBlocksUtil(pmNode); + return docToBlocks(pmNode); } /** From e7e39070444bc7004181e84d8c03fc545095c794 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 11 Nov 2025 11:41:46 +0100 Subject: [PATCH 4/5] docs: update link --- .../docs/reference/editor/yjs-utilities.mdx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/content/docs/reference/editor/yjs-utilities.mdx b/docs/content/docs/reference/editor/yjs-utilities.mdx index 64254d34e4..184232da89 100644 --- a/docs/content/docs/reference/editor/yjs-utilities.mdx +++ b/docs/content/docs/reference/editor/yjs-utilities.mdx @@ -42,7 +42,8 @@ These conversions are essential for: Converts BlockNote blocks into a Y.Doc. This is useful when importing existing content to a Y.Doc for the first time. - **Important:** This should not be used to rehydrate a Y.Doc from a database once collaboration has begun, as all history will be lost. + **Important:** This should not be used to rehydrate a Y.Doc from a database + once collaboration has begun, as all history will be lost. ```typescript @@ -53,8 +54,8 @@ function blocksToYDoc< >( editor: BlockNoteEditor, blocks: PartialBlock[], - xmlFragment?: string -): Y.Doc + xmlFragment?: string, +): Y.Doc; ``` **Parameters:** @@ -105,8 +106,8 @@ function blocksToYXmlFragment< >( editor: BlockNoteEditor, blocks: Block[], - xmlFragment?: Y.XmlFragment -): Y.XmlFragment + xmlFragment?: Y.XmlFragment, +): Y.XmlFragment; ``` **Parameters:** @@ -153,8 +154,8 @@ function yDocToBlocks< >( editor: BlockNoteEditor, ydoc: Y.Doc, - xmlFragment?: string -): Block[] + xmlFragment?: string, +): Block[]; ``` **Parameters:** @@ -194,8 +195,8 @@ function yXmlFragmentToBlocks< SSchema extends StyleSchema, >( editor: BlockNoteEditor, - xmlFragment: Y.XmlFragment -): Block[] + xmlFragment: Y.XmlFragment, +): Block[]; ``` **Parameters:** @@ -235,8 +236,8 @@ function _blocksToProsemirrorNode< SSchema extends StyleSchema, >( editor: BlockNoteEditor, - blocks: PartialBlock[] -): Node + blocks: PartialBlock[], +): Node; ``` **Parameters:** @@ -257,8 +258,8 @@ function _prosemirrorJSONToBlocks< SSchema extends StyleSchema, >( editor: BlockNoteEditor, - json: any -): Block[] + json: any, +): Block[]; ``` **Parameters:** @@ -346,5 +347,4 @@ console.log(originalBlocks); // Same structure as convertedBlocks - [Real-time Collaboration](/docs/features/collaboration) - Learn how to set up collaboration in BlockNote - [Manipulating Content](/docs/reference/editor/manipulating-content) - Working with blocks and inline content -- [Server Utilities](/docs/reference/server-util) - Server-side utilities for BlockNote (uses these YJS utilities internally) - +- [Server Processing](/docs/features/server-processing) - Server-side processing for BlockNote (uses these YJS utilities internally) From f2f752aecbd28baced9b86c0237ea063649dc346 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 11 Nov 2025 15:44:15 +0100 Subject: [PATCH 5/5] docs: rm prosemirror stuff, make clear when to use --- .../docs/reference/editor/yjs-utilities.mdx | 105 ++---------------- 1 file changed, 7 insertions(+), 98 deletions(-) diff --git a/docs/content/docs/reference/editor/yjs-utilities.mdx b/docs/content/docs/reference/editor/yjs-utilities.mdx index 184232da89..50ead8d1c6 100644 --- a/docs/content/docs/reference/editor/yjs-utilities.mdx +++ b/docs/content/docs/reference/editor/yjs-utilities.mdx @@ -8,6 +8,13 @@ imageTitle: YJS Utilities The `@blocknote/core/yjs` export provides utilities for converting between BlockNote blocks and YJS collaborative documents. These utilities are useful when you need to work with YJS documents outside of the standard collaboration setup, such as importing existing content or working with YJS documents programmatically. + + **Important:** This package is for advanced use cases where you need to + convert between BlockNote blocks and YJS documents programmatically. For most + use cases, you should use the [collaboration + features](/docs/features/collaboration) directly instead. + + ## Import ```typescript @@ -16,8 +23,6 @@ import { blocksToYXmlFragment, yDocToBlocks, yXmlFragmentToBlocks, - _blocksToProsemirrorNode, - _prosemirrorJSONToBlocks, } from "@blocknote/core/yjs"; ``` @@ -27,7 +32,6 @@ YJS utilities enable bidirectional conversion between: - **BlockNote blocks** ↔ **Y.Doc** (YJS document) - **BlockNote blocks** ↔ **Y.XmlFragment** (YJS XML fragment) -- **BlockNote blocks** ↔ **Prosemirror nodes** (intermediate format) These conversions are essential for: @@ -223,101 +227,6 @@ const fragment = doc.getXmlFragment("my-fragment"); const blocks = yXmlFragmentToBlocks(editor, fragment); ``` -## Prosemirror Conversions - -### `_blocksToProsemirrorNode` - -Converts BlockNote blocks to a Prosemirror node. This is an intermediate conversion step used internally. - -```typescript -function _blocksToProsemirrorNode< - BSchema extends BlockSchema, - ISchema extends InlineContentSchema, - SSchema extends StyleSchema, ->( - editor: BlockNoteEditor, - blocks: PartialBlock[], -): Node; -``` - -**Parameters:** - -- `editor` - The BlockNote editor instance -- `blocks` - Array of blocks to convert - -**Returns:** Prosemirror root node - -### `_prosemirrorJSONToBlocks` - -Converts Prosemirror JSON to BlockNote blocks. - -```typescript -function _prosemirrorJSONToBlocks< - BSchema extends BlockSchema, - ISchema extends InlineContentSchema, - SSchema extends StyleSchema, ->( - editor: BlockNoteEditor, - json: any, -): Block[]; -``` - -**Parameters:** - -- `editor` - The BlockNote editor instance -- `json` - Prosemirror JSON object - -**Returns:** Array of BlockNote blocks - -## Complete Example: Importing Existing Content - -Here's a complete example showing how to import existing BlockNote content into a YJS document for collaboration: - -```typescript -import { BlockNoteEditor } from "@blocknote/core"; -import { blocksToYDoc, yDocToBlocks } from "@blocknote/core/yjs"; -import { WebrtcProvider } from "y-webrtc"; -import * as Y from "yjs"; - -// Create editor -const editor = BlockNoteEditor.create(); - -// Existing content you want to import -const existingBlocks = [ - { - type: "heading", - props: { level: 1 }, - content: "Welcome to Collaboration", - }, - { - type: "paragraph", - content: "This content will be synced across all users.", - }, -]; - -// Convert blocks to Y.Doc -const ydoc = blocksToYDoc(editor, existingBlocks); - -// Set up collaboration provider -const provider = new WebrtcProvider("my-room", ydoc); - -// Create editor with collaboration -const collaborativeEditor = BlockNoteEditor.create({ - collaboration: { - provider, - fragment: ydoc.getXmlFragment("prosemirror"), - user: { - name: "User 1", - color: "#ff0000", - }, - }, -}); - -// Later, you can read the content back -const currentBlocks = yDocToBlocks(editor, ydoc); -console.log("Current document:", currentBlocks); -``` - ## Round-trip Conversion All conversion functions support round-trip conversion, meaning you can convert blocks → YJS → blocks and get back the same content: