snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
This commit is contained in:
248
packages/html/src/generateHTML.test.ts
Normal file
248
packages/html/src/generateHTML.test.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { TextStyle } from '@tiptap/extension-text-style'
|
||||
import Youtube from '@tiptap/extension-youtube'
|
||||
import { generateHTML } from './generateHTML.js'
|
||||
import { generateJSON } from './generateJSON.js'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
describe('generateHTML', () => {
|
||||
it('generate HTML from JSON without an editor instance', () => {
|
||||
const json = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Example Text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const html = generateHTML(json, [Document, Paragraph, Text])
|
||||
|
||||
expect(html).toBe('<p>Example Text</p>')
|
||||
})
|
||||
|
||||
it('can convert from & to html', async () => {
|
||||
const extensions = [Document, Paragraph, Text, Youtube]
|
||||
const html = `<p>Tiptap now supports YouTube embeds! Awesome!</p>
|
||||
<div data-youtube-video>
|
||||
<iframe src="https://www.youtube.com/watch?v=cqHqLQgVCgY"></iframe>
|
||||
</div>`
|
||||
const json = generateJSON(html, extensions)
|
||||
|
||||
expect(json).toEqual({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Tiptap now supports YouTube embeds! Awesome!',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'youtube',
|
||||
attrs: {
|
||||
src: 'https://www.youtube.com/watch?v=cqHqLQgVCgY',
|
||||
start: 0,
|
||||
width: 640,
|
||||
height: 480,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(generateHTML(json, extensions)).toBe(
|
||||
'<p>Tiptap now supports YouTube embeds! Awesome!</p><div data-youtube-video=""><iframe width="640" height="480" allowfullscreen="true" autoplay="false" disablekbcontrols="false" enableiframeapi="false" endtime="0" ivloadpolicy="0" loop="false" modestbranding="false" origin="" playlist="" rel="1" src="https://www.youtube.com/embed/cqHqLQgVCgY?rel=1" start="0"></iframe></div>',
|
||||
)
|
||||
})
|
||||
|
||||
it('can convert from & to HTML with a complex schema', async () => {
|
||||
const extensions = [StarterKit, TextStyle]
|
||||
const html = `
|
||||
<h2>
|
||||
Hi there,
|
||||
</h2>
|
||||
<p>
|
||||
this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
That’s a bullet list with one …
|
||||
</li>
|
||||
<li>
|
||||
… or two list items.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
|
||||
</p>
|
||||
<pre><code class="language-css">body {
|
||||
display: none;
|
||||
}</code></pre>
|
||||
<p>
|
||||
I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too.
|
||||
</p>
|
||||
<blockquote>
|
||||
Wow, that’s amazing. Good work, boy! 👏
|
||||
<br />
|
||||
— Mom
|
||||
</blockquote>`
|
||||
const json = generateJSON(html, extensions)
|
||||
|
||||
const expected = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: {
|
||||
level: 2,
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hi there,',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'this is a ',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'italic',
|
||||
},
|
||||
],
|
||||
text: 'basic',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ' example of ',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
marks: [
|
||||
{
|
||||
type: 'bold',
|
||||
},
|
||||
],
|
||||
text: 'Tiptap',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: '. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'bulletList',
|
||||
content: [
|
||||
{
|
||||
type: 'listItem',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'That’s a bullet list with one …',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'listItem',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: '… or two list items.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'codeBlock',
|
||||
attrs: {
|
||||
language: 'css',
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'body {\n display: none;\n}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'blockquote',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Wow, that’s amazing. Good work, boy! 👏 ',
|
||||
},
|
||||
{
|
||||
type: 'hardBreak',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: '— Mom',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
expect(json).toEqual(expected)
|
||||
|
||||
expect(generateHTML(json, extensions)).toBe(
|
||||
`<h2>Hi there,</h2><p>this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:</p><ul><li><p>That’s a bullet list with one …</p></li><li><p>… or two list items.</p></li></ul><p>Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:</p><pre><code class="language-css">body {
|
||||
display: none;
|
||||
}</code></pre><p>I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too.</p><blockquote><p>Wow, that’s amazing. Good work, boy! 👏 <br>— Mom</p></blockquote>`,
|
||||
)
|
||||
})
|
||||
})
|
||||
41
packages/html/src/generateHTML.ts
Normal file
41
packages/html/src/generateHTML.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Extensions, JSONContent } from '@tiptap/core'
|
||||
import { getSchema } from '@tiptap/core'
|
||||
import { Node as PMNode } from '@tiptap/pm/model'
|
||||
|
||||
import { getHTMLFromFragment } from './getHTMLFromFragment.js'
|
||||
|
||||
/**
|
||||
* Generates HTML from a ProseMirror JSON content object.
|
||||
* @param doc - The ProseMirror JSON content object.
|
||||
* @param extensions - The Tiptap extensions used to build the schema.
|
||||
* @returns The generated HTML string.
|
||||
* @example
|
||||
* const doc = {
|
||||
* type: 'doc',
|
||||
* content: [
|
||||
* {
|
||||
* type: 'paragraph',
|
||||
* content: [
|
||||
* {
|
||||
* type: 'text',
|
||||
* text: 'Hello world!'
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* const extensions = [...]
|
||||
* const html = generateHTML(doc, extensions)
|
||||
*/
|
||||
export function generateHTML(doc: JSONContent, extensions: Extensions): string {
|
||||
if (typeof window === 'undefined') {
|
||||
throw new Error(
|
||||
'generateHTML can only be used in a browser environment\nIf you want to use this in a Node environment, use the `@tiptap/html/server` import instead.',
|
||||
)
|
||||
}
|
||||
|
||||
const schema = getSchema(extensions)
|
||||
const contentNode = PMNode.fromJSON(schema, doc)
|
||||
|
||||
return getHTMLFromFragment(contentNode, schema)
|
||||
}
|
||||
82
packages/html/src/generateJSON.test.ts
Normal file
82
packages/html/src/generateJSON.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import TextAlign from '@tiptap/extension-text-align'
|
||||
import { generateJSON } from './generateJSON.js'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
describe('generateJSON', () => {
|
||||
it('generate JSON from HTML without an editor instance', () => {
|
||||
const html = '<p>Example Text</p>'
|
||||
|
||||
const json = generateJSON(html, [Document, Paragraph, Text])
|
||||
|
||||
expect(JSON.stringify(json)).toBe(
|
||||
JSON.stringify({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Example Text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// issue: https://github.com/ueberdosis/tiptap/issues/1601
|
||||
it('generate JSON with style attributes', () => {
|
||||
const html = '<p style="text-align: center;">Example Text</p>'
|
||||
|
||||
const json = generateJSON(html, [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
TextAlign.configure({ types: ['paragraph'] }),
|
||||
])
|
||||
|
||||
expect(JSON.stringify(json)).toBe(
|
||||
JSON.stringify({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
attrs: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Example Text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// issue: https://github.com/ueberdosis/tiptap/issues/6968
|
||||
it('should not crash when HTML contains link, script, or style tags with resource references', () => {
|
||||
const html = `
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
<link rel="preload" href="/assets/app.js" as="script">
|
||||
<script src="/assets/app.js"></script>
|
||||
<style>body { color: red; }</style>
|
||||
<p>Content after resource tags</p>
|
||||
`
|
||||
|
||||
// This should not throw "TypeError: Invalid URL (ERR_INVALID_URL)"
|
||||
const json = generateJSON(html, [Document, Paragraph, Text])
|
||||
|
||||
expect(json.type).toBe('doc')
|
||||
expect(json.content).toBeDefined()
|
||||
// The paragraph content should be parsed correctly
|
||||
expect(json.content.some((node: any) => node.type === 'paragraph')).toBe(true)
|
||||
})
|
||||
})
|
||||
39
packages/html/src/generateJSON.ts
Normal file
39
packages/html/src/generateJSON.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Extensions } from '@tiptap/core'
|
||||
import { getSchema } from '@tiptap/core'
|
||||
import type { ParseOptions } from '@tiptap/pm/model'
|
||||
import { DOMParser } from '@tiptap/pm/model'
|
||||
|
||||
/**
|
||||
* Generates a JSON object from the given HTML string and converts it into a Prosemirror node with content.
|
||||
* @param {string} html - The HTML string to be converted into a Prosemirror node.
|
||||
* @param {Extensions} extensions - The extensions to be used for generating the schema.
|
||||
* @param {ParseOptions} options - The options to be supplied to the parser.
|
||||
* @returns {Record<string, any>} - The generated JSON object.
|
||||
* @example
|
||||
* const html = '<p>Hello, world!</p>'
|
||||
* const extensions = [...]
|
||||
* const json = generateJSON(html, extensions)
|
||||
* console.log(json) // { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }] }] }
|
||||
*/
|
||||
export function generateJSON(
|
||||
html: string,
|
||||
extensions: Extensions,
|
||||
options?: ParseOptions,
|
||||
): Record<string, any> {
|
||||
if (typeof window === 'undefined') {
|
||||
throw new Error(
|
||||
'generateJSON can only be used in a browser environment\nIf you want to use this in a Node environment, use the `@tiptap/html/server` import instead.',
|
||||
)
|
||||
}
|
||||
|
||||
const schema = getSchema(extensions)
|
||||
const doc: Document = new window.DOMParser().parseFromString(html, 'text/html')
|
||||
|
||||
if (!doc) {
|
||||
throw new Error('Failed to parse HTML string')
|
||||
}
|
||||
|
||||
return DOMParser.fromSchema(schema)
|
||||
.parse(doc.body as Node, options)
|
||||
.toJSON()
|
||||
}
|
||||
49
packages/html/src/getHTMLFromFragment.ts
Normal file
49
packages/html/src/getHTMLFromFragment.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Node as PMNode, Schema } from '@tiptap/pm/model'
|
||||
import { DOMSerializer } from '@tiptap/pm/model'
|
||||
|
||||
/**
|
||||
* Returns the HTML string representation of a given document node.
|
||||
*
|
||||
* @param doc - The document node to serialize.
|
||||
* @param schema - The Prosemirror schema to use for serialization.
|
||||
* @returns The HTML string representation of the document fragment.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const html = getHTMLFromFragment(doc, schema)
|
||||
* ```
|
||||
*/
|
||||
export function getHTMLFromFragment(
|
||||
doc: PMNode,
|
||||
schema: Schema,
|
||||
options?: { document?: Document },
|
||||
): string {
|
||||
if (typeof window === 'undefined') {
|
||||
throw new Error(
|
||||
'getHTMLFromFragment can only be used in a browser environment\nIf you want to use this in a Node environment, use the `@tiptap/html/server` import instead.',
|
||||
)
|
||||
}
|
||||
|
||||
if (options?.document) {
|
||||
const wrap = options.document.createElement('div')
|
||||
|
||||
DOMSerializer.fromSchema(schema).serializeFragment(
|
||||
doc.content,
|
||||
{ document: options.document },
|
||||
wrap,
|
||||
)
|
||||
return wrap.innerHTML
|
||||
}
|
||||
|
||||
const wrap = window.document.createElement('div')
|
||||
|
||||
DOMSerializer.fromSchema(schema).serializeFragment(
|
||||
doc.content,
|
||||
{
|
||||
document: window.document as unknown as Document,
|
||||
},
|
||||
wrap,
|
||||
)
|
||||
|
||||
return wrap.innerHTML
|
||||
}
|
||||
2
packages/html/src/index.ts
Normal file
2
packages/html/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './generateHTML.js'
|
||||
export * from './generateJSON.js'
|
||||
34
packages/html/src/server/generateHTML.ts
Normal file
34
packages/html/src/server/generateHTML.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { type Extensions, type JSONContent, getSchema } from '@tiptap/core'
|
||||
import { Node as PMNode } from '@tiptap/pm/model'
|
||||
|
||||
import { getHTMLFromFragment } from './getHTMLFromFragment.js'
|
||||
|
||||
/**
|
||||
* This function generates HTML from a ProseMirror JSON content object.
|
||||
*
|
||||
* @remarks **Important**: This function requires `happy-dom` to be installed in your project.
|
||||
* @param doc - The ProseMirror JSON content object.
|
||||
* @param extensions - The Tiptap extensions used to build the schema.
|
||||
* @returns The generated HTML string.
|
||||
* @example
|
||||
* ```js
|
||||
* const html = generateHTML(doc, extensions)
|
||||
* console.log(html)
|
||||
* ```
|
||||
*/
|
||||
export function generateHTML(doc: JSONContent, extensions: Extensions): string {
|
||||
// Use positive Node.js detection to allow for jsdom/happy-dom environments in tests
|
||||
const isNode =
|
||||
typeof process !== 'undefined' && process.versions != null && process.versions.node != null
|
||||
|
||||
if (!isNode) {
|
||||
throw new Error(
|
||||
'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.',
|
||||
)
|
||||
}
|
||||
|
||||
const schema = getSchema(extensions)
|
||||
const contentNode = PMNode.fromJSON(schema, doc)
|
||||
|
||||
return getHTMLFromFragment(contentNode, schema)
|
||||
}
|
||||
67
packages/html/src/server/generateJSON.ts
Normal file
67
packages/html/src/server/generateJSON.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { Extensions } from '@tiptap/core'
|
||||
import { getSchema } from '@tiptap/core'
|
||||
import { type ParseOptions, DOMParser as PMDOMParser } from '@tiptap/pm/model'
|
||||
import { Window } from 'happy-dom'
|
||||
|
||||
/**
|
||||
* Generates a JSON object from the given HTML string and converts it into a Prosemirror node with content.
|
||||
* @remarks **Important**: This function requires `happy-dom` to be installed in your project.
|
||||
* @param {string} html - The HTML string to be converted into a Prosemirror node.
|
||||
* @param {Extensions} extensions - The extensions to be used for generating the schema.
|
||||
* @param {ParseOptions} options - The options to be supplied to the parser.
|
||||
* @returns {Promise<Record<string, any>>} - A promise with the generated JSON object.
|
||||
* @example
|
||||
* const html = '<p>Hello, world!</p>'
|
||||
* const extensions = [...]
|
||||
* const json = generateJSON(html, extensions)
|
||||
* console.log(json) // { type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello, world!' }] }] }
|
||||
*/
|
||||
export function generateJSON(
|
||||
html: string,
|
||||
extensions: Extensions,
|
||||
options?: ParseOptions,
|
||||
): Record<string, any> {
|
||||
// Use positive Node.js detection to allow for jsdom/happy-dom environments in tests
|
||||
const isNode =
|
||||
typeof process !== 'undefined' && process.versions != null && process.versions.node != null
|
||||
|
||||
if (!isNode) {
|
||||
throw new Error(
|
||||
'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.',
|
||||
)
|
||||
}
|
||||
|
||||
const localWindow = new Window({
|
||||
settings: {
|
||||
disableJavaScriptEvaluation: true,
|
||||
disableJavaScriptFileLoading: true,
|
||||
disableCSSFileLoading: true,
|
||||
disableIframePageLoading: true,
|
||||
disableComputedStyleRendering: true,
|
||||
},
|
||||
})
|
||||
const localDOMParser = new localWindow.DOMParser()
|
||||
let result: Record<string, any>
|
||||
|
||||
try {
|
||||
const schema = getSchema(extensions)
|
||||
let doc: ReturnType<typeof localDOMParser.parseFromString> | null = null
|
||||
|
||||
const htmlString = `<!DOCTYPE html><html><body>${html}</body></html>`
|
||||
doc = localDOMParser.parseFromString(htmlString, 'text/html')
|
||||
|
||||
if (!doc) {
|
||||
throw new Error('Failed to parse HTML string')
|
||||
}
|
||||
|
||||
result = PMDOMParser.fromSchema(schema)
|
||||
.parse(doc.body as unknown as Node, options)
|
||||
.toJSON()
|
||||
} finally {
|
||||
// clean up happy-dom to avoid memory leaks
|
||||
localWindow.happyDOM.abort()
|
||||
localWindow.happyDOM.close()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
64
packages/html/src/server/getHTMLFromFragment.ts
Normal file
64
packages/html/src/server/getHTMLFromFragment.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { Node as PMNode, Schema } from '@tiptap/pm/model'
|
||||
import { DOMSerializer } from '@tiptap/pm/model'
|
||||
import { Window } from 'happy-dom'
|
||||
|
||||
/**
|
||||
* Returns the HTML string representation of a given document node.
|
||||
*
|
||||
* @remarks **Important**: This function requires `happy-dom` to be installed in your project.
|
||||
* @param doc - The document node to serialize.
|
||||
* @param schema - The Prosemirror schema to use for serialization.
|
||||
* @returns A promise containing the HTML string representation of the document fragment.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const html = getHTMLFromFragment(doc, schema)
|
||||
* ```
|
||||
*/
|
||||
export function getHTMLFromFragment(
|
||||
doc: PMNode,
|
||||
schema: Schema,
|
||||
options?: { document?: Document },
|
||||
): string {
|
||||
if (options?.document) {
|
||||
const wrap = options.document.createElement('div')
|
||||
|
||||
DOMSerializer.fromSchema(schema).serializeFragment(
|
||||
doc.content,
|
||||
{ document: options.document },
|
||||
wrap,
|
||||
)
|
||||
return wrap.innerHTML
|
||||
}
|
||||
|
||||
const localWindow = new Window({
|
||||
settings: {
|
||||
disableJavaScriptEvaluation: true,
|
||||
disableJavaScriptFileLoading: true,
|
||||
disableCSSFileLoading: true,
|
||||
disableIframePageLoading: true,
|
||||
disableComputedStyleRendering: true,
|
||||
},
|
||||
})
|
||||
let result: string
|
||||
|
||||
try {
|
||||
const wrap = localWindow.document.createElement('div')
|
||||
|
||||
DOMSerializer.fromSchema(schema).serializeFragment(
|
||||
doc.content,
|
||||
{
|
||||
document: localWindow.document as unknown as Document,
|
||||
},
|
||||
wrap,
|
||||
)
|
||||
|
||||
result = wrap.innerHTML
|
||||
} finally {
|
||||
// clean up happy-dom to avoid memory leaks
|
||||
localWindow.happyDOM.abort()
|
||||
localWindow.happyDOM.close()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
2
packages/html/src/server/index.ts
Normal file
2
packages/html/src/server/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './generateHTML.js'
|
||||
export * from './generateJSON.js'
|
||||
Reference in New Issue
Block a user