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:
2512
packages/html/CHANGELOG.md
Normal file
2512
packages/html/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
18
packages/html/README.md
Normal file
18
packages/html/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# @tiptap/html
|
||||
|
||||
[](https://www.npmjs.com/package/@tiptap/html)
|
||||
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
||||
[](https://www.npmjs.com/package/@tiptap/html)
|
||||
[](https://github.com/sponsors/ueberdosis)
|
||||
|
||||
## Introduction
|
||||
|
||||
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as _New York Times_, _The Guardian_ or _Atlassian_.
|
||||
|
||||
## Official Documentation
|
||||
|
||||
Documentation can be found on the [Tiptap website](https://tiptap.dev).
|
||||
|
||||
## License
|
||||
|
||||
Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).
|
||||
67
packages/html/package.json
Normal file
67
packages/html/package.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "@tiptap/html",
|
||||
"version": "3.30.3",
|
||||
"description": "utility package to render tiptap JSON as HTML",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
"tiptap utility"
|
||||
],
|
||||
"homepage": "https://tiptap.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ueberdosis/tiptap/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ueberdosis/tiptap",
|
||||
"directory": "packages/html"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": {
|
||||
"import": "./dist/index.d.ts",
|
||||
"require": "./dist/index.d.cts"
|
||||
},
|
||||
"import": {
|
||||
"browser": "./dist/index.js",
|
||||
"node": "./dist/server/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./server": {
|
||||
"types": {
|
||||
"import": "./dist/server/index.d.ts",
|
||||
"require": "./dist/server/index.d.cts"
|
||||
},
|
||||
"import": "./dist/server/index.js",
|
||||
"require": "./dist/server/index.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vp pack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tiptap/core": "workspace:^",
|
||||
"@tiptap/pm": "workspace:^",
|
||||
"@types/node": "^22.0.0",
|
||||
"happy-dom": "^20.8.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "workspace:*",
|
||||
"@tiptap/pm": "workspace:*",
|
||||
"happy-dom": "^20.8.9"
|
||||
}
|
||||
}
|
||||
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'
|
||||
58
packages/html/tests/integration/serverWithJsdom.test.ts
Normal file
58
packages/html/tests/integration/serverWithJsdom.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
import { generateHTML, generateJSON } from '../../src/server/index.js'
|
||||
|
||||
describe('server exports with jsdom/happy-dom environment (issue #6951)', () => {
|
||||
it('should have global window defined (test environment check)', () => {
|
||||
expect(typeof window).toBe('object')
|
||||
expect(window).toBeDefined()
|
||||
})
|
||||
|
||||
it('generateJSON should work even when global window is defined', () => {
|
||||
const html = '<p>Hello, world!</p>'
|
||||
const extensions = [Document, Paragraph, Text]
|
||||
|
||||
const json = generateJSON(html, extensions)
|
||||
|
||||
expect(json).toEqual({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello, world!',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('generateHTML should work even when global window is defined', () => {
|
||||
const json = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello, world!',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
const extensions = [Document, Paragraph, Text]
|
||||
|
||||
// This should NOT throw "generateHTML can only be used in a Node environment"
|
||||
const html = generateHTML(json, extensions)
|
||||
|
||||
expect(html).toBe('<p>Hello, world!</p>')
|
||||
})
|
||||
})
|
||||
16
packages/html/vite.config.ts
Normal file
16
packages/html/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vite-plus'
|
||||
import { tsupCompatibleExtensions } from '../../pack.config.mjs'
|
||||
|
||||
export default defineConfig({
|
||||
pack: {
|
||||
entry: ['src/index.ts', 'src/server/index.ts'],
|
||||
tsconfig: '../../tsconfig.build.json',
|
||||
outDir: 'dist',
|
||||
dts: { sourcemap: true },
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
target: 'es2019',
|
||||
format: ['esm', 'cjs'],
|
||||
outExtensions: tsupCompatibleExtensions,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user