@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
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@tiptap/static-renderer",
3
+ "description": "statically render Tiptap JSON",
4
+ "version": "3.0.0-beta.0",
5
+ "homepage": "https://tiptap.dev",
6
+ "keywords": [
7
+ "tiptap",
8
+ "tiptap static renderer",
9
+ "tiptap react renderer"
10
+ ],
11
+ "license": "MIT",
12
+ "funding": {
13
+ "type": "github",
14
+ "url": "https://github.com/sponsors/ueberdosis"
15
+ },
16
+ "type": "module",
17
+ "exports": {
18
+ ".": {
19
+ "types": {
20
+ "import": "./dist/index.d.ts",
21
+ "require": "./dist/index.d.cts"
22
+ },
23
+ "import": "./dist/index.js",
24
+ "require": "./dist/index.cjs"
25
+ },
26
+ "./json/react": {
27
+ "types": {
28
+ "import": "./dist/json/react/index.d.ts",
29
+ "require": "./dist/json/react/index.d.cts"
30
+ },
31
+ "import": "./dist/json/react/index.js",
32
+ "require": "./dist/json/react/index.cjs"
33
+ },
34
+ "./json/html-string": {
35
+ "types": {
36
+ "import": "./dist/json/html-string/index.d.ts",
37
+ "require": "./dist/json/html-string/index.d.cts"
38
+ },
39
+ "import": "./dist/json/html-string/index.js",
40
+ "require": "./dist/json/html-string/index.cjs"
41
+ },
42
+ "./pm/react": {
43
+ "types": {
44
+ "import": "./dist/pm/react/index.d.ts",
45
+ "require": "./dist/pm/react/index.d.cts"
46
+ },
47
+ "import": "./dist/pm/react/index.js",
48
+ "require": "./dist/pm/react/index.cjs"
49
+ },
50
+ "./pm/html-string": {
51
+ "types": {
52
+ "import": "./dist/pm/html-string/index.d.ts",
53
+ "require": "./dist/pm/html-string/index.d.cts"
54
+ },
55
+ "import": "./dist/pm/html-string/index.js",
56
+ "require": "./dist/pm/html-string/index.cjs"
57
+ },
58
+ "./pm/markdown": {
59
+ "types": {
60
+ "import": "./dist/pm/markdown/index.d.ts",
61
+ "require": "./dist/pm/markdown/index.d.cts"
62
+ },
63
+ "import": "./dist/pm/markdown/index.js",
64
+ "require": "./dist/pm/markdown/index.cjs"
65
+ }
66
+ },
67
+ "main": "dist/index.cjs",
68
+ "module": "dist/index.js",
69
+ "types": "dist/index.d.ts",
70
+ "files": [
71
+ "src",
72
+ "dist"
73
+ ],
74
+ "devDependencies": {
75
+ "@tiptap/core": "^3.0.0-beta.0",
76
+ "@tiptap/pm": "^3.0.0-beta.0",
77
+ "@types/react": "^18.2.14",
78
+ "@types/react-dom": "^18.2.6",
79
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
80
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
81
+ },
82
+ "peerDependencies": {
83
+ "@tiptap/core": "^3.0.0-beta.0",
84
+ "@tiptap/pm": "^3.0.0-beta.0"
85
+ },
86
+ "optionalDependencies": {
87
+ "@types/react": "^18.2.14",
88
+ "@types/react-dom": "^18.2.6",
89
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
90
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
91
+ },
92
+ "repository": {
93
+ "type": "git",
94
+ "url": "https://github.com/ueberdosis/tiptap",
95
+ "directory": "packages/static-renderer"
96
+ },
97
+ "scripts": {
98
+ "build": "tsup",
99
+ "lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
100
+ }
101
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,54 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { type ExtensionAttribute, type MarkType, type NodeType, mergeAttributes } from '@tiptap/core'
3
+
4
+ /**
5
+ * This function returns the attributes of a node or mark that are defined by the given extension attributes.
6
+ * @param nodeOrMark The node or mark to get the attributes from
7
+ * @param extensionAttributes The extension attributes to use
8
+ * @param onlyRenderedAttributes If true, only attributes that are rendered in the HTML are returned
9
+ */
10
+ export function getAttributes(
11
+ nodeOrMark: NodeType | MarkType,
12
+ extensionAttributes: ExtensionAttribute[],
13
+ onlyRenderedAttributes?: boolean,
14
+ ): Record<string, any> {
15
+ const nodeOrMarkAttributes = nodeOrMark.attrs
16
+
17
+ if (!nodeOrMarkAttributes) {
18
+ return {}
19
+ }
20
+
21
+ return extensionAttributes
22
+ .filter(item => {
23
+ if (item.type !== (typeof nodeOrMark.type === 'string' ? nodeOrMark.type : nodeOrMark.type.name)) {
24
+ return false
25
+ }
26
+ if (onlyRenderedAttributes) {
27
+ return item.attribute.rendered
28
+ }
29
+ return true
30
+ })
31
+ .map(item => {
32
+ if (!item.attribute.renderHTML) {
33
+ return {
34
+ [item.name]: item.name in nodeOrMarkAttributes ? nodeOrMarkAttributes[item.name] : item.attribute.default,
35
+ }
36
+ }
37
+
38
+ return (
39
+ item.attribute.renderHTML(nodeOrMarkAttributes) || {
40
+ [item.name]: item.name in nodeOrMarkAttributes ? nodeOrMarkAttributes[item.name] : item.attribute.default,
41
+ }
42
+ )
43
+ })
44
+ .reduce((attributes, attribute) => mergeAttributes(attributes, attribute), {})
45
+ }
46
+
47
+ /**
48
+ * This function returns the HTML attributes of a node or mark that are defined by the given extension attributes.
49
+ * @param nodeOrMark The node or mark to get the attributes from
50
+ * @param extensionAttributes The extension attributes to use
51
+ */
52
+ export function getHTMLAttributes(nodeOrMark: NodeType | MarkType, extensionAttributes: ExtensionAttribute[]) {
53
+ return getAttributes(nodeOrMark, extensionAttributes, true)
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './helpers.js'
2
+ export * from './json/html-string/index.js'
3
+ export * from './json/react/index.js'
4
+ export * from './pm/html-string/index.js'
5
+ export * from './pm/markdown/index.js'
6
+ export * from './pm/react/index.js'
@@ -0,0 +1,2 @@
1
+ export * from '../renderer.js'
2
+ export * from './string.js'
@@ -0,0 +1,49 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { MarkType, NodeType } from '@tiptap/core'
3
+
4
+ import type { TiptapStaticRendererOptions } from '../renderer.js'
5
+ import { TiptapStaticRenderer } from '../renderer.js'
6
+
7
+ export function renderJSONContentToString<
8
+ /**
9
+ * A mark type is either a JSON representation of a mark or a Prosemirror mark instance
10
+ */
11
+ TMarkType extends { type: any } = MarkType,
12
+ /**
13
+ * A node type is either a JSON representation of a node or a Prosemirror node instance
14
+ */
15
+ TNodeType extends {
16
+ content?: { forEach: (cb: (node: TNodeType) => void) => void }
17
+ marks?: readonly TMarkType[]
18
+ type: string | { name: string }
19
+ } = NodeType,
20
+ >(options: TiptapStaticRendererOptions<string, TMarkType, TNodeType>) {
21
+ return TiptapStaticRenderer(ctx => {
22
+ return ctx.component(ctx.props as any)
23
+ }, options)
24
+ }
25
+
26
+ /**
27
+ * Serialize the attributes of a node or mark to a string
28
+ * @param attrs The attributes to serialize
29
+ * @returns The serialized attributes as a string
30
+ */
31
+ export function serializeAttrsToHTMLString(attrs: Record<string, any> | undefined | null): string {
32
+ const output = Object.entries(attrs || {})
33
+ .map(([key, value]) => `${key.split(' ').at(-1)}=${JSON.stringify(value)}`)
34
+ .join(' ')
35
+
36
+ return output ? ` ${output}` : ''
37
+ }
38
+
39
+ /**
40
+ * Serialize the children of a node or mark to a string
41
+ * @param children The children to serialize
42
+ * @returns The serialized children as a string
43
+ */
44
+ export function serializeChildrenToHTMLString(children?: string | string[]): string {
45
+ return ([] as string[])
46
+ .concat(children || '')
47
+ .filter(Boolean)
48
+ .join('')
49
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../renderer.js'
2
+ export * from './react.js'
@@ -0,0 +1,33 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import type { MarkType, NodeType } from '@tiptap/core'
4
+ import React from 'react'
5
+
6
+ import type { TiptapStaticRendererOptions } from '../renderer.js'
7
+ import { TiptapStaticRenderer } from '../renderer.js'
8
+
9
+ export function renderJSONContentToReactElement<
10
+ /**
11
+ * A mark type is either a JSON representation of a mark or a Prosemirror mark instance
12
+ */
13
+ TMarkType extends { type: any } = MarkType,
14
+ /**
15
+ * A node type is either a JSON representation of a node or a Prosemirror node instance
16
+ */
17
+ TNodeType extends {
18
+ content?: { forEach: (cb: (node: TNodeType) => void) => void }
19
+ marks?: readonly TMarkType[]
20
+ type: string | { name: string }
21
+ } = NodeType,
22
+ >(options: TiptapStaticRendererOptions<React.ReactNode, TMarkType, TNodeType>) {
23
+ let key = 0
24
+
25
+ return TiptapStaticRenderer<React.ReactNode, TMarkType, TNodeType>(({ 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
+ }, options)
33
+ }
@@ -0,0 +1,241 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { MarkType, NodeType } from '@tiptap/core'
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, NoInfer<TNodeRender>>
91
+ /**
92
+ * Mapping of mark types to react components
93
+ */
94
+ markMapping: Record<string, NoInfer<TMarkRender>>
95
+ /**
96
+ * Component to render if a node type is not handled
97
+ */
98
+ unhandledNode?: NoInfer<TNodeRender>
99
+ /**
100
+ * Component to render if a mark type is not handled
101
+ */
102
+ unhandledMark?: NoInfer<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: (cb: (node: TNodeType) => void) => void }
130
+ marks?: readonly TMarkType[]
131
+ type: string | { name: string }
132
+ } = NodeType,
133
+ /**
134
+ * A node renderer is a function that takes a node and its children and returns the rendered output
135
+ */
136
+ TNodeRender extends (ctx: NodeProps<TNodeType, TReturnType | TReturnType[]>) => TReturnType = (
137
+ ctx: NodeProps<TNodeType, TReturnType | TReturnType[]>,
138
+ ) => TReturnType,
139
+ /**
140
+ * A mark renderer is a function that takes a mark and its children and returns the rendered output
141
+ */
142
+ TMarkRender extends (ctx: MarkProps<TMarkType, TReturnType | TReturnType[], TNodeType>) => TReturnType = (
143
+ ctx: MarkProps<TMarkType, TReturnType | TReturnType[], TNodeType>,
144
+ ) => TReturnType,
145
+ >(
146
+ /**
147
+ * The function that actually renders the component
148
+ */
149
+ renderComponent: (
150
+ ctx:
151
+ | {
152
+ component: TNodeRender
153
+ props: NodeProps<TNodeType, TReturnType | TReturnType[]>
154
+ }
155
+ | {
156
+ component: TMarkRender
157
+ props: MarkProps<TMarkType, TReturnType | TReturnType[], TNodeType>
158
+ },
159
+ ) => TReturnType,
160
+ {
161
+ nodeMapping,
162
+ markMapping,
163
+ unhandledNode,
164
+ unhandledMark,
165
+ }: TiptapStaticRendererOptions<TReturnType, TMarkType, TNodeType, TNodeRender, TMarkRender>,
166
+ ) {
167
+ /**
168
+ * Render Tiptap JSON and all its children using the provided node and mark mappings.
169
+ */
170
+ return function renderContent({
171
+ content,
172
+ parent,
173
+ }: {
174
+ /**
175
+ * Tiptap JSON content to render
176
+ */
177
+ content: TNodeType
178
+ /**
179
+ * The parent node of the current node
180
+ */
181
+ parent?: TNodeType
182
+ }): TReturnType {
183
+ const nodeType = typeof content.type === 'string' ? content.type : content.type.name
184
+ const NodeHandler = nodeMapping[nodeType] ?? unhandledNode
185
+
186
+ if (!NodeHandler) {
187
+ throw new Error(`missing handler for node type ${nodeType}`)
188
+ }
189
+
190
+ const nodeContent = renderComponent({
191
+ component: NodeHandler,
192
+ props: {
193
+ node: content,
194
+ parent,
195
+ renderElement: renderContent,
196
+ // Lazily compute the children to avoid unnecessary recursion
197
+ get children() {
198
+ // recursively render child content nodes
199
+ const children: TReturnType[] = []
200
+
201
+ if (content.content) {
202
+ content.content.forEach(child => {
203
+ children.push(
204
+ renderContent({
205
+ content: child,
206
+ parent: content,
207
+ }),
208
+ )
209
+ })
210
+ }
211
+
212
+ return children
213
+ },
214
+ },
215
+ })
216
+
217
+ // apply marks to the content
218
+ const markedContent = content.marks
219
+ ? content.marks.reduce((acc, mark) => {
220
+ const markType = typeof mark.type === 'string' ? mark.type : mark.type.name
221
+ const MarkHandler = markMapping[markType] ?? unhandledMark
222
+
223
+ if (!MarkHandler) {
224
+ throw new Error(`missing handler for mark type ${markType}`)
225
+ }
226
+
227
+ return renderComponent({
228
+ component: MarkHandler,
229
+ props: {
230
+ mark,
231
+ parent,
232
+ node: content,
233
+ children: acc,
234
+ },
235
+ })
236
+ }, nodeContent)
237
+ : nodeContent
238
+
239
+ return markedContent
240
+ }
241
+ }