@tiptap/static-renderer 3.0.0-beta.0

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.
Files changed (59) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +18 -0
  3. package/dist/index.cjs +629 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +327 -0
  6. package/dist/index.d.ts +327 -0
  7. package/dist/index.js +584 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/json/html-string/index.cjs +111 -0
  10. package/dist/json/html-string/index.cjs.map +1 -0
  11. package/dist/json/html-string/index.d.cts +201 -0
  12. package/dist/json/html-string/index.d.ts +201 -0
  13. package/dist/json/html-string/index.js +81 -0
  14. package/dist/json/html-string/index.js.map +1 -0
  15. package/dist/json/react/index.cjs +117 -0
  16. package/dist/json/react/index.cjs.map +1 -0
  17. package/dist/json/react/index.d.cts +190 -0
  18. package/dist/json/react/index.d.ts +190 -0
  19. package/dist/json/react/index.js +79 -0
  20. package/dist/json/react/index.js.map +1 -0
  21. package/dist/json/renderer.cjs +89 -0
  22. package/dist/json/renderer.cjs.map +1 -0
  23. package/dist/json/renderer.d.cts +166 -0
  24. package/dist/json/renderer.d.ts +166 -0
  25. package/dist/json/renderer.js +64 -0
  26. package/dist/json/renderer.js.map +1 -0
  27. package/dist/pm/html-string/index.cjs +344 -0
  28. package/dist/pm/html-string/index.cjs.map +1 -0
  29. package/dist/pm/html-string/index.d.cts +175 -0
  30. package/dist/pm/html-string/index.d.ts +175 -0
  31. package/dist/pm/html-string/index.js +317 -0
  32. package/dist/pm/html-string/index.js.map +1 -0
  33. package/dist/pm/markdown/index.cjs +473 -0
  34. package/dist/pm/markdown/index.cjs.map +1 -0
  35. package/dist/pm/markdown/index.d.cts +153 -0
  36. package/dist/pm/markdown/index.d.ts +153 -0
  37. package/dist/pm/markdown/index.js +449 -0
  38. package/dist/pm/markdown/index.js.map +1 -0
  39. package/dist/pm/react/index.cjs +399 -0
  40. package/dist/pm/react/index.cjs.map +1 -0
  41. package/dist/pm/react/index.d.cts +163 -0
  42. package/dist/pm/react/index.d.ts +163 -0
  43. package/dist/pm/react/index.js +364 -0
  44. package/dist/pm/react/index.js.map +1 -0
  45. package/package.json +101 -0
  46. package/src/helpers.ts +54 -0
  47. package/src/index.ts +6 -0
  48. package/src/json/html-string/index.ts +2 -0
  49. package/src/json/html-string/string.ts +49 -0
  50. package/src/json/react/index.ts +2 -0
  51. package/src/json/react/react.ts +33 -0
  52. package/src/json/renderer.ts +241 -0
  53. package/src/pm/extensionRenderer.ts +215 -0
  54. package/src/pm/html-string/html-string.ts +105 -0
  55. package/src/pm/html-string/index.ts +2 -0
  56. package/src/pm/markdown/index.ts +2 -0
  57. package/src/pm/markdown/markdown.ts +142 -0
  58. package/src/pm/react/index.ts +2 -0
  59. package/src/pm/react/react.ts +152 -0
@@ -0,0 +1,215 @@
1
+ /* eslint-disable no-plusplus */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+
4
+ import type {
5
+ ExtensionAttribute,
6
+ Extensions,
7
+ JSONContent,
8
+ Mark as MarkExtension,
9
+ MarkConfig,
10
+ Node as NodeExtension,
11
+ NodeConfig,
12
+ } from '@tiptap/core'
13
+ import {
14
+ getAttributesFromExtensions,
15
+ getExtensionField,
16
+ getSchemaByResolvedExtensions,
17
+ resolveExtensions,
18
+ splitExtensions,
19
+ } from '@tiptap/core'
20
+ import type { DOMOutputSpec, Mark } from '@tiptap/pm/model'
21
+ import { Node } from '@tiptap/pm/model'
22
+
23
+ import { getHTMLAttributes } from '../helpers.js'
24
+ import type { MarkProps, NodeProps, TiptapStaticRendererOptions } from '../json/renderer.js'
25
+
26
+ export type DomOutputSpecToElement<T> = (content: DOMOutputSpec) => (children?: T | T[]) => T
27
+
28
+ /**
29
+ * This takes a NodeExtension and maps it to a React component
30
+ * @param extension The node extension to map to a React component
31
+ * @param extensionAttributes All available extension attributes
32
+ * @returns A tuple with the name of the extension and a React component that renders the extension
33
+ */
34
+ export function mapNodeExtensionToReactNode<T>(
35
+ domOutputSpecToElement: DomOutputSpecToElement<T>,
36
+ extension: NodeExtension,
37
+ extensionAttributes: ExtensionAttribute[],
38
+ options?: Partial<Pick<TiptapStaticRendererOptions<T, Mark, Node>, 'unhandledNode'>>,
39
+ ): [string, (props: NodeProps<Node, T | T[]>) => T] {
40
+ const context = {
41
+ name: extension.name,
42
+ options: extension.options,
43
+ storage: extension.storage,
44
+ parent: extension.parent,
45
+ }
46
+
47
+ const renderToHTML = getExtensionField<NodeConfig['renderHTML']>(extension, 'renderHTML', context)
48
+
49
+ if (!renderToHTML) {
50
+ if (options?.unhandledNode) {
51
+ return [extension.name, options.unhandledNode]
52
+ }
53
+ return [
54
+ extension.name,
55
+ () => {
56
+ throw new Error(
57
+ `[tiptap error]: Node ${extension.name} cannot be rendered, it is missing a "renderToHTML" method, please implement it or override the corresponding "nodeMapping" method to have a custom rendering`,
58
+ )
59
+ },
60
+ ]
61
+ }
62
+
63
+ return [
64
+ extension.name,
65
+ ({ node, children }) => {
66
+ try {
67
+ return domOutputSpecToElement(
68
+ renderToHTML({
69
+ node,
70
+ HTMLAttributes: getHTMLAttributes(node, extensionAttributes),
71
+ }),
72
+ )(children)
73
+ } catch (e) {
74
+ throw new Error(
75
+ `[tiptap error]: Node ${
76
+ extension.name
77
+ } cannot be rendered, it's "renderToHTML" method threw an error: ${(e as Error).message}`,
78
+ { cause: e },
79
+ )
80
+ }
81
+ },
82
+ ]
83
+ }
84
+
85
+ /**
86
+ * This takes a MarkExtension and maps it to a React component
87
+ * @param extension The mark extension to map to a React component
88
+ * @param extensionAttributes All available extension attributes
89
+ * @returns A tuple with the name of the extension and a React component that renders the extension
90
+ */
91
+ export function mapMarkExtensionToReactNode<T>(
92
+ domOutputSpecToElement: DomOutputSpecToElement<T>,
93
+ extension: MarkExtension,
94
+ extensionAttributes: ExtensionAttribute[],
95
+ options?: Partial<Pick<TiptapStaticRendererOptions<T, Mark, Node>, 'unhandledMark'>>,
96
+ ): [string, (props: MarkProps<Mark, T | T[]>) => T] {
97
+ const context = {
98
+ name: extension.name,
99
+ options: extension.options,
100
+ storage: extension.storage,
101
+ parent: extension.parent,
102
+ }
103
+
104
+ const renderToHTML = getExtensionField<MarkConfig['renderHTML']>(extension, 'renderHTML', context)
105
+
106
+ if (!renderToHTML) {
107
+ if (options?.unhandledMark) {
108
+ return [extension.name, options.unhandledMark]
109
+ }
110
+ return [
111
+ extension.name,
112
+ () => {
113
+ throw new Error(`Node ${extension.name} cannot be rendered, it is missing a "renderToHTML" method`)
114
+ },
115
+ ]
116
+ }
117
+
118
+ return [
119
+ extension.name,
120
+ ({ mark, children }) => {
121
+ try {
122
+ return domOutputSpecToElement(
123
+ renderToHTML({
124
+ mark,
125
+ HTMLAttributes: getHTMLAttributes(mark, extensionAttributes),
126
+ }),
127
+ )(children)
128
+ } catch (e) {
129
+ throw new Error(
130
+ `[tiptap error]: Mark ${
131
+ extension.name
132
+ } cannot be rendered, it's "renderToHTML" method threw an error: ${(e as Error).message}`,
133
+ { cause: e },
134
+ )
135
+ }
136
+ },
137
+ ]
138
+ }
139
+
140
+ /**
141
+ * This function will statically render a Prosemirror Node to a target element type using the given extensions
142
+ * @param renderer The renderer to use to render the Prosemirror Node to the target element type
143
+ * @param domOutputSpecToElement A function that takes a Prosemirror DOMOutputSpec and returns a function that takes children and returns the target element type
144
+ * @param mapDefinedTypes An object with functions to map the doc and text types to the target element type
145
+ * @param content The Prosemirror Node to render
146
+ * @param extensions The extensions to use to render the Prosemirror Node
147
+ * @param options Additional options to pass to the renderer that can override the default behavior
148
+ * @returns The rendered target element type
149
+ */
150
+ export function renderToElement<T>({
151
+ renderer,
152
+ domOutputSpecToElement,
153
+ mapDefinedTypes,
154
+ content,
155
+ extensions,
156
+ options,
157
+ }: {
158
+ renderer: (options: TiptapStaticRendererOptions<T, Mark, Node>) => (ctx: { content: Node }) => T
159
+ domOutputSpecToElement: DomOutputSpecToElement<T>
160
+ mapDefinedTypes: {
161
+ doc: (props: NodeProps<Node, T | T[]>) => T
162
+ text: (props: NodeProps<Node, T | T[]>) => T
163
+ }
164
+ content: Node | JSONContent
165
+ extensions: Extensions
166
+ options?: Partial<TiptapStaticRendererOptions<T, Mark, Node>>
167
+ }): T {
168
+ // get all extensions in order & split them into nodes and marks
169
+ extensions = resolveExtensions(extensions)
170
+ const extensionAttributes = getAttributesFromExtensions(extensions)
171
+ const { nodeExtensions, markExtensions } = splitExtensions(extensions)
172
+
173
+ if (!(content instanceof Node)) {
174
+ content = Node.fromJSON(getSchemaByResolvedExtensions(extensions), content)
175
+ }
176
+
177
+ return renderer({
178
+ ...options,
179
+ nodeMapping: {
180
+ ...Object.fromEntries(
181
+ nodeExtensions
182
+ .filter(e => {
183
+ if (e.name in mapDefinedTypes) {
184
+ // These are predefined types that we don't need to map
185
+ return false
186
+ }
187
+ // No need to generate mappings for nodes that are already mapped
188
+ if (options?.nodeMapping) {
189
+ return !(e.name in options.nodeMapping)
190
+ }
191
+ return true
192
+ })
193
+ .map(nodeExtension =>
194
+ mapNodeExtensionToReactNode<T>(domOutputSpecToElement, nodeExtension, extensionAttributes, options),
195
+ ),
196
+ ),
197
+ ...mapDefinedTypes,
198
+ ...options?.nodeMapping,
199
+ },
200
+ markMapping: {
201
+ ...Object.fromEntries(
202
+ markExtensions
203
+ .filter(e => {
204
+ // No need to generate mappings for marks that are already mapped
205
+ if (options?.markMapping) {
206
+ return !(e.name in options.markMapping)
207
+ }
208
+ return true
209
+ })
210
+ .map(mark => mapMarkExtensionToReactNode<T>(domOutputSpecToElement, mark, extensionAttributes, options)),
211
+ ),
212
+ ...options?.markMapping,
213
+ },
214
+ })({ content })
215
+ }
@@ -0,0 +1,105 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { DOMOutputSpecArray, Extensions, JSONContent } from '@tiptap/core'
3
+ import type { DOMOutputSpec, Mark, Node } from '@tiptap/pm/model'
4
+
5
+ import {
6
+ renderJSONContentToString,
7
+ serializeAttrsToHTMLString,
8
+ serializeChildrenToHTMLString,
9
+ } from '../../json/html-string/string.js'
10
+ import type { TiptapStaticRendererOptions } from '../../json/renderer.js'
11
+ import { renderToElement } from '../extensionRenderer.js'
12
+
13
+ export { serializeAttrsToHTMLString, serializeChildrenToHTMLString } from '../../json/html-string/string.js'
14
+
15
+ /**
16
+ * Take a DOMOutputSpec and return a function that can render it to a string
17
+ * @param content The DOMOutputSpec to convert to a string
18
+ * @returns A function that can render the DOMOutputSpec to a string
19
+ */
20
+ export function domOutputSpecToHTMLString(content: DOMOutputSpec): (children?: string | string[]) => string {
21
+ if (typeof content === 'string') {
22
+ return () => content
23
+ }
24
+ if (typeof content === 'object' && 'length' in content) {
25
+ const [_tag, attrs, children, ...rest] = content as DOMOutputSpecArray
26
+ let tag = _tag
27
+ const parts = tag.split(' ')
28
+
29
+ if (parts.length > 1) {
30
+ tag = `${parts[1]} xmlns="${parts[0]}"`
31
+ }
32
+
33
+ if (attrs === undefined) {
34
+ return () => `<${tag}/>`
35
+ }
36
+ if (attrs === 0) {
37
+ return child => `<${tag}>${serializeChildrenToHTMLString(child)}</${tag}>`
38
+ }
39
+ if (typeof attrs === 'object') {
40
+ if (Array.isArray(attrs)) {
41
+ if (children === undefined) {
42
+ return child => `<${tag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}</${tag}>`
43
+ }
44
+ if (children === 0) {
45
+ return child => `<${tag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}</${tag}>`
46
+ }
47
+ return child =>
48
+ `<${tag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}${[children]
49
+ .concat(rest)
50
+ .map(a => domOutputSpecToHTMLString(a)(child))}</${tag}>`
51
+ }
52
+ if (children === undefined) {
53
+ return () => `<${tag}${serializeAttrsToHTMLString(attrs)}/>`
54
+ }
55
+ if (children === 0) {
56
+ return child => `<${tag}${serializeAttrsToHTMLString(attrs)}>${serializeChildrenToHTMLString(child)}</${tag}>`
57
+ }
58
+
59
+ return child =>
60
+ `<${tag}${serializeAttrsToHTMLString(attrs)}>${[children]
61
+ .concat(rest)
62
+ .map(a => domOutputSpecToHTMLString(a)(child))
63
+ .join('')}</${tag}>`
64
+ }
65
+ }
66
+
67
+ // TODO support DOM elements? How to handle them?
68
+ throw new Error(
69
+ '[tiptap error]: Unsupported DomOutputSpec type, check the `renderHTML` method output or implement a node mapping',
70
+ {
71
+ cause: content,
72
+ },
73
+ )
74
+ }
75
+
76
+ /**
77
+ * This function will statically render a Prosemirror Node to HTML using the provided extensions and options
78
+ * @param content The content to render to HTML
79
+ * @param extensions The extensions to use for rendering
80
+ * @param options The options to use for rendering
81
+ * @returns The rendered HTML string
82
+ */
83
+ export function renderToHTMLString({
84
+ content,
85
+ extensions,
86
+ options,
87
+ }: {
88
+ content: Node | JSONContent
89
+ extensions: Extensions
90
+ options?: Partial<TiptapStaticRendererOptions<string, Mark, Node>>
91
+ }): string {
92
+ return renderToElement<string>({
93
+ renderer: renderJSONContentToString,
94
+ domOutputSpecToElement: domOutputSpecToHTMLString,
95
+ mapDefinedTypes: {
96
+ // Map a doc node to concatenated children
97
+ doc: ({ children }) => serializeChildrenToHTMLString(children),
98
+ // Map a text node to its text content
99
+ text: ({ node }) => node.text ?? '',
100
+ },
101
+ content,
102
+ extensions,
103
+ options,
104
+ })
105
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../extensionRenderer.js'
2
+ export * from './html-string.js'
@@ -0,0 +1,2 @@
1
+ export * from '../extensionRenderer.js'
2
+ export * from './markdown.js'
@@ -0,0 +1,142 @@
1
+ import type { Extensions, JSONContent } from '@tiptap/core'
2
+ import type { Mark, Node } from '@tiptap/pm/model'
3
+
4
+ import type { TiptapStaticRendererOptions } from '../../json/renderer.js'
5
+ import { renderToHTMLString, serializeChildrenToHTMLString } from '../html-string/html-string.js'
6
+
7
+ /**
8
+ * This code is just to show the flexibility of this renderer. We can potentially render content to any format we want.
9
+ * This is a simple example of how we can render content to markdown. This is not a full implementation of a markdown renderer.
10
+ */
11
+ export function renderToMarkdown({
12
+ content,
13
+ extensions,
14
+ options,
15
+ }: {
16
+ content: Node | JSONContent
17
+ extensions: Extensions
18
+ options?: Partial<TiptapStaticRendererOptions<string, Mark, Node>>
19
+ }) {
20
+ return renderToHTMLString({
21
+ content,
22
+ extensions,
23
+ options: {
24
+ nodeMapping: {
25
+ bulletList({ children }) {
26
+ return `\n${serializeChildrenToHTMLString(children)}`
27
+ },
28
+ orderedList({ children }) {
29
+ return `\n${serializeChildrenToHTMLString(children)}`
30
+ },
31
+ listItem({ node, children, parent }) {
32
+ if (parent?.type.name === 'bulletList') {
33
+ return `- ${serializeChildrenToHTMLString(children).trim()}\n`
34
+ }
35
+ if (parent?.type.name === 'orderedList') {
36
+ let number = parent.attrs.start || 1
37
+
38
+ parent.forEach((parentChild, _offset, index) => {
39
+ if (node === parentChild) {
40
+ number = index + 1
41
+ }
42
+ })
43
+
44
+ return `${number}. ${serializeChildrenToHTMLString(children).trim()}\n`
45
+ }
46
+
47
+ return serializeChildrenToHTMLString(children)
48
+ },
49
+ paragraph({ children }) {
50
+ return `\n${serializeChildrenToHTMLString(children)}\n`
51
+ },
52
+ heading({ node, children }) {
53
+ const level = node.attrs.level as number
54
+
55
+ return `${new Array(level).fill('#').join('')} ${children}\n`
56
+ },
57
+ codeBlock({ node, children }) {
58
+ return `\n\`\`\`${node.attrs.language}\n${serializeChildrenToHTMLString(children)}\n\`\`\`\n`
59
+ },
60
+ blockquote({ children }) {
61
+ return `\n${serializeChildrenToHTMLString(children)
62
+ .trim()
63
+ .split('\n')
64
+ .map(a => `> ${a}`)
65
+ .join('\n')}`
66
+ },
67
+ image({ node }) {
68
+ return `![${node.attrs.alt}](${node.attrs.src})`
69
+ },
70
+ hardBreak() {
71
+ return '\n'
72
+ },
73
+ horizontalRule() {
74
+ return '\n---\n'
75
+ },
76
+ table({ children, node }) {
77
+ if (!Array.isArray(children)) {
78
+ return `\n${serializeChildrenToHTMLString(children)}\n`
79
+ }
80
+
81
+ return `\n${serializeChildrenToHTMLString(children[0])}| ${new Array(node.childCount - 2).fill('---').join(' | ')} |\n${serializeChildrenToHTMLString(children.slice(1))}\n`
82
+ },
83
+ tableRow({ children }) {
84
+ if (Array.isArray(children)) {
85
+ return `| ${children.join(' | ')} |\n`
86
+ }
87
+ return `${serializeChildrenToHTMLString(children)}\n`
88
+ },
89
+ tableHeader({ children }) {
90
+ return serializeChildrenToHTMLString(children).trim()
91
+ },
92
+ tableCell({ children }) {
93
+ return serializeChildrenToHTMLString(children).trim()
94
+ },
95
+ ...options?.nodeMapping,
96
+ },
97
+ markMapping: {
98
+ bold({ children }) {
99
+ return `**${serializeChildrenToHTMLString(children)}**`
100
+ },
101
+ italic({ children, node }) {
102
+ let isBoldToo = false
103
+
104
+ // Check if the node being wrapped also has a bold mark, if so, we need to use the bold markdown syntax
105
+ if (node?.marks.some(m => m.type.name === 'bold')) {
106
+ isBoldToo = true
107
+ }
108
+
109
+ if (isBoldToo) {
110
+ // If the content is bold, just wrap the bold content in italic markdown syntax with another set of asterisks
111
+ return `*${serializeChildrenToHTMLString(children)}*`
112
+ }
113
+
114
+ return `_${serializeChildrenToHTMLString(children)}_`
115
+ },
116
+ code({ children }) {
117
+ return `\`${serializeChildrenToHTMLString(children)}\``
118
+ },
119
+ strike({ children }) {
120
+ return `~~${serializeChildrenToHTMLString(children)}~~`
121
+ },
122
+ underline({ children }) {
123
+ return `<u>${serializeChildrenToHTMLString(children)}</u>`
124
+ },
125
+ subscript({ children }) {
126
+ return `<sub>${serializeChildrenToHTMLString(children)}</sub>`
127
+ },
128
+ superscript({ children }) {
129
+ return `<sup>${serializeChildrenToHTMLString(children)}</sup>`
130
+ },
131
+ link({ node, children }) {
132
+ return `[${serializeChildrenToHTMLString(children)}](${node.attrs.href})`
133
+ },
134
+ highlight({ children }) {
135
+ return `==${serializeChildrenToHTMLString(children)}==`
136
+ },
137
+ ...options?.markMapping,
138
+ },
139
+ ...options,
140
+ },
141
+ })
142
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../extensionRenderer.js'
2
+ export * from './react.js'
@@ -0,0 +1,152 @@
1
+ /* eslint-disable no-plusplus, @typescript-eslint/no-explicit-any */
2
+ import type { DOMOutputSpecArray, Extensions, JSONContent } from '@tiptap/core'
3
+ import type { DOMOutputSpec, Mark, Node } from '@tiptap/pm/model'
4
+ import React from 'react'
5
+
6
+ import { renderJSONContentToReactElement } from '../../json/react/react.js'
7
+ import type { TiptapStaticRendererOptions } from '../../json/renderer.js'
8
+ import { renderToElement } from '../extensionRenderer.js'
9
+
10
+ /**
11
+ * This function maps the attributes of a node or mark to HTML attributes
12
+ * @param attrs The attributes to map
13
+ * @param key The key to use for the React element
14
+ * @returns The mapped HTML attributes as an object
15
+ */
16
+ function mapAttrsToHTMLAttributes(attrs?: Record<string, any>, key?: string): Record<string, any> {
17
+ if (!attrs) {
18
+ return { key }
19
+ }
20
+ return Object.entries(attrs).reduce(
21
+ (acc, [name, value]) => {
22
+ if (name === 'class') {
23
+ return Object.assign(acc, { className: value })
24
+ }
25
+ return Object.assign(acc, { [name]: value })
26
+ },
27
+ { key },
28
+ )
29
+ }
30
+
31
+ /**
32
+ * Take a DOMOutputSpec and return a function that can render it to a React element
33
+ * @param content The DOMOutputSpec to convert to a React element
34
+ * @returns A function that can render the DOMOutputSpec to a React element
35
+ */
36
+ export function domOutputSpecToReactElement(
37
+ content: DOMOutputSpec,
38
+ key = 0,
39
+ ): (children?: React.ReactNode) => React.ReactNode {
40
+ if (typeof content === 'string') {
41
+ return () => content
42
+ }
43
+ if (typeof content === 'object' && 'length' in content) {
44
+ // eslint-disable-next-line prefer-const
45
+ let [tag, attrs, children, ...rest] = content as DOMOutputSpecArray
46
+ const parts = tag.split(' ')
47
+
48
+ if (parts.length > 1) {
49
+ tag = parts[1]
50
+ if (attrs === undefined) {
51
+ attrs = {
52
+ xmlns: parts[0],
53
+ }
54
+ }
55
+ if (attrs === 0) {
56
+ attrs = {
57
+ xmlns: parts[0],
58
+ }
59
+ children = 0
60
+ }
61
+ if (typeof attrs === 'object') {
62
+ attrs = Object.assign(attrs, { xmlns: parts[0] })
63
+ }
64
+ }
65
+
66
+ if (attrs === undefined) {
67
+ return () => React.createElement(tag, mapAttrsToHTMLAttributes(undefined, key.toString()))
68
+ }
69
+ if (attrs === 0) {
70
+ return child => React.createElement(tag, mapAttrsToHTMLAttributes(undefined, key.toString()), child)
71
+ }
72
+ if (typeof attrs === 'object') {
73
+ if (Array.isArray(attrs)) {
74
+ if (children === undefined) {
75
+ return child =>
76
+ React.createElement(
77
+ tag,
78
+ mapAttrsToHTMLAttributes(undefined, key.toString()),
79
+ domOutputSpecToReactElement(attrs as DOMOutputSpecArray, key++)(child),
80
+ )
81
+ }
82
+ if (children === 0) {
83
+ return child =>
84
+ React.createElement(
85
+ tag,
86
+ mapAttrsToHTMLAttributes(undefined, key.toString()),
87
+ domOutputSpecToReactElement(attrs as DOMOutputSpecArray, key++)(child),
88
+ )
89
+ }
90
+ return child =>
91
+ React.createElement(
92
+ tag,
93
+ mapAttrsToHTMLAttributes(undefined, key.toString()),
94
+ domOutputSpecToReactElement(attrs as DOMOutputSpecArray)(child),
95
+ [children].concat(rest).map(outputSpec => domOutputSpecToReactElement(outputSpec, key++)(child)),
96
+ )
97
+ }
98
+ if (children === undefined) {
99
+ return () => React.createElement(tag, mapAttrsToHTMLAttributes(attrs, key.toString()))
100
+ }
101
+ if (children === 0) {
102
+ return child => React.createElement(tag, mapAttrsToHTMLAttributes(attrs, key.toString()), child)
103
+ }
104
+
105
+ return child =>
106
+ React.createElement(
107
+ tag,
108
+ mapAttrsToHTMLAttributes(attrs, key.toString()),
109
+ [children].concat(rest).map(outputSpec => domOutputSpecToReactElement(outputSpec, key++)(child)),
110
+ )
111
+ }
112
+ }
113
+
114
+ // TODO support DOM elements? How to handle them?
115
+ throw new Error(
116
+ '[tiptap error]: Unsupported DomOutputSpec type, check the `renderHTML` method output or implement a node mapping',
117
+ {
118
+ cause: content,
119
+ },
120
+ )
121
+ }
122
+
123
+ /**
124
+ * This function will statically render a Prosemirror Node to a React component using the given extensions
125
+ * @param content The content to render to a React component
126
+ * @param extensions The extensions to use for rendering
127
+ * @param options The options to use for rendering
128
+ * @returns The React element that represents the rendered content
129
+ */
130
+ export function renderToReactElement({
131
+ content,
132
+ extensions,
133
+ options,
134
+ }: {
135
+ content: Node | JSONContent
136
+ extensions: Extensions
137
+ options?: Partial<TiptapStaticRendererOptions<React.ReactNode, Mark, Node>>
138
+ }): React.ReactNode {
139
+ return renderToElement<React.ReactNode>({
140
+ renderer: renderJSONContentToReactElement,
141
+ domOutputSpecToElement: domOutputSpecToReactElement,
142
+ mapDefinedTypes: {
143
+ // Map a doc node to concatenated children
144
+ doc: ({ children }) => React.createElement(React.Fragment, {}, children),
145
+ // Map a text node to its text content
146
+ text: ({ node }) => node.text ?? '',
147
+ },
148
+ content,
149
+ extensions,
150
+ options,
151
+ })
152
+ }