Skip to content

Commit 87e1213

Browse files
committed
fix(server-util): make domShim be pluggable
1 parent d192b37 commit 87e1213

File tree

7 files changed

+344
-50
lines changed

7 files changed

+344
-50
lines changed

docs/content/docs/features/server-processing.mdx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,29 @@ While you can use the `BlockNoteEditor` on the client side, you can also use `Se
1212
For example, use the following code to convert a BlockNote document to HTML on the server:
1313

1414
```tsx
15-
import { ServerBlockNoteEditor } from "@blocknote/server-util";
16-
17-
const editor = ServerBlockNoteEditor.create();
15+
import { ServerBlockNoteEditor, DomShim } from "@blocknote/server-util";
16+
import { JSDOM } from "jsdom";
17+
18+
// Create a DOM shim (you can use jsdom, happydom, or any other DOM implementation)
19+
const jsdomShim: DomShim = {
20+
acquire() {
21+
const dom = new JSDOM();
22+
return {
23+
window: dom.window as any,
24+
document: dom.window.document as any,
25+
};
26+
},
27+
};
28+
29+
const editor = ServerBlockNoteEditor.create({}, jsdomShim);
1830
const html = await editor.blocksToFullHTML(blocks);
1931
```
2032

2133
`ServerBlockNoteEditor.create` takes the same BlockNoteEditorOptions as `useCreateBlockNote` and `BlockNoteEditor.create` ([see docs](/docs/getting-started)),
2234
so you can pass the same configuration (for example, your custom schema) to your server-side BlockNote editor as on the client.
2335

36+
**Note:** Methods that require DOM (like `blocksToFullHTML`, `blocksToHTMLLossy`, `blocksToMarkdownLossy`, etc.) require a `DomShim` to be provided. You can use any DOM implementation (jsdom, happydom, etc.) by implementing the `DomShim` interface.
37+
2438
## Functions for converting blocks
2539

2640
`ServerBlockNoteEditor` exposes the same functions for converting blocks as the client side editor ([HTML](/docs/features/import/html), [Markdown](/docs/features/import/markdown)):

examples/02-backend/04-rendering-static-documents/src/App.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,20 @@ import "@blocknote/mantine/style.css";
44
/**
55
On Server Side, you can use the ServerBlockNoteEditor to render BlockNote documents to HTML. e.g.:
66
7-
import { ServerBlockNoteEditor } from "@blocknote/server-util";
7+
import { ServerBlockNoteEditor, DomShim } from "@blocknote/server-util";
8+
import { JSDOM } from "jsdom";
89
9-
const editor = ServerBlockNoteEditor.create();
10+
const jsdomShim: DomShim = {
11+
acquire() {
12+
const dom = new JSDOM();
13+
return {
14+
window: dom.window as any,
15+
document: dom.window.document as any,
16+
};
17+
},
18+
};
19+
20+
const editor = ServerBlockNoteEditor.create({}, jsdomShim);
1021
const html = await editor.blocksToFullHTML(document);
1122
1223
You can then use render this HTML as a static page or send it to the client. Make sure to include the editor stylesheets:

packages/server-util/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
"@blocknote/react": "0.42.0",
6161
"@tiptap/core": "^3.10.2",
6262
"@tiptap/pm": "^3.10.2",
63-
"jsdom": "^25.0.1",
6463
"y-prosemirror": "^1.3.7",
6564
"y-protocols": "^1.0.6",
6665
"yjs": "^13.6.27"

packages/server-util/src/context/ServerBlockNoteEditor.test.ts

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
import { Block } from "@blocknote/core";
22
import { describe, expect, it } from "vitest";
3-
import { ServerBlockNoteEditor } from "./ServerBlockNoteEditor.js";
3+
import { JSDOM } from "jsdom";
4+
import { DomShim, ServerBlockNoteEditor } from "./ServerBlockNoteEditor.js";
5+
6+
const jsdomShim: DomShim = {
7+
acquire() {
8+
const dom = new JSDOM();
9+
return {
10+
window: dom.window as any,
11+
document: dom.window.document as any,
12+
};
13+
},
14+
};
415

516
describe("Test ServerBlockNoteEditor", () => {
6-
const editor = ServerBlockNoteEditor.create();
17+
const editor = ServerBlockNoteEditor.create({}, jsdomShim);
718

819
const blocks: Block[] = [
920
{
@@ -124,3 +135,146 @@ describe("Test ServerBlockNoteEditor", () => {
124135
expect(blockOutput).toMatchSnapshot();
125136
});
126137
});
138+
139+
describe("ServerBlockNoteEditor with domShim", () => {
140+
it("uses provided domShim correctly", async () => {
141+
let acquireCalled = false;
142+
let releaseCalled = false;
143+
let releasedGlobals: any = null;
144+
145+
const testShim: DomShim = {
146+
acquire() {
147+
acquireCalled = true;
148+
const dom = new JSDOM();
149+
return {
150+
window: dom.window as any,
151+
document: dom.window.document as any,
152+
};
153+
},
154+
release(globals) {
155+
releaseCalled = true;
156+
releasedGlobals = globals;
157+
},
158+
};
159+
160+
const editor = ServerBlockNoteEditor.create({}, testShim);
161+
const blocks: Block[] = [
162+
{
163+
id: "1",
164+
type: "paragraph",
165+
props: {
166+
backgroundColor: "default",
167+
textColor: "default",
168+
textAlignment: "left",
169+
},
170+
content: [
171+
{
172+
type: "text",
173+
text: "Test",
174+
styles: {},
175+
},
176+
],
177+
children: [],
178+
},
179+
];
180+
181+
const html = await editor.blocksToFullHTML(blocks);
182+
183+
expect(acquireCalled).toBe(true);
184+
expect(releaseCalled).toBe(true);
185+
expect(releasedGlobals).toBeTruthy();
186+
expect(releasedGlobals.document).toBeTruthy();
187+
expect(releasedGlobals.window).toBeTruthy();
188+
expect(html).toBeTruthy();
189+
});
190+
191+
it("throws error when no domShim is provided and globals don't exist", async () => {
192+
// Save original globals
193+
const originalWindow = globalThis.window;
194+
const originalDocument = globalThis.document;
195+
196+
try {
197+
// Remove globals to simulate server environment
198+
delete (globalThis as any).window;
199+
delete (globalThis as any).document;
200+
201+
const editor = ServerBlockNoteEditor.create();
202+
const blocks: Block[] = [
203+
{
204+
id: "1",
205+
type: "paragraph",
206+
props: {
207+
backgroundColor: "default",
208+
textColor: "default",
209+
textAlignment: "left",
210+
},
211+
content: [
212+
{
213+
type: "text",
214+
text: "Test",
215+
styles: {},
216+
},
217+
],
218+
children: [],
219+
},
220+
];
221+
222+
await expect(editor.blocksToFullHTML(blocks)).rejects.toThrow(
223+
"DOM globals (window/document) are required but not available",
224+
);
225+
226+
await expect(editor.blocksToHTMLLossy(blocks)).rejects.toThrow(
227+
"DOM globals (window/document) are required but not available",
228+
);
229+
230+
await expect(editor.blocksToMarkdownLossy(blocks)).rejects.toThrow(
231+
"DOM globals (window/document) are required but not available",
232+
);
233+
} finally {
234+
// Restore original globals
235+
globalThis.window = originalWindow;
236+
globalThis.document = originalDocument;
237+
}
238+
});
239+
240+
it("works when globals already exist without domShim", async () => {
241+
// This test verifies that if window/document already exist globally,
242+
// methods work without a domShim
243+
// Note: This test only works if the test environment has globals set up
244+
// (e.g., via jsdom in vitest config)
245+
if (
246+
typeof globalThis.window !== "undefined" &&
247+
typeof globalThis.document !== "undefined"
248+
) {
249+
const editor = ServerBlockNoteEditor.create();
250+
const blocks: Block[] = [
251+
{
252+
id: "1",
253+
type: "paragraph",
254+
props: {
255+
backgroundColor: "default",
256+
textColor: "default",
257+
textAlignment: "left",
258+
},
259+
content: [
260+
{
261+
type: "text",
262+
text: "Test",
263+
styles: {},
264+
},
265+
],
266+
children: [],
267+
},
268+
];
269+
270+
// Should work if globals exist (like in a test environment with jsdom)
271+
const html = await editor.blocksToFullHTML(blocks);
272+
// eslint-disable-next-line jest/no-conditional-expect
273+
expect(html).toBeTruthy();
274+
} else {
275+
// Skip test if globals don't exist in this environment
276+
// eslint-disable-next-line jest/no-conditional-expect
277+
expect(true).toBe(true);
278+
}
279+
});
280+
});

0 commit comments

Comments
 (0)