@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.
- package/LICENSE.md +21 -0
- package/README.md +18 -0
- package/dist/index.cjs +629 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +327 -0
- package/dist/index.d.ts +327 -0
- package/dist/index.js +584 -0
- package/dist/index.js.map +1 -0
- package/dist/json/html-string/index.cjs +111 -0
- package/dist/json/html-string/index.cjs.map +1 -0
- package/dist/json/html-string/index.d.cts +201 -0
- package/dist/json/html-string/index.d.ts +201 -0
- package/dist/json/html-string/index.js +81 -0
- package/dist/json/html-string/index.js.map +1 -0
- package/dist/json/react/index.cjs +117 -0
- package/dist/json/react/index.cjs.map +1 -0
- package/dist/json/react/index.d.cts +190 -0
- package/dist/json/react/index.d.ts +190 -0
- package/dist/json/react/index.js +79 -0
- package/dist/json/react/index.js.map +1 -0
- package/dist/json/renderer.cjs +89 -0
- package/dist/json/renderer.cjs.map +1 -0
- package/dist/json/renderer.d.cts +166 -0
- package/dist/json/renderer.d.ts +166 -0
- package/dist/json/renderer.js +64 -0
- package/dist/json/renderer.js.map +1 -0
- package/dist/pm/html-string/index.cjs +344 -0
- package/dist/pm/html-string/index.cjs.map +1 -0
- package/dist/pm/html-string/index.d.cts +175 -0
- package/dist/pm/html-string/index.d.ts +175 -0
- package/dist/pm/html-string/index.js +317 -0
- package/dist/pm/html-string/index.js.map +1 -0
- package/dist/pm/markdown/index.cjs +473 -0
- package/dist/pm/markdown/index.cjs.map +1 -0
- package/dist/pm/markdown/index.d.cts +153 -0
- package/dist/pm/markdown/index.d.ts +153 -0
- package/dist/pm/markdown/index.js +449 -0
- package/dist/pm/markdown/index.js.map +1 -0
- package/dist/pm/react/index.cjs +399 -0
- package/dist/pm/react/index.cjs.map +1 -0
- package/dist/pm/react/index.d.cts +163 -0
- package/dist/pm/react/index.d.ts +163 -0
- package/dist/pm/react/index.js +364 -0
- package/dist/pm/react/index.js.map +1 -0
- package/package.json +101 -0
- package/src/helpers.ts +54 -0
- package/src/index.ts +6 -0
- package/src/json/html-string/index.ts +2 -0
- package/src/json/html-string/string.ts +49 -0
- package/src/json/react/index.ts +2 -0
- package/src/json/react/react.ts +33 -0
- package/src/json/renderer.ts +241 -0
- package/src/pm/extensionRenderer.ts +215 -0
- package/src/pm/html-string/html-string.ts +105 -0
- package/src/pm/html-string/index.ts +2 -0
- package/src/pm/markdown/index.ts +2 -0
- package/src/pm/markdown/markdown.ts +142 -0
- package/src/pm/react/index.ts +2 -0
- 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,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 ``
|
|
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,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
|
+
}
|