Skip to content

Commit 58592c4

Browse files
authored
feat(yjs): expose Y.js BlockNote conversion primitives #1866 (#2166)
1 parent 5014a2d commit 58592c4

File tree

12 files changed

+1480
-81
lines changed

12 files changed

+1480
-81
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"title": "Editor",
3-
"pages": ["overview", "manipulating-content", "cursor-selections", "..."]
3+
"pages": ["overview", "manipulating-content", "cursor-selections", "yjs-utilities", "..."]
44
}

docs/content/docs/reference/editor/overview.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ The editor can be configured with the following options when using `BlockNoteEdi
118118
name="BlockNoteEditorOptions"
119119
/>
120120

121+
## YJS Utilities
122+
123+
BlockNote provides utilities for working with YJS collaborative documents. These utilities allow you to convert between BlockNote blocks and YJS documents programmatically.
124+
125+
To read more about YJS utilities, see the [YJS Utilities](/docs/reference/editor/yjs-utilities) reference.
126+
121127
## Related Documentation
122128

123129
For more detailed information about specific areas:
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
---
2+
title: YJS Utilities
3+
description: Utilities for converting between BlockNote blocks and YJS collaborative documents
4+
imageTitle: YJS Utilities
5+
---
6+
7+
# YJS Utilities
8+
9+
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.
10+
11+
<Callout type="warning">
12+
**Important:** This package is for advanced use cases where you need to
13+
convert between BlockNote blocks and YJS documents programmatically. For most
14+
use cases, you should use the [collaboration
15+
features](/docs/features/collaboration) directly instead.
16+
</Callout>
17+
18+
## Import
19+
20+
```typescript
21+
import {
22+
blocksToYDoc,
23+
blocksToYXmlFragment,
24+
yDocToBlocks,
25+
yXmlFragmentToBlocks,
26+
} from "@blocknote/core/yjs";
27+
```
28+
29+
## Overview
30+
31+
YJS utilities enable bidirectional conversion between:
32+
33+
- **BlockNote blocks****Y.Doc** (YJS document)
34+
- **BlockNote blocks****Y.XmlFragment** (YJS XML fragment)
35+
36+
These conversions are essential for:
37+
38+
- Importing existing BlockNote content into a YJS document for collaboration
39+
- Exporting content from a YJS document back to BlockNote blocks
40+
- Working with YJS documents programmatically without an active editor instance
41+
42+
## Converting Blocks to YJS Documents
43+
44+
### `blocksToYDoc`
45+
46+
Converts BlockNote blocks into a Y.Doc. This is useful when importing existing content to a Y.Doc for the first time.
47+
48+
<Callout type="warning">
49+
**Important:** This should not be used to rehydrate a Y.Doc from a database
50+
once collaboration has begun, as all history will be lost.
51+
</Callout>
52+
53+
```typescript
54+
function blocksToYDoc<
55+
BSchema extends BlockSchema,
56+
ISchema extends InlineContentSchema,
57+
SSchema extends StyleSchema,
58+
>(
59+
editor: BlockNoteEditor<BSchema, ISchema, SSchema>,
60+
blocks: PartialBlock<BSchema, ISchema, SSchema>[],
61+
xmlFragment?: string,
62+
): Y.Doc;
63+
```
64+
65+
**Parameters:**
66+
67+
- `editor` - The BlockNote editor instance
68+
- `blocks` - Array of blocks to convert
69+
- `xmlFragment` - Optional XML fragment name (defaults to `"prosemirror"`)
70+
71+
**Returns:** A new Y.Doc containing the converted blocks
72+
73+
**Example:**
74+
75+
```typescript
76+
import { BlockNoteEditor } from "@blocknote/core";
77+
import { blocksToYDoc } from "@blocknote/core/yjs";
78+
import * as Y from "yjs";
79+
80+
const editor = BlockNoteEditor.create();
81+
82+
const blocks = [
83+
{
84+
type: "paragraph",
85+
content: "Hello, world!",
86+
},
87+
{
88+
type: "heading",
89+
props: { level: 1 },
90+
content: "My Document",
91+
},
92+
];
93+
94+
// Convert blocks to Y.Doc
95+
const ydoc = blocksToYDoc(editor, blocks);
96+
97+
// Now you can use this Y.Doc with a YJS provider for collaboration
98+
const provider = new WebrtcProvider("my-room", ydoc);
99+
```
100+
101+
### `blocksToYXmlFragment`
102+
103+
Converts BlockNote blocks into a Y.XmlFragment. This is useful when you want to work with a specific XML fragment within a Y.Doc.
104+
105+
```typescript
106+
function blocksToYXmlFragment<
107+
BSchema extends BlockSchema,
108+
ISchema extends InlineContentSchema,
109+
SSchema extends StyleSchema,
110+
>(
111+
editor: BlockNoteEditor<BSchema, ISchema, SSchema>,
112+
blocks: Block<BSchema, ISchema, SSchema>[],
113+
xmlFragment?: Y.XmlFragment,
114+
): Y.XmlFragment;
115+
```
116+
117+
**Parameters:**
118+
119+
- `editor` - The BlockNote editor instance
120+
- `blocks` - Array of blocks to convert
121+
- `xmlFragment` - Optional existing Y.XmlFragment to populate (creates new one if not provided)
122+
123+
**Returns:** A Y.XmlFragment containing the converted blocks
124+
125+
**Example:**
126+
127+
```typescript
128+
import { BlockNoteEditor } from "@blocknote/core";
129+
import { blocksToYXmlFragment } from "@blocknote/core/yjs";
130+
import * as Y from "yjs";
131+
132+
const editor = BlockNoteEditor.create();
133+
const doc = new Y.Doc();
134+
const fragment = doc.getXmlFragment("my-fragment");
135+
136+
const blocks = [
137+
{
138+
type: "paragraph",
139+
content: "Content for fragment",
140+
},
141+
];
142+
143+
// Convert blocks to the XML fragment
144+
blocksToYXmlFragment(editor, blocks, fragment);
145+
```
146+
147+
## Converting YJS Documents to Blocks
148+
149+
### `yDocToBlocks`
150+
151+
Converts a Y.Doc back into BlockNote blocks. This is useful for reading content from a YJS document.
152+
153+
```typescript
154+
function yDocToBlocks<
155+
BSchema extends BlockSchema,
156+
ISchema extends InlineContentSchema,
157+
SSchema extends StyleSchema,
158+
>(
159+
editor: BlockNoteEditor<BSchema, ISchema, SSchema>,
160+
ydoc: Y.Doc,
161+
xmlFragment?: string,
162+
): Block<BSchema, ISchema, SSchema>[];
163+
```
164+
165+
**Parameters:**
166+
167+
- `editor` - The BlockNote editor instance
168+
- `ydoc` - The Y.Doc to convert
169+
- `xmlFragment` - Optional XML fragment name (defaults to `"prosemirror"`)
170+
171+
**Returns:** Array of BlockNote blocks
172+
173+
**Example:**
174+
175+
```typescript
176+
import { BlockNoteEditor } from "@blocknote/core";
177+
import { yDocToBlocks } from "@blocknote/core/yjs";
178+
import * as Y from "yjs";
179+
180+
const editor = BlockNoteEditor.create();
181+
const ydoc = new Y.Doc();
182+
183+
// ... Y.Doc is populated through collaboration or other means ...
184+
185+
// Convert Y.Doc back to blocks
186+
const blocks = yDocToBlocks(editor, ydoc);
187+
188+
console.log(blocks); // Array of BlockNote blocks
189+
```
190+
191+
### `yXmlFragmentToBlocks`
192+
193+
Converts a Y.XmlFragment back into BlockNote blocks.
194+
195+
```typescript
196+
function yXmlFragmentToBlocks<
197+
BSchema extends BlockSchema,
198+
ISchema extends InlineContentSchema,
199+
SSchema extends StyleSchema,
200+
>(
201+
editor: BlockNoteEditor<BSchema, ISchema, SSchema>,
202+
xmlFragment: Y.XmlFragment,
203+
): Block<BSchema, ISchema, SSchema>[];
204+
```
205+
206+
**Parameters:**
207+
208+
- `editor` - The BlockNote editor instance
209+
- `xmlFragment` - The Y.XmlFragment to convert
210+
211+
**Returns:** Array of BlockNote blocks
212+
213+
**Example:**
214+
215+
```typescript
216+
import { BlockNoteEditor } from "@blocknote/core";
217+
import { yXmlFragmentToBlocks } from "@blocknote/core/yjs";
218+
import * as Y from "yjs";
219+
220+
const editor = BlockNoteEditor.create();
221+
const doc = new Y.Doc();
222+
const fragment = doc.getXmlFragment("my-fragment");
223+
224+
// ... Fragment is populated through collaboration or other means ...
225+
226+
// Convert fragment back to blocks
227+
const blocks = yXmlFragmentToBlocks(editor, fragment);
228+
```
229+
230+
## Round-trip Conversion
231+
232+
All conversion functions support round-trip conversion, meaning you can convert blocks → YJS → blocks and get back the same content:
233+
234+
```typescript
235+
import { BlockNoteEditor } from "@blocknote/core";
236+
import { blocksToYDoc, yDocToBlocks } from "@blocknote/core/yjs";
237+
238+
const editor = BlockNoteEditor.create();
239+
240+
const originalBlocks = [
241+
{
242+
type: "paragraph",
243+
content: "Test content",
244+
},
245+
];
246+
247+
// Convert to Y.Doc and back
248+
const ydoc = blocksToYDoc(editor, originalBlocks);
249+
const convertedBlocks = yDocToBlocks(editor, ydoc);
250+
251+
// originalBlocks and convertedBlocks are equivalent
252+
console.log(originalBlocks); // Same structure as convertedBlocks
253+
```
254+
255+
## Related Documentation
256+
257+
- [Real-time Collaboration](/docs/features/collaboration) - Learn how to set up collaboration in BlockNote
258+
- [Manipulating Content](/docs/reference/editor/manipulating-content) - Working with blocks and inline content
259+
- [Server Processing](/docs/features/server-processing) - Server-side processing for BlockNote (uses these YJS utilities internally)

packages/core/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@
6666
"types": "./types/src/i18n/index.d.ts",
6767
"import": "./dist/locales.js",
6868
"require": "./dist/locales.cjs"
69+
},
70+
"./yjs": {
71+
"types": "./types/src/yjs/index.d.ts",
72+
"import": "./dist/yjs.js",
73+
"require": "./dist/yjs.cjs"
6974
}
7075
},
7176
"scripts": {

packages/core/src/api/nodeConversions/nodeToBlock.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
getBlockCache,
2424
getBlockSchema,
2525
getInlineContentSchema,
26+
getPmSchema,
2627
getStyleSchema,
2728
} from "../pmUtil.js";
2829

@@ -503,26 +504,28 @@ export function docToBlocks<
503504
S extends StyleSchema,
504505
>(
505506
doc: Node,
506-
schema: Schema,
507+
schema: Schema = getPmSchema(doc),
507508
blockSchema: BSchema = getBlockSchema(schema) as BSchema,
508509
inlineContentSchema: I = getInlineContentSchema(schema) as I,
509510
styleSchema: S = getStyleSchema(schema) as S,
510511
blockCache = getBlockCache(schema),
511512
) {
512513
const blocks: Block<BSchema, I, S>[] = [];
513-
doc.firstChild!.descendants((node) => {
514-
blocks.push(
515-
nodeToBlock(
516-
node,
517-
schema,
518-
blockSchema,
519-
inlineContentSchema,
520-
styleSchema,
521-
blockCache,
522-
),
523-
);
524-
return false;
525-
});
514+
if (doc.firstChild) {
515+
doc.firstChild.descendants((node) => {
516+
blocks.push(
517+
nodeToBlock(
518+
node,
519+
schema,
520+
blockSchema,
521+
inlineContentSchema,
522+
styleSchema,
523+
blockCache,
524+
),
525+
);
526+
return false;
527+
});
528+
}
526529
return blocks;
527530
}
528531

packages/core/src/yjs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./utils.js";

0 commit comments

Comments
 (0)