@tiptap/static-renderer 3.0.0-next.1
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/README.md +18 -0
- package/dist/index.cjs +62 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/json/html-string/index.cjs +100 -0
- package/dist/json/html-string/index.cjs.map +1 -0
- package/dist/json/html-string/index.d.cts +205 -0
- package/dist/json/html-string/index.d.ts +205 -0
- package/dist/json/html-string/index.js +72 -0
- package/dist/json/html-string/index.js.map +1 -0
- package/dist/json/react/index.cjs +2306 -0
- package/dist/json/react/index.cjs.map +1 -0
- package/dist/json/react/index.d.cts +207 -0
- package/dist/json/react/index.d.ts +207 -0
- package/dist/json/react/index.js +2291 -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 +182 -0
- package/dist/json/renderer.d.ts +182 -0
- package/dist/json/renderer.js +64 -0
- package/dist/json/renderer.js.map +1 -0
- package/dist/pm/html-string/index.cjs +359 -0
- package/dist/pm/html-string/index.cjs.map +1 -0
- package/dist/pm/html-string/index.d.cts +192 -0
- package/dist/pm/html-string/index.d.ts +192 -0
- package/dist/pm/html-string/index.js +332 -0
- package/dist/pm/html-string/index.js.map +1 -0
- package/dist/pm/react/index.cjs +2588 -0
- package/dist/pm/react/index.cjs.map +1 -0
- package/dist/pm/react/index.d.cts +181 -0
- package/dist/pm/react/index.d.ts +181 -0
- package/dist/pm/react/index.js +2576 -0
- package/dist/pm/react/index.js.map +1 -0
- package/package.json +82 -0
- package/src/helpers.example.ts +35 -0
- package/src/helpers.ts +65 -0
- package/src/index.ts +2 -0
- package/src/json/html-string/index.ts +2 -0
- package/src/json/html-string/string.example.ts +46 -0
- package/src/json/html-string/string.ts +22 -0
- package/src/json/react/index.ts +2 -0
- package/src/json/react/react.example.ts +45 -0
- package/src/json/react/react.tsx +35 -0
- package/src/json/renderer.ts +242 -0
- package/src/pm/extensionRenderer.ts +230 -0
- package/src/pm/html-string/html-string.example.ts +225 -0
- package/src/pm/html-string/html-string.ts +121 -0
- package/src/pm/html-string/index.ts +2 -0
- package/src/pm/markdown/markdown.example.ts +296 -0
- package/src/pm/react/index.ts +2 -0
- package/src/pm/react/react.example.tsx +306 -0
- package/src/pm/react/react.tsx +133 -0
- package/src/types.ts +57 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { renderJSONContentToString } from './string.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This example demonstrates how to render a JSON representation of a node to a string
|
|
5
|
+
* It does so without including Prosemirror or Tiptap, it is the lightest possible way to render JSON content
|
|
6
|
+
* But, since it doesn't include Prosemirror or Tiptap, it cannot automatically render marks or nodes for you.
|
|
7
|
+
* If you need that, you should use the `renderToHTMLString` from `@tiptap/static-renderer`
|
|
8
|
+
*
|
|
9
|
+
* You have complete control over the rendering process. And can replace how each Node/Mark is rendered.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
console.log(
|
|
14
|
+
renderJSONContentToString({
|
|
15
|
+
nodeMapping: {
|
|
16
|
+
text({ node }) {
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
18
|
+
return node.text!
|
|
19
|
+
},
|
|
20
|
+
heading({ node, children }) {
|
|
21
|
+
const level = node.attrs?.level
|
|
22
|
+
const attrs = Object.entries(node.attrs || {})
|
|
23
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
24
|
+
.join(' ')
|
|
25
|
+
|
|
26
|
+
return `<h${level}${attrs ? ` ${attrs}` : ''}>${([] as string[])
|
|
27
|
+
.concat(children || '')
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.join('\n')}</h${level}>`
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
markMapping: {},
|
|
33
|
+
})({
|
|
34
|
+
content: {
|
|
35
|
+
type: 'heading',
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: 'hello world',
|
|
40
|
+
marks: [],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
attrs: { level: 2 },
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { MarkType, NodeType } from '../../types.js'
|
|
3
|
+
import { TiptapStaticRenderer, TiptapStaticRendererOptions } from '../renderer.js'
|
|
4
|
+
|
|
5
|
+
export function renderJSONContentToString<
|
|
6
|
+
/**
|
|
7
|
+
* A mark type is either a JSON representation of a mark or a Prosemirror mark instance
|
|
8
|
+
*/
|
|
9
|
+
TMarkType extends { type: any } = MarkType,
|
|
10
|
+
/**
|
|
11
|
+
* A node type is either a JSON representation of a node or a Prosemirror node instance
|
|
12
|
+
*/
|
|
13
|
+
TNodeType extends {
|
|
14
|
+
content?: { forEach:(cb: (node: TNodeType) => void) => void };
|
|
15
|
+
marks?: readonly TMarkType[];
|
|
16
|
+
type: string | { name: string };
|
|
17
|
+
} = NodeType,
|
|
18
|
+
>(options: TiptapStaticRendererOptions<string, TMarkType, TNodeType>) {
|
|
19
|
+
return TiptapStaticRenderer(ctx => {
|
|
20
|
+
return ctx.component(ctx.props as any)
|
|
21
|
+
}, options)
|
|
22
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { NodeType } from '../../types.js'
|
|
4
|
+
import { NodeProps } from '../renderer.js'
|
|
5
|
+
import { renderJSONContentToReactElement } from './react.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This example demonstrates how to render a JSON representation of a node to a React element
|
|
9
|
+
* It does so without including Prosemirror or Tiptap, it is the lightest possible way to render JSON content
|
|
10
|
+
* But, since it doesn't include Prosemirror or Tiptap, it cannot automatically render marks or nodes for you.
|
|
11
|
+
* If you need that, you should use the `renderToReactElement` from `@tiptap/static-renderer`
|
|
12
|
+
*
|
|
13
|
+
* You have complete control over the rendering process. And can replace how each Node/Mark is rendered.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.log(renderJSONContentToReactElement({
|
|
18
|
+
nodeMapping: {
|
|
19
|
+
text({ node }) {
|
|
20
|
+
return node.text ?? null
|
|
21
|
+
},
|
|
22
|
+
heading({
|
|
23
|
+
node,
|
|
24
|
+
children,
|
|
25
|
+
}: NodeProps<NodeType<'heading', { level: number }>, React.ReactNode>) {
|
|
26
|
+
const level = node.attrs.level
|
|
27
|
+
const hTag = `h${level}`
|
|
28
|
+
|
|
29
|
+
return React.createElement(hTag, node.attrs, children)
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
markMapping: {},
|
|
33
|
+
})({
|
|
34
|
+
content: {
|
|
35
|
+
type: 'heading',
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: 'hello world',
|
|
40
|
+
marks: [],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
attrs: { level: 2 },
|
|
44
|
+
},
|
|
45
|
+
}))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
import { MarkType, NodeType } from '../../types.js'
|
|
6
|
+
import { TiptapStaticRenderer, TiptapStaticRendererOptions } from '../renderer.js'
|
|
7
|
+
|
|
8
|
+
export function renderJSONContentToReactElement<
|
|
9
|
+
/**
|
|
10
|
+
* A mark type is either a JSON representation of a mark or a Prosemirror mark instance
|
|
11
|
+
*/
|
|
12
|
+
TMarkType extends { type: any } = MarkType,
|
|
13
|
+
/**
|
|
14
|
+
* A node type is either a JSON representation of a node or a Prosemirror node instance
|
|
15
|
+
*/
|
|
16
|
+
TNodeType extends {
|
|
17
|
+
content?: { forEach:(cb: (node: TNodeType) => void) => void };
|
|
18
|
+
marks?: readonly TMarkType[];
|
|
19
|
+
type: string | { name: string };
|
|
20
|
+
} = NodeType,
|
|
21
|
+
>(options: TiptapStaticRendererOptions<React.ReactNode, TMarkType, TNodeType>) {
|
|
22
|
+
let key = 0
|
|
23
|
+
|
|
24
|
+
return TiptapStaticRenderer<React.ReactNode, TMarkType, TNodeType>(
|
|
25
|
+
({ component, props: { children, ...props } }) => {
|
|
26
|
+
return React.createElement(
|
|
27
|
+
component as React.FC<typeof props>,
|
|
28
|
+
// eslint-disable-next-line no-plusplus
|
|
29
|
+
Object.assign(props, { key: key++ }),
|
|
30
|
+
([] as React.ReactNode[]).concat(children),
|
|
31
|
+
)
|
|
32
|
+
},
|
|
33
|
+
options,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type { MarkType, NodeType } from '../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for a node renderer
|
|
6
|
+
*/
|
|
7
|
+
export type NodeProps<TNodeType = any, TChildren = any> = {
|
|
8
|
+
/**
|
|
9
|
+
* The current node to render
|
|
10
|
+
*/
|
|
11
|
+
node: TNodeType;
|
|
12
|
+
/**
|
|
13
|
+
* Unless the node is the root node, this will always be defined
|
|
14
|
+
*/
|
|
15
|
+
parent?: TNodeType;
|
|
16
|
+
/**
|
|
17
|
+
* The children of the current node
|
|
18
|
+
*/
|
|
19
|
+
children?: TChildren;
|
|
20
|
+
/**
|
|
21
|
+
* Render a child element
|
|
22
|
+
*/
|
|
23
|
+
renderElement: (props: {
|
|
24
|
+
/**
|
|
25
|
+
* Tiptap JSON content to render
|
|
26
|
+
*/
|
|
27
|
+
content: TNodeType;
|
|
28
|
+
/**
|
|
29
|
+
* The parent node of the current node
|
|
30
|
+
*/
|
|
31
|
+
parent?: TNodeType;
|
|
32
|
+
}) => TChildren;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Props for a mark renderer
|
|
37
|
+
*/
|
|
38
|
+
export type MarkProps<TMarkType = any, TChildren = any, TNodeType = any> = {
|
|
39
|
+
/**
|
|
40
|
+
* The current mark to render
|
|
41
|
+
*/
|
|
42
|
+
mark: TMarkType;
|
|
43
|
+
/**
|
|
44
|
+
* The children of the current mark
|
|
45
|
+
*/
|
|
46
|
+
children?: TChildren;
|
|
47
|
+
/**
|
|
48
|
+
* The node the current mark is applied to
|
|
49
|
+
*/
|
|
50
|
+
node: TNodeType;
|
|
51
|
+
/**
|
|
52
|
+
* The node the current mark is applied to
|
|
53
|
+
*/
|
|
54
|
+
parent?: TNodeType;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type TiptapStaticRendererOptions<
|
|
58
|
+
/**
|
|
59
|
+
* The return type of the render function (e.g. React.ReactNode, string)
|
|
60
|
+
*/
|
|
61
|
+
TReturnType,
|
|
62
|
+
/**
|
|
63
|
+
* A mark type is either a JSON representation of a mark or a Prosemirror mark instance
|
|
64
|
+
*/
|
|
65
|
+
TMarkType extends { type: any } = MarkType,
|
|
66
|
+
/**
|
|
67
|
+
* A node type is either a JSON representation of a node or a Prosemirror node instance
|
|
68
|
+
*/
|
|
69
|
+
TNodeType extends {
|
|
70
|
+
content?: { forEach: (cb: (node: TNodeType) => void) => void };
|
|
71
|
+
marks?: readonly TMarkType[];
|
|
72
|
+
type: string | { name: string };
|
|
73
|
+
} = NodeType,
|
|
74
|
+
/**
|
|
75
|
+
* A node renderer is a function that takes a node and its children and returns the rendered output
|
|
76
|
+
*/
|
|
77
|
+
TNodeRender extends (ctx: NodeProps<TNodeType, TReturnType | TReturnType[]>) => TReturnType = (
|
|
78
|
+
ctx: NodeProps<TNodeType, TReturnType | TReturnType[]>
|
|
79
|
+
) => TReturnType,
|
|
80
|
+
/**
|
|
81
|
+
* A mark renderer is a function that takes a mark and its children and returns the rendered output
|
|
82
|
+
*/
|
|
83
|
+
TMarkRender extends (ctx: MarkProps<TMarkType, TReturnType | TReturnType[], TNodeType>) => TReturnType = (
|
|
84
|
+
ctx: MarkProps<TMarkType, TReturnType | TReturnType[], TNodeType>
|
|
85
|
+
) => TReturnType,
|
|
86
|
+
> = {
|
|
87
|
+
/**
|
|
88
|
+
* Mapping of node types to react components
|
|
89
|
+
*/
|
|
90
|
+
nodeMapping: Record<string, TNodeRender>;
|
|
91
|
+
/**
|
|
92
|
+
* Mapping of mark types to react components
|
|
93
|
+
*/
|
|
94
|
+
markMapping: Record<string, TMarkRender>;
|
|
95
|
+
/**
|
|
96
|
+
* Component to render if a node type is not handled
|
|
97
|
+
*/
|
|
98
|
+
unhandledNode?: TNodeRender;
|
|
99
|
+
/**
|
|
100
|
+
* Component to render if a mark type is not handled
|
|
101
|
+
*/
|
|
102
|
+
unhandledMark?: TMarkRender;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Tiptap Static Renderer
|
|
107
|
+
* ----------------------
|
|
108
|
+
*
|
|
109
|
+
* This function is a basis to allow for different renderers to be created.
|
|
110
|
+
* Generic enough to be able to statically render Prosemirror JSON or Prosemirror Nodes.
|
|
111
|
+
*
|
|
112
|
+
* Using this function, you can create a renderer that takes a JSON representation of a Prosemirror document
|
|
113
|
+
* and renders it using a mapping of node types to React components or even to a string.
|
|
114
|
+
* This function is used as the basis to create the `reactRenderer` and `stringRenderer` functions.
|
|
115
|
+
*/
|
|
116
|
+
export function TiptapStaticRenderer<
|
|
117
|
+
/**
|
|
118
|
+
* The return type of the render function (e.g. React.ReactNode, string)
|
|
119
|
+
*/
|
|
120
|
+
TReturnType,
|
|
121
|
+
/**
|
|
122
|
+
* A mark type is either a JSON representation of a mark or a Prosemirror mark instance
|
|
123
|
+
*/
|
|
124
|
+
TMarkType extends { type: string | { name: string } } = MarkType,
|
|
125
|
+
/**
|
|
126
|
+
* A node type is either a JSON representation of a node or a Prosemirror node instance
|
|
127
|
+
*/
|
|
128
|
+
TNodeType extends {
|
|
129
|
+
content?: { forEach:(
|
|
130
|
+
cb: (node: TNodeType) => void) => void };
|
|
131
|
+
marks?: readonly TMarkType[];
|
|
132
|
+
type: string | { name: string };
|
|
133
|
+
} = NodeType,
|
|
134
|
+
/**
|
|
135
|
+
* A node renderer is a function that takes a node and its children and returns the rendered output
|
|
136
|
+
*/
|
|
137
|
+
TNodeRender extends (ctx: NodeProps<TNodeType, TReturnType | TReturnType[]>) => TReturnType = (
|
|
138
|
+
ctx: NodeProps<TNodeType, TReturnType | TReturnType[]>
|
|
139
|
+
) => TReturnType,
|
|
140
|
+
/**
|
|
141
|
+
* A mark renderer is a function that takes a mark and its children and returns the rendered output
|
|
142
|
+
*/
|
|
143
|
+
TMarkRender extends (ctx: MarkProps<TMarkType, TReturnType | TReturnType[], TNodeType>) => TReturnType = (
|
|
144
|
+
ctx: MarkProps<TMarkType, TReturnType | TReturnType[], TNodeType>
|
|
145
|
+
) => TReturnType,
|
|
146
|
+
>(
|
|
147
|
+
/**
|
|
148
|
+
* The function that actually renders the component
|
|
149
|
+
*/
|
|
150
|
+
renderComponent: (
|
|
151
|
+
ctx:
|
|
152
|
+
| {
|
|
153
|
+
component: TNodeRender;
|
|
154
|
+
props: NodeProps<TNodeType, TReturnType | TReturnType[]>;
|
|
155
|
+
}
|
|
156
|
+
| {
|
|
157
|
+
component: TMarkRender;
|
|
158
|
+
props: MarkProps<TMarkType, TReturnType | TReturnType[], TNodeType>;
|
|
159
|
+
}
|
|
160
|
+
) => TReturnType,
|
|
161
|
+
{
|
|
162
|
+
nodeMapping,
|
|
163
|
+
markMapping,
|
|
164
|
+
unhandledNode,
|
|
165
|
+
unhandledMark,
|
|
166
|
+
}: TiptapStaticRendererOptions<TReturnType, TMarkType, TNodeType, TNodeRender, TMarkRender>,
|
|
167
|
+
) {
|
|
168
|
+
/**
|
|
169
|
+
* Render Tiptap JSON and all its children using the provided node and mark mappings.
|
|
170
|
+
*/
|
|
171
|
+
return function renderContent({
|
|
172
|
+
content,
|
|
173
|
+
parent,
|
|
174
|
+
}: {
|
|
175
|
+
/**
|
|
176
|
+
* Tiptap JSON content to render
|
|
177
|
+
*/
|
|
178
|
+
content: TNodeType;
|
|
179
|
+
/**
|
|
180
|
+
* The parent node of the current node
|
|
181
|
+
*/
|
|
182
|
+
parent?: TNodeType;
|
|
183
|
+
}): TReturnType {
|
|
184
|
+
const nodeType = typeof content.type === 'string' ? content.type : content.type.name
|
|
185
|
+
const NodeHandler = nodeMapping[nodeType] ?? unhandledNode
|
|
186
|
+
|
|
187
|
+
if (!NodeHandler) {
|
|
188
|
+
throw new Error(`missing handler for node type ${nodeType}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const nodeContent = renderComponent({
|
|
192
|
+
component: NodeHandler,
|
|
193
|
+
props: {
|
|
194
|
+
node: content,
|
|
195
|
+
parent,
|
|
196
|
+
renderElement: renderContent,
|
|
197
|
+
// Lazily compute the children to avoid unnecessary recursion
|
|
198
|
+
get children() {
|
|
199
|
+
// recursively render child content nodes
|
|
200
|
+
const children: TReturnType[] = []
|
|
201
|
+
|
|
202
|
+
if (content.content) {
|
|
203
|
+
content.content.forEach(child => {
|
|
204
|
+
children.push(
|
|
205
|
+
renderContent({
|
|
206
|
+
content: child,
|
|
207
|
+
parent: content,
|
|
208
|
+
}),
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return children
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// apply marks to the content
|
|
219
|
+
const markedContent = content.marks
|
|
220
|
+
? content.marks.reduce((acc, mark) => {
|
|
221
|
+
const markType = typeof mark.type === 'string' ? mark.type : mark.type.name
|
|
222
|
+
const MarkHandler = markMapping[markType] ?? unhandledMark
|
|
223
|
+
|
|
224
|
+
if (!MarkHandler) {
|
|
225
|
+
throw new Error(`missing handler for mark type ${markType}`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return renderComponent({
|
|
229
|
+
component: MarkHandler,
|
|
230
|
+
props: {
|
|
231
|
+
mark,
|
|
232
|
+
parent,
|
|
233
|
+
node: content,
|
|
234
|
+
children: acc,
|
|
235
|
+
},
|
|
236
|
+
})
|
|
237
|
+
}, nodeContent)
|
|
238
|
+
: nodeContent
|
|
239
|
+
|
|
240
|
+
return markedContent
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* eslint-disable no-plusplus */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ExtensionAttribute,
|
|
6
|
+
Extensions,
|
|
7
|
+
getAttributesFromExtensions,
|
|
8
|
+
getExtensionField,
|
|
9
|
+
getSchemaByResolvedExtensions,
|
|
10
|
+
JSONContent,
|
|
11
|
+
Mark as MarkExtension,
|
|
12
|
+
MarkConfig,
|
|
13
|
+
Node as NodeExtension,
|
|
14
|
+
NodeConfig,
|
|
15
|
+
resolveExtensions,
|
|
16
|
+
splitExtensions,
|
|
17
|
+
} from '@tiptap/core'
|
|
18
|
+
import { DOMOutputSpec, Mark, Node } from '@tiptap/pm/model'
|
|
19
|
+
|
|
20
|
+
import { getHTMLAttributes } from '../helpers.js'
|
|
21
|
+
import { MarkProps, NodeProps, TiptapStaticRendererOptions } from '../json/renderer.js'
|
|
22
|
+
|
|
23
|
+
export type DomOutputSpecToElement<T> = (content: DOMOutputSpec) => (children?: T | T[]) => T;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* This takes a NodeExtension and maps it to a React component
|
|
27
|
+
* @param extension The node extension to map to a React component
|
|
28
|
+
* @param extensionAttributes All available extension attributes
|
|
29
|
+
* @returns A tuple with the name of the extension and a React component that renders the extension
|
|
30
|
+
*/
|
|
31
|
+
export function mapNodeExtensionToReactNode<T>(
|
|
32
|
+
domOutputSpecToElement: DomOutputSpecToElement<T>,
|
|
33
|
+
extension: NodeExtension,
|
|
34
|
+
extensionAttributes: ExtensionAttribute[],
|
|
35
|
+
options?: Partial<Pick<TiptapStaticRendererOptions<T, Mark, Node>, 'unhandledNode'>>,
|
|
36
|
+
): [string, (props: NodeProps<Node, T | T[]>) => T] {
|
|
37
|
+
const context = {
|
|
38
|
+
name: extension.name,
|
|
39
|
+
options: extension.options,
|
|
40
|
+
storage: extension.storage,
|
|
41
|
+
parent: extension.parent,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const renderToHTML = getExtensionField<NodeConfig['renderHTML']>(
|
|
45
|
+
extension,
|
|
46
|
+
'renderHTML',
|
|
47
|
+
context,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if (!renderToHTML) {
|
|
51
|
+
if (options?.unhandledNode) {
|
|
52
|
+
return [extension.name, options.unhandledNode]
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
extension.name,
|
|
56
|
+
() => {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`[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`,
|
|
59
|
+
)
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
extension.name,
|
|
66
|
+
({ node, children }) => {
|
|
67
|
+
try {
|
|
68
|
+
return domOutputSpecToElement(
|
|
69
|
+
renderToHTML({
|
|
70
|
+
node,
|
|
71
|
+
HTMLAttributes: getHTMLAttributes(node, extensionAttributes),
|
|
72
|
+
}),
|
|
73
|
+
)(children)
|
|
74
|
+
} catch (e) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`[tiptap error]: Node ${
|
|
77
|
+
extension.name
|
|
78
|
+
} cannot be rendered, it's "renderToHTML" method threw an error: ${(e as Error).message}`,
|
|
79
|
+
{ cause: e },
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* This takes a MarkExtension and maps it to a React component
|
|
88
|
+
* @param extension The mark extension to map to a React component
|
|
89
|
+
* @param extensionAttributes All available extension attributes
|
|
90
|
+
* @returns A tuple with the name of the extension and a React component that renders the extension
|
|
91
|
+
*/
|
|
92
|
+
export function mapMarkExtensionToReactNode<T>(
|
|
93
|
+
domOutputSpecToElement: DomOutputSpecToElement<T>,
|
|
94
|
+
extension: MarkExtension,
|
|
95
|
+
extensionAttributes: ExtensionAttribute[],
|
|
96
|
+
options?: Partial<Pick<TiptapStaticRendererOptions<T, Mark, Node>, 'unhandledMark'>>,
|
|
97
|
+
): [string, (props: MarkProps<Mark, T | T[]>) => T] {
|
|
98
|
+
const context = {
|
|
99
|
+
name: extension.name,
|
|
100
|
+
options: extension.options,
|
|
101
|
+
storage: extension.storage,
|
|
102
|
+
parent: extension.parent,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const renderToHTML = getExtensionField<MarkConfig['renderHTML']>(
|
|
106
|
+
extension,
|
|
107
|
+
'renderHTML',
|
|
108
|
+
context,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (!renderToHTML) {
|
|
112
|
+
if (options?.unhandledMark) {
|
|
113
|
+
return [extension.name, options.unhandledMark]
|
|
114
|
+
}
|
|
115
|
+
return [
|
|
116
|
+
extension.name,
|
|
117
|
+
() => {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Node ${extension.name} cannot be rendered, it is missing a "renderToHTML" method`,
|
|
120
|
+
)
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [
|
|
126
|
+
extension.name,
|
|
127
|
+
({ mark, children }) => {
|
|
128
|
+
try {
|
|
129
|
+
return domOutputSpecToElement(
|
|
130
|
+
renderToHTML({
|
|
131
|
+
mark,
|
|
132
|
+
HTMLAttributes: getHTMLAttributes(mark, extensionAttributes),
|
|
133
|
+
}),
|
|
134
|
+
)(children)
|
|
135
|
+
} catch (e) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`[tiptap error]: Mark ${
|
|
138
|
+
extension.name
|
|
139
|
+
} cannot be rendered, it's "renderToHTML" method threw an error: ${(e as Error).message}`,
|
|
140
|
+
{ cause: e },
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* This function will statically render a Prosemirror Node to a target element type using the given extensions
|
|
149
|
+
* @param renderer The renderer to use to render the Prosemirror Node to the target element type
|
|
150
|
+
* @param domOutputSpecToElement A function that takes a Prosemirror DOMOutputSpec and returns a function that takes children and returns the target element type
|
|
151
|
+
* @param mapDefinedTypes An object with functions to map the doc and text types to the target element type
|
|
152
|
+
* @param content The Prosemirror Node to render
|
|
153
|
+
* @param extensions The extensions to use to render the Prosemirror Node
|
|
154
|
+
* @param options Additional options to pass to the renderer that can override the default behavior
|
|
155
|
+
* @returns The rendered target element type
|
|
156
|
+
*/
|
|
157
|
+
export function renderToElement<T>({
|
|
158
|
+
renderer,
|
|
159
|
+
domOutputSpecToElement,
|
|
160
|
+
mapDefinedTypes,
|
|
161
|
+
content,
|
|
162
|
+
extensions,
|
|
163
|
+
options,
|
|
164
|
+
}: {
|
|
165
|
+
renderer: (options: TiptapStaticRendererOptions<T, Mark, Node>) => (ctx: { content: Node }) => T;
|
|
166
|
+
domOutputSpecToElement: DomOutputSpecToElement<T>;
|
|
167
|
+
mapDefinedTypes: {
|
|
168
|
+
doc: (props: NodeProps<Node, T | T[]>) => T;
|
|
169
|
+
text: (props: NodeProps<Node, T | T[]>) => T;
|
|
170
|
+
};
|
|
171
|
+
content: Node | JSONContent;
|
|
172
|
+
extensions: Extensions;
|
|
173
|
+
options?: Partial<TiptapStaticRendererOptions<T, Mark, Node>>;
|
|
174
|
+
}): T {
|
|
175
|
+
// get all extensions in order & split them into nodes and marks
|
|
176
|
+
extensions = resolveExtensions(extensions)
|
|
177
|
+
const extensionAttributes = getAttributesFromExtensions(extensions)
|
|
178
|
+
const { nodeExtensions, markExtensions } = splitExtensions(extensions)
|
|
179
|
+
|
|
180
|
+
if (!(content instanceof Node)) {
|
|
181
|
+
content = Node.fromJSON(getSchemaByResolvedExtensions(extensions), content)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return renderer({
|
|
185
|
+
...options,
|
|
186
|
+
nodeMapping: {
|
|
187
|
+
...Object.fromEntries(
|
|
188
|
+
nodeExtensions
|
|
189
|
+
.filter(e => {
|
|
190
|
+
if (e.name in mapDefinedTypes) {
|
|
191
|
+
// These are predefined types that we don't need to map
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
// No need to generate mappings for nodes that are already mapped
|
|
195
|
+
if (options?.nodeMapping) {
|
|
196
|
+
return !(e.name in options.nodeMapping)
|
|
197
|
+
}
|
|
198
|
+
return true
|
|
199
|
+
})
|
|
200
|
+
.map(nodeExtension => mapNodeExtensionToReactNode<T>(
|
|
201
|
+
domOutputSpecToElement,
|
|
202
|
+
nodeExtension,
|
|
203
|
+
extensionAttributes,
|
|
204
|
+
options,
|
|
205
|
+
)),
|
|
206
|
+
),
|
|
207
|
+
...mapDefinedTypes,
|
|
208
|
+
...options?.nodeMapping,
|
|
209
|
+
},
|
|
210
|
+
markMapping: {
|
|
211
|
+
...Object.fromEntries(
|
|
212
|
+
markExtensions
|
|
213
|
+
.filter(e => {
|
|
214
|
+
// No need to generate mappings for marks that are already mapped
|
|
215
|
+
if (options?.markMapping) {
|
|
216
|
+
return !(e.name in options.markMapping)
|
|
217
|
+
}
|
|
218
|
+
return true
|
|
219
|
+
})
|
|
220
|
+
.map(mark => mapMarkExtensionToReactNode<T>(
|
|
221
|
+
domOutputSpecToElement,
|
|
222
|
+
mark,
|
|
223
|
+
extensionAttributes,
|
|
224
|
+
options,
|
|
225
|
+
)),
|
|
226
|
+
),
|
|
227
|
+
...options?.markMapping,
|
|
228
|
+
},
|
|
229
|
+
})({ content })
|
|
230
|
+
}
|