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

This commit is contained in:
upstream-mirror
2026-09-15 21:11:02 +05:00
commit 083a46e93b
2115 changed files with 245958 additions and 0 deletions

View 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>`,
)
})
})

View 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)
}

View 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)
})
})

View 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()
}

View 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
}

View File

@@ -0,0 +1,2 @@
export * from './generateHTML.js'
export * from './generateJSON.js'

View 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)
}

View 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
}

View 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
}

View File

@@ -0,0 +1,2 @@
export * from './generateHTML.js'
export * from './generateJSON.js'