Skip to content

Commit 3135030

Browse files
authored
fix editor converter (docmost#1647)
1 parent 3fae41a commit 3135030

File tree

5 files changed

+124
-29
lines changed

5 files changed

+124
-29
lines changed

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"class-validator": "^0.14.1",
6363
"cookie": "^1.0.2",
6464
"fs-extra": "^11.3.0",
65-
"happy-dom": "^15.11.6",
65+
"happy-dom": "^18.0.1",
6666
"jsonwebtoken": "^9.0.2",
6767
"kysely": "^0.28.2",
6868
"kysely-migration-cli": "^0.4.2",

apps/server/src/collaboration/collaboration.util.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,10 @@ import {
3535
Subpages,
3636
} from '@docmost/editor-ext';
3737
import { generateText, getSchema, JSONContent } from '@tiptap/core';
38-
import { generateHTML } from '../common/helpers/prosemirror/html';
38+
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
3939
// @tiptap/html library works best for generating prosemirror json state but not HTML
4040
// see: https://github.com/ueberdosis/tiptap/issues/5352
4141
// see:https://github.com/ueberdosis/tiptap/issues/4089
42-
import { generateJSON } from '@tiptap/html';
4342
import { Node } from '@tiptap/pm/model';
4443

4544
export const tiptapExtensions = [
Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
import { Extensions, getSchema, JSONContent } from '@tiptap/core';
2-
import { DOMSerializer, Node } from '@tiptap/pm/model';
3-
import { Window } from 'happy-dom';
1+
import { type Extensions, type JSONContent, getSchema } from '@tiptap/core';
2+
import { Node } from '@tiptap/pm/model';
3+
import { getHTMLFromFragment } from './getHTMLFromFragment';
44

5+
/**
6+
* This function generates HTML from a ProseMirror JSON content object.
7+
*
8+
* @remarks **Important**: This function requires `happy-dom` to be installed in your project.
9+
* @param doc - The ProseMirror JSON content object.
10+
* @param extensions - The Tiptap extensions used to build the schema.
11+
* @returns The generated HTML string.
12+
* @example
13+
* ```js
14+
* const html = generateHTML(doc, extensions)
15+
* console.log(html)
16+
* ```
17+
*/
518
export function generateHTML(doc: JSONContent, extensions: Extensions): string {
19+
if (typeof window !== 'undefined') {
20+
throw new Error(
21+
'generateHTML can only be used in a Node environment\nIf you want to use this in a browser environment, use the `@tiptap/html` import instead.',
22+
);
23+
}
24+
625
const schema = getSchema(extensions);
726
const contentNode = Node.fromJSON(schema, doc);
827

9-
const window = new Window();
10-
11-
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(
12-
contentNode.content,
13-
{
14-
document: window.document as unknown as Document,
15-
},
16-
);
17-
18-
const serializer = new window.XMLSerializer();
19-
// @ts-ignore
20-
return serializer.serializeToString(fragment as unknown as Node);
28+
return getHTMLFromFragment(contentNode, schema);
2129
}
Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,55 @@
1-
import { Extensions, getSchema } from '@tiptap/core';
2-
import { DOMParser, ParseOptions } from '@tiptap/pm/model';
1+
import type { Extensions } from '@tiptap/core';
2+
import { getSchema } from '@tiptap/core';
3+
import { type ParseOptions, DOMParser as PMDOMParser } from '@tiptap/pm/model';
34
import { Window } from 'happy-dom';
45

5-
// this function does not work as intended
6-
// it has issues with closing tags
6+
/**
7+
* Generates a JSON object from the given HTML string and converts it into a Prosemirror node with content.
8+
* @remarks **Important**: This function requires `happy-dom` to be installed in your project.
9+
* @param {string} html - The HTML string to be converted into a Prosemirror node.
10+
* @param {Extensions} extensions - The extensions to be used for generating the schema.
11+
* @param {ParseOptions} options - The options to be supplied to the parser.
12+
* @returns {Promise<Record<string, any>>} - A promise with the generated JSON object.
13+
* @example
14+
* const html = '<p>Hello, world!</p>'
15+
* const extensions = [...]
16+
* const json = generateJSON(html, extensions)
17+
* console.log(json) // { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }] }] }
18+
*/
719
export function generateJSON(
820
html: string,
921
extensions: Extensions,
1022
options?: ParseOptions,
1123
): Record<string, any> {
12-
const schema = getSchema(extensions);
24+
if (typeof window !== 'undefined') {
25+
throw new Error(
26+
'generateJSON can only be used in a Node environment\nIf you want to use this in a browser environment, use the `@tiptap/html` import instead.',
27+
);
28+
}
1329

14-
const window = new Window();
15-
const document = window.document;
16-
document.body.innerHTML = html;
30+
const localWindow = new Window();
31+
const localDOMParser = new localWindow.DOMParser();
32+
let result: Record<string, any>;
1733

18-
return DOMParser.fromSchema(schema)
19-
.parse(document as never, options)
20-
.toJSON();
34+
try {
35+
const schema = getSchema(extensions);
36+
let doc: ReturnType<typeof localDOMParser.parseFromString> | null = null;
37+
38+
const htmlString = `<!DOCTYPE html><html><body>${html}</body></html>`;
39+
doc = localDOMParser.parseFromString(htmlString, 'text/html');
40+
41+
if (!doc) {
42+
throw new Error('Failed to parse HTML string');
43+
}
44+
45+
result = PMDOMParser.fromSchema(schema)
46+
.parse(doc.body as unknown as Node, options)
47+
.toJSON();
48+
} finally {
49+
// clean up happy-dom to avoid memory leaks
50+
localWindow.happyDOM.abort();
51+
localWindow.happyDOM.close();
52+
}
53+
54+
return result;
2155
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Node, Schema } from '@tiptap/pm/model';
2+
import { DOMSerializer } from '@tiptap/pm/model';
3+
import { Window } from 'happy-dom';
4+
5+
/**
6+
* Returns the HTML string representation of a given document node.
7+
*
8+
* @remarks **Important**: This function requires `happy-dom` to be installed in your project.
9+
* @param doc - The document node to serialize.
10+
* @param schema - The Prosemirror schema to use for serialization.
11+
* @returns A promise containing the HTML string representation of the document fragment.
12+
*
13+
* @example
14+
* ```typescript
15+
* const html = getHTMLFromFragment(doc, schema)
16+
* ```
17+
*/
18+
export function getHTMLFromFragment(
19+
doc: Node,
20+
schema: Schema,
21+
options?: { document?: Document },
22+
): string {
23+
if (options?.document) {
24+
const wrap = options.document.createElement('div');
25+
26+
DOMSerializer.fromSchema(schema).serializeFragment(
27+
doc.content,
28+
{ document: options.document },
29+
wrap,
30+
);
31+
return wrap.innerHTML;
32+
}
33+
34+
const localWindow = new Window();
35+
let result: string;
36+
37+
try {
38+
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(
39+
doc.content,
40+
{
41+
document: localWindow.document as unknown as Document,
42+
},
43+
);
44+
45+
const serializer = new localWindow.XMLSerializer();
46+
result = serializer.serializeToString(fragment as any);
47+
} finally {
48+
// clean up happy-dom to avoid memory leaks
49+
localWindow.happyDOM.abort();
50+
localWindow.happyDOM.close();
51+
}
52+
53+
return result;
54+
}

0 commit comments

Comments
 (0)