@open-mercato/documents 0.10.1-develop.7809.1.ea24461aeb → 0.10.1-develop.7811.1.66050c1838

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,62 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { TiptapTransformer } from "@hocuspocus/transformer";
3
3
  import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
4
+ import { getSchema } from "@tiptap/core";
5
+ import {
6
+ DOMParser as ProseMirrorDOMParser,
7
+ DOMSerializer,
8
+ Node as ProseMirrorNode
9
+ } from "@tiptap/pm/model";
4
10
  import * as Y from "yjs";
5
11
  import { deriveContentTextFromHtml } from "./contentService.js";
6
12
  import { COLLAB_FRAGMENT_FIELD, getDocumentEditorExtensions } from "./editorConfig.js";
7
13
  import { assertDocumentContentResourceLimits } from "./resourceLimits.js";
8
14
  const requireFromHere = createRequire(import.meta.url);
9
- let tiptapHtmlServer = null;
10
- function getTiptapHtmlServer() {
11
- if (!tiptapHtmlServer) {
12
- tiptapHtmlServer = requireFromHere("@tiptap/html/server");
15
+ let happyDomModule = null;
16
+ function createHtmlWindow() {
17
+ if (!happyDomModule) {
18
+ happyDomModule = requireFromHere("happy-dom");
19
+ }
20
+ return new happyDomModule.Window({
21
+ settings: {
22
+ disableJavaScriptEvaluation: true,
23
+ disableJavaScriptFileLoading: true,
24
+ disableCSSFileLoading: true,
25
+ disableIframePageLoading: true,
26
+ disableComputedStyleRendering: true
27
+ }
28
+ });
29
+ }
30
+ function generateHtml(doc, extensions) {
31
+ const schema = getSchema(extensions);
32
+ const documentNode = ProseMirrorNode.fromJSON(schema, doc);
33
+ const localWindow = createHtmlWindow();
34
+ try {
35
+ const wrapper = localWindow.document.createElement("div");
36
+ DOMSerializer.fromSchema(schema).serializeFragment(
37
+ documentNode.content,
38
+ { document: localWindow.document },
39
+ wrapper
40
+ );
41
+ return wrapper.innerHTML;
42
+ } finally {
43
+ localWindow.happyDOM.abort();
44
+ localWindow.happyDOM.close();
45
+ }
46
+ }
47
+ function generateJson(html, extensions, options) {
48
+ const localWindow = createHtmlWindow();
49
+ try {
50
+ const parsedDocument = new localWindow.DOMParser().parseFromString(
51
+ `<!DOCTYPE html><html><body>${html}</body></html>`,
52
+ "text/html"
53
+ );
54
+ const schema = getSchema(extensions);
55
+ return ProseMirrorDOMParser.fromSchema(schema).parse(parsedDocument.body, options).toJSON();
56
+ } finally {
57
+ localWindow.happyDOM.abort();
58
+ localWindow.happyDOM.close();
13
59
  }
14
- return tiptapHtmlServer;
15
60
  }
16
61
  function hasRenderableContent(node) {
17
62
  if (node.type === "text") return Boolean(node.text);
@@ -27,8 +72,7 @@ function yDocToContent(ydoc) {
27
72
  if (!json || !hasRenderableContent(json)) {
28
73
  return { html: "", text: "" };
29
74
  }
30
- const { generateHTML } = getTiptapHtmlServer();
31
- const html = generateHTML(json, getDocumentEditorExtensions());
75
+ const html = generateHtml(json, getDocumentEditorExtensions());
32
76
  const text = deriveContentTextFromHtml(html);
33
77
  assertDocumentContentResourceLimits({ contentHtml: html, contentText: text });
34
78
  return { html, text };
@@ -41,8 +85,7 @@ function htmlToYDoc(html) {
41
85
  assertDocumentContentResourceLimits({ contentHtml: html });
42
86
  if (!html.trim()) return new Y.Doc();
43
87
  const extensions = getDocumentEditorExtensions();
44
- const { generateJSON } = getTiptapHtmlServer();
45
- const json = generateJSON(html, extensions);
88
+ const json = generateJson(html, extensions);
46
89
  return TiptapTransformer.toYdoc(json, COLLAB_FRAGMENT_FIELD, extensions);
47
90
  }
48
91
  function materializeDocumentHtml(html) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/documents/lib/collabMaterializer.ts"],
4
- "sourcesContent": ["import { createRequire } from 'node:module'\nimport type { JSONContent } from '@tiptap/core'\nimport { TiptapTransformer } from '@hocuspocus/transformer'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport * as Y from 'yjs'\nimport { deriveContentTextFromHtml } from './contentService'\nimport { COLLAB_FRAGMENT_FIELD, getDocumentEditorExtensions } from './editorConfig'\nimport { assertDocumentContentResourceLimits } from './resourceLimits'\n\ntype TiptapHtmlServer = typeof import('@tiptap/html/server')\n\nconst requireFromHere = createRequire(import.meta.url)\nlet tiptapHtmlServer: TiptapHtmlServer | null = null\n\nfunction getTiptapHtmlServer(): TiptapHtmlServer {\n if (!tiptapHtmlServer) {\n tiptapHtmlServer = requireFromHere('@tiptap/html/server') as TiptapHtmlServer\n }\n return tiptapHtmlServer\n}\n\nfunction hasRenderableContent(node: JSONContent): boolean {\n if (node.type === 'text') return Boolean(node.text)\n if (Array.isArray(node.content) && node.content.some(hasRenderableContent)) return true\n return Boolean(node.type && node.type !== 'doc' && node.type !== 'paragraph' && node.type !== 'text')\n}\n\nexport function yDocToContent(ydoc: Y.Doc): { html: string; text: string } | null {\n try {\n if (ydoc.getXmlFragment(COLLAB_FRAGMENT_FIELD).length === 0) {\n return { html: '', text: '' }\n }\n\n const json = TiptapTransformer.fromYdoc(ydoc, COLLAB_FRAGMENT_FIELD) as JSONContent | null\n if (!json || !hasRenderableContent(json)) {\n return { html: '', text: '' }\n }\n\n const { generateHTML } = getTiptapHtmlServer()\n const html = generateHTML(json, getDocumentEditorExtensions())\n const text = deriveContentTextFromHtml(html)\n assertDocumentContentResourceLimits({ contentHtml: html, contentText: text })\n return { html, text }\n } catch (error) {\n if (isCrudHttpError(error) && error.status === 413) throw error\n return null\n }\n}\n\nexport function htmlToYDoc(html: string): Y.Doc {\n assertDocumentContentResourceLimits({ contentHtml: html })\n if (!html.trim()) return new Y.Doc()\n\n const extensions = getDocumentEditorExtensions()\n const { generateJSON } = getTiptapHtmlServer()\n const json = generateJSON(html, extensions) as JSONContent\n return TiptapTransformer.toYdoc(json, COLLAB_FRAGMENT_FIELD, extensions)\n}\n\nexport type MaterializedDocumentHtml = {\n yjsState: Buffer\n html: string\n text: string\n}\n\n/**\n * Convert authored HTML through the same editor/Yjs pipeline used by persisted\n * document content. Callers that preview content before writing it must use the\n * returned HTML too, otherwise the preview can differ from the canonical markup\n * users receive after the document is created.\n */\nexport function materializeDocumentHtml(html: string): MaterializedDocumentHtml | null {\n try {\n const ydoc = htmlToYDoc(html)\n const content = yDocToContent(ydoc)\n if (!content) return null\n const result = {\n yjsState: Buffer.from(Y.encodeStateAsUpdate(ydoc)),\n html: content.html,\n text: content.text,\n }\n assertDocumentContentResourceLimits({\n yjsState: result.yjsState,\n contentHtml: result.html,\n contentText: result.text,\n })\n return result\n } catch (error) {\n if (isCrudHttpError(error) && error.status === 413) throw error\n return null\n }\n}\n\n/**\n * Replace REST-authored content as a Yjs update derived from the current CRDT\n * epoch. Keeping the old structs as deleted tombstones means a retained client\n * can merge the replacement without resurrecting or duplicating stale text.\n */\nexport function materializeDocumentContentReplacement(\n existingState: Buffer | Uint8Array | null | undefined,\n html: string,\n): MaterializedDocumentHtml | null {\n try {\n assertDocumentContentResourceLimits({ yjsState: existingState, contentHtml: html })\n const replacement = htmlToYDoc(html)\n const ydoc = new Y.Doc()\n if (existingState && existingState.length > 0) {\n Y.applyUpdate(ydoc, new Uint8Array(existingState))\n }\n\n const target = ydoc.getXmlFragment(COLLAB_FRAGMENT_FIELD)\n const replacementNodes = replacement\n .getXmlFragment(COLLAB_FRAGMENT_FIELD)\n .toArray()\n .filter((node): node is Y.XmlElement | Y.XmlText => (\n node instanceof Y.XmlElement || node instanceof Y.XmlText\n ))\n .map((node) => node.clone())\n ydoc.transact(() => {\n if (target.length > 0) target.delete(0, target.length)\n if (replacementNodes.length > 0) target.insert(0, replacementNodes)\n })\n const content = yDocToContent(ydoc)\n if (!content) return null\n const result = {\n yjsState: Buffer.from(Y.encodeStateAsUpdate(ydoc)),\n html: content.html,\n text: content.text,\n }\n assertDocumentContentResourceLimits({\n yjsState: result.yjsState,\n contentHtml: result.html,\n contentText: result.text,\n })\n return result\n } catch (error) {\n if (isCrudHttpError(error) && error.status === 413) throw error\n return null\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createRequire } from 'node:module'\nimport { TiptapTransformer } from '@hocuspocus/transformer'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getSchema, type Extensions, type JSONContent } from '@tiptap/core'\nimport {\n DOMParser as ProseMirrorDOMParser,\n DOMSerializer,\n Node as ProseMirrorNode,\n type ParseOptions,\n} from '@tiptap/pm/model'\nimport * as Y from 'yjs'\nimport { deriveContentTextFromHtml } from './contentService'\nimport { COLLAB_FRAGMENT_FIELD, getDocumentEditorExtensions } from './editorConfig'\nimport { assertDocumentContentResourceLimits } from './resourceLimits'\n\ntype HappyDomModule = typeof import('happy-dom')\ntype HappyDomWindow = InstanceType<HappyDomModule['Window']>\n\nconst requireFromHere = createRequire(import.meta.url)\nlet happyDomModule: HappyDomModule | null = null\n\nfunction createHtmlWindow(): HappyDomWindow {\n if (!happyDomModule) {\n happyDomModule = requireFromHere('happy-dom') as HappyDomModule\n }\n\n return new happyDomModule.Window({\n settings: {\n disableJavaScriptEvaluation: true,\n disableJavaScriptFileLoading: true,\n disableCSSFileLoading: true,\n disableIframePageLoading: true,\n disableComputedStyleRendering: true,\n },\n })\n}\n\nfunction generateHtml(doc: JSONContent, extensions: Extensions): string {\n const schema = getSchema(extensions)\n const documentNode = ProseMirrorNode.fromJSON(schema, doc)\n const localWindow = createHtmlWindow()\n\n try {\n const wrapper = localWindow.document.createElement('div')\n DOMSerializer.fromSchema(schema).serializeFragment(\n documentNode.content,\n { document: localWindow.document as unknown as Document },\n wrapper as unknown as HTMLElement,\n )\n return wrapper.innerHTML\n } finally {\n localWindow.happyDOM.abort()\n localWindow.happyDOM.close()\n }\n}\n\nfunction generateJson(html: string, extensions: Extensions, options?: ParseOptions): JSONContent {\n const localWindow = createHtmlWindow()\n\n try {\n const parsedDocument = new localWindow.DOMParser().parseFromString(\n `<!DOCTYPE html><html><body>${html}</body></html>`,\n 'text/html',\n )\n const schema = getSchema(extensions)\n return ProseMirrorDOMParser.fromSchema(schema)\n .parse(parsedDocument.body as unknown as Node, options)\n .toJSON()\n } finally {\n localWindow.happyDOM.abort()\n localWindow.happyDOM.close()\n }\n}\n\nfunction hasRenderableContent(node: JSONContent): boolean {\n if (node.type === 'text') return Boolean(node.text)\n if (Array.isArray(node.content) && node.content.some(hasRenderableContent)) return true\n return Boolean(node.type && node.type !== 'doc' && node.type !== 'paragraph' && node.type !== 'text')\n}\n\nexport function yDocToContent(ydoc: Y.Doc): { html: string; text: string } | null {\n try {\n if (ydoc.getXmlFragment(COLLAB_FRAGMENT_FIELD).length === 0) {\n return { html: '', text: '' }\n }\n\n const json = TiptapTransformer.fromYdoc(ydoc, COLLAB_FRAGMENT_FIELD) as JSONContent | null\n if (!json || !hasRenderableContent(json)) {\n return { html: '', text: '' }\n }\n\n const html = generateHtml(json, getDocumentEditorExtensions())\n const text = deriveContentTextFromHtml(html)\n assertDocumentContentResourceLimits({ contentHtml: html, contentText: text })\n return { html, text }\n } catch (error) {\n if (isCrudHttpError(error) && error.status === 413) throw error\n return null\n }\n}\n\nexport function htmlToYDoc(html: string): Y.Doc {\n assertDocumentContentResourceLimits({ contentHtml: html })\n if (!html.trim()) return new Y.Doc()\n\n const extensions = getDocumentEditorExtensions()\n const json = generateJson(html, extensions)\n return TiptapTransformer.toYdoc(json, COLLAB_FRAGMENT_FIELD, extensions)\n}\n\nexport type MaterializedDocumentHtml = {\n yjsState: Buffer\n html: string\n text: string\n}\n\n/**\n * Convert authored HTML through the same editor/Yjs pipeline used by persisted\n * document content. Callers that preview content before writing it must use the\n * returned HTML too, otherwise the preview can differ from the canonical markup\n * users receive after the document is created.\n */\nexport function materializeDocumentHtml(html: string): MaterializedDocumentHtml | null {\n try {\n const ydoc = htmlToYDoc(html)\n const content = yDocToContent(ydoc)\n if (!content) return null\n const result = {\n yjsState: Buffer.from(Y.encodeStateAsUpdate(ydoc)),\n html: content.html,\n text: content.text,\n }\n assertDocumentContentResourceLimits({\n yjsState: result.yjsState,\n contentHtml: result.html,\n contentText: result.text,\n })\n return result\n } catch (error) {\n if (isCrudHttpError(error) && error.status === 413) throw error\n return null\n }\n}\n\n/**\n * Replace REST-authored content as a Yjs update derived from the current CRDT\n * epoch. Keeping the old structs as deleted tombstones means a retained client\n * can merge the replacement without resurrecting or duplicating stale text.\n */\nexport function materializeDocumentContentReplacement(\n existingState: Buffer | Uint8Array | null | undefined,\n html: string,\n): MaterializedDocumentHtml | null {\n try {\n assertDocumentContentResourceLimits({ yjsState: existingState, contentHtml: html })\n const replacement = htmlToYDoc(html)\n const ydoc = new Y.Doc()\n if (existingState && existingState.length > 0) {\n Y.applyUpdate(ydoc, new Uint8Array(existingState))\n }\n\n const target = ydoc.getXmlFragment(COLLAB_FRAGMENT_FIELD)\n const replacementNodes = replacement\n .getXmlFragment(COLLAB_FRAGMENT_FIELD)\n .toArray()\n .filter((node): node is Y.XmlElement | Y.XmlText => (\n node instanceof Y.XmlElement || node instanceof Y.XmlText\n ))\n .map((node) => node.clone())\n ydoc.transact(() => {\n if (target.length > 0) target.delete(0, target.length)\n if (replacementNodes.length > 0) target.insert(0, replacementNodes)\n })\n const content = yDocToContent(ydoc)\n if (!content) return null\n const result = {\n yjsState: Buffer.from(Y.encodeStateAsUpdate(ydoc)),\n html: content.html,\n text: content.text,\n }\n assertDocumentContentResourceLimits({\n yjsState: result.yjsState,\n contentHtml: result.html,\n contentText: result.text,\n })\n return result\n } catch (error) {\n if (isCrudHttpError(error) && error.status === 413) throw error\n return null\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/documents",
3
- "version": "0.10.1-develop.7809.1.ea24461aeb",
3
+ "version": "0.10.1-develop.7811.1.66050c1838",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -75,33 +75,32 @@
75
75
  "@hocuspocus/server": "^4.4.0",
76
76
  "@hocuspocus/transformer": "^4.3.0",
77
77
  "@mikro-orm/core": "^7.1.14",
78
- "@open-mercato/events": "0.10.1-develop.7809.1.ea24461aeb",
79
- "@open-mercato/ui": "0.10.1-develop.7809.1.ea24461aeb",
80
- "@tiptap/core": "^3.32.2",
81
- "@tiptap/extension-bubble-menu": "^3.32.2",
82
- "@tiptap/extension-collaboration": "^3.32.2",
83
- "@tiptap/extension-collaboration-caret": "^3.32.2",
84
- "@tiptap/extension-highlight": "^3.32.2",
85
- "@tiptap/extension-image": "^3.32.2",
86
- "@tiptap/extension-link": "^3.32.2",
87
- "@tiptap/extension-list": "^3.32.2",
88
- "@tiptap/extension-placeholder": "^3.32.2",
89
- "@tiptap/extension-table": "^3.32.2",
90
- "@tiptap/extension-table-cell": "^3.32.2",
91
- "@tiptap/extension-table-header": "^3.32.2",
92
- "@tiptap/extension-table-row": "^3.32.2",
93
- "@tiptap/extension-task-item": "^3.32.2",
94
- "@tiptap/extension-task-list": "^3.32.2",
95
- "@tiptap/extension-text-align": "^3.32.2",
96
- "@tiptap/extension-text-style": "^3.32.2",
97
- "@tiptap/extensions": "^3.32.2",
98
- "@tiptap/html": "^3.32.2",
99
- "@tiptap/pm": "^3.32.2",
100
- "@tiptap/react": "^3.32.2",
101
- "@tiptap/starter-kit": "^3.32.2",
102
- "@tiptap/suggestion": "^3.32.2",
103
- "@tiptap/y-tiptap": "^3.0.7",
104
- "happy-dom": "^21.0.4",
78
+ "@open-mercato/events": "0.10.1-develop.7811.1.66050c1838",
79
+ "@open-mercato/ui": "0.10.1-develop.7811.1.66050c1838",
80
+ "@tiptap/core": "3.32.0",
81
+ "@tiptap/extension-bubble-menu": "3.32.0",
82
+ "@tiptap/extension-collaboration": "3.32.0",
83
+ "@tiptap/extension-collaboration-caret": "3.32.0",
84
+ "@tiptap/extension-highlight": "3.32.0",
85
+ "@tiptap/extension-image": "3.32.0",
86
+ "@tiptap/extension-link": "3.32.0",
87
+ "@tiptap/extension-list": "3.32.0",
88
+ "@tiptap/extension-placeholder": "3.32.0",
89
+ "@tiptap/extension-table": "3.32.0",
90
+ "@tiptap/extension-table-cell": "3.32.0",
91
+ "@tiptap/extension-table-header": "3.32.0",
92
+ "@tiptap/extension-table-row": "3.32.0",
93
+ "@tiptap/extension-task-item": "3.32.0",
94
+ "@tiptap/extension-task-list": "3.32.0",
95
+ "@tiptap/extension-text-align": "3.32.0",
96
+ "@tiptap/extension-text-style": "3.32.0",
97
+ "@tiptap/extensions": "3.32.0",
98
+ "@tiptap/pm": "3.32.0",
99
+ "@tiptap/react": "3.32.0",
100
+ "@tiptap/starter-kit": "3.32.0",
101
+ "@tiptap/suggestion": "3.32.0",
102
+ "@tiptap/y-tiptap": "3.0.7",
103
+ "happy-dom": "21.0.4",
105
104
  "jszip": "^3.10.1",
106
105
  "prosemirror-model": "^1.25.12",
107
106
  "prosemirror-state": "^1.4.4",
@@ -113,13 +112,13 @@
113
112
  },
114
113
  "peerDependencies": {
115
114
  "@mikro-orm/postgresql": "^7.0.14",
116
- "@open-mercato/shared": "0.10.1-develop.7809.1.ea24461aeb",
115
+ "@open-mercato/shared": "0.10.1-develop.7811.1.66050c1838",
117
116
  "react": "^19.0.0",
118
117
  "react-dom": "^19.0.0"
119
118
  },
120
119
  "devDependencies": {
121
- "@open-mercato/core": "0.10.1-develop.7809.1.ea24461aeb",
122
- "@open-mercato/shared": "0.10.1-develop.7809.1.ea24461aeb",
120
+ "@open-mercato/core": "0.10.1-develop.7811.1.66050c1838",
121
+ "@open-mercato/shared": "0.10.1-develop.7811.1.66050c1838",
123
122
  "@types/jest": "^30.0.0",
124
123
  "@types/pg": "^8.23.1",
125
124
  "@types/react": "^19.2.18",
@@ -1,22 +1,75 @@
1
1
  import { createRequire } from 'node:module'
2
- import type { JSONContent } from '@tiptap/core'
3
2
  import { TiptapTransformer } from '@hocuspocus/transformer'
4
3
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
4
+ import { getSchema, type Extensions, type JSONContent } from '@tiptap/core'
5
+ import {
6
+ DOMParser as ProseMirrorDOMParser,
7
+ DOMSerializer,
8
+ Node as ProseMirrorNode,
9
+ type ParseOptions,
10
+ } from '@tiptap/pm/model'
5
11
  import * as Y from 'yjs'
6
12
  import { deriveContentTextFromHtml } from './contentService'
7
13
  import { COLLAB_FRAGMENT_FIELD, getDocumentEditorExtensions } from './editorConfig'
8
14
  import { assertDocumentContentResourceLimits } from './resourceLimits'
9
15
 
10
- type TiptapHtmlServer = typeof import('@tiptap/html/server')
16
+ type HappyDomModule = typeof import('happy-dom')
17
+ type HappyDomWindow = InstanceType<HappyDomModule['Window']>
11
18
 
12
19
  const requireFromHere = createRequire(import.meta.url)
13
- let tiptapHtmlServer: TiptapHtmlServer | null = null
20
+ let happyDomModule: HappyDomModule | null = null
14
21
 
15
- function getTiptapHtmlServer(): TiptapHtmlServer {
16
- if (!tiptapHtmlServer) {
17
- tiptapHtmlServer = requireFromHere('@tiptap/html/server') as TiptapHtmlServer
22
+ function createHtmlWindow(): HappyDomWindow {
23
+ if (!happyDomModule) {
24
+ happyDomModule = requireFromHere('happy-dom') as HappyDomModule
25
+ }
26
+
27
+ return new happyDomModule.Window({
28
+ settings: {
29
+ disableJavaScriptEvaluation: true,
30
+ disableJavaScriptFileLoading: true,
31
+ disableCSSFileLoading: true,
32
+ disableIframePageLoading: true,
33
+ disableComputedStyleRendering: true,
34
+ },
35
+ })
36
+ }
37
+
38
+ function generateHtml(doc: JSONContent, extensions: Extensions): string {
39
+ const schema = getSchema(extensions)
40
+ const documentNode = ProseMirrorNode.fromJSON(schema, doc)
41
+ const localWindow = createHtmlWindow()
42
+
43
+ try {
44
+ const wrapper = localWindow.document.createElement('div')
45
+ DOMSerializer.fromSchema(schema).serializeFragment(
46
+ documentNode.content,
47
+ { document: localWindow.document as unknown as Document },
48
+ wrapper as unknown as HTMLElement,
49
+ )
50
+ return wrapper.innerHTML
51
+ } finally {
52
+ localWindow.happyDOM.abort()
53
+ localWindow.happyDOM.close()
54
+ }
55
+ }
56
+
57
+ function generateJson(html: string, extensions: Extensions, options?: ParseOptions): JSONContent {
58
+ const localWindow = createHtmlWindow()
59
+
60
+ try {
61
+ const parsedDocument = new localWindow.DOMParser().parseFromString(
62
+ `<!DOCTYPE html><html><body>${html}</body></html>`,
63
+ 'text/html',
64
+ )
65
+ const schema = getSchema(extensions)
66
+ return ProseMirrorDOMParser.fromSchema(schema)
67
+ .parse(parsedDocument.body as unknown as Node, options)
68
+ .toJSON()
69
+ } finally {
70
+ localWindow.happyDOM.abort()
71
+ localWindow.happyDOM.close()
18
72
  }
19
- return tiptapHtmlServer
20
73
  }
21
74
 
22
75
  function hasRenderableContent(node: JSONContent): boolean {
@@ -36,8 +89,7 @@ export function yDocToContent(ydoc: Y.Doc): { html: string; text: string } | nul
36
89
  return { html: '', text: '' }
37
90
  }
38
91
 
39
- const { generateHTML } = getTiptapHtmlServer()
40
- const html = generateHTML(json, getDocumentEditorExtensions())
92
+ const html = generateHtml(json, getDocumentEditorExtensions())
41
93
  const text = deriveContentTextFromHtml(html)
42
94
  assertDocumentContentResourceLimits({ contentHtml: html, contentText: text })
43
95
  return { html, text }
@@ -52,8 +104,7 @@ export function htmlToYDoc(html: string): Y.Doc {
52
104
  if (!html.trim()) return new Y.Doc()
53
105
 
54
106
  const extensions = getDocumentEditorExtensions()
55
- const { generateJSON } = getTiptapHtmlServer()
56
- const json = generateJSON(html, extensions) as JSONContent
107
+ const json = generateJson(html, extensions)
57
108
  return TiptapTransformer.toYdoc(json, COLLAB_FRAGMENT_FIELD, extensions)
58
109
  }
59
110