@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.
Files changed (57) hide show
  1. package/README.md +18 -0
  2. package/dist/index.cjs +62 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +52 -0
  5. package/dist/index.d.ts +52 -0
  6. package/dist/index.js +34 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/json/html-string/index.cjs +100 -0
  9. package/dist/json/html-string/index.cjs.map +1 -0
  10. package/dist/json/html-string/index.d.cts +205 -0
  11. package/dist/json/html-string/index.d.ts +205 -0
  12. package/dist/json/html-string/index.js +72 -0
  13. package/dist/json/html-string/index.js.map +1 -0
  14. package/dist/json/react/index.cjs +2306 -0
  15. package/dist/json/react/index.cjs.map +1 -0
  16. package/dist/json/react/index.d.cts +207 -0
  17. package/dist/json/react/index.d.ts +207 -0
  18. package/dist/json/react/index.js +2291 -0
  19. package/dist/json/react/index.js.map +1 -0
  20. package/dist/json/renderer.cjs +89 -0
  21. package/dist/json/renderer.cjs.map +1 -0
  22. package/dist/json/renderer.d.cts +182 -0
  23. package/dist/json/renderer.d.ts +182 -0
  24. package/dist/json/renderer.js +64 -0
  25. package/dist/json/renderer.js.map +1 -0
  26. package/dist/pm/html-string/index.cjs +359 -0
  27. package/dist/pm/html-string/index.cjs.map +1 -0
  28. package/dist/pm/html-string/index.d.cts +192 -0
  29. package/dist/pm/html-string/index.d.ts +192 -0
  30. package/dist/pm/html-string/index.js +332 -0
  31. package/dist/pm/html-string/index.js.map +1 -0
  32. package/dist/pm/react/index.cjs +2588 -0
  33. package/dist/pm/react/index.cjs.map +1 -0
  34. package/dist/pm/react/index.d.cts +181 -0
  35. package/dist/pm/react/index.d.ts +181 -0
  36. package/dist/pm/react/index.js +2576 -0
  37. package/dist/pm/react/index.js.map +1 -0
  38. package/package.json +82 -0
  39. package/src/helpers.example.ts +35 -0
  40. package/src/helpers.ts +65 -0
  41. package/src/index.ts +2 -0
  42. package/src/json/html-string/index.ts +2 -0
  43. package/src/json/html-string/string.example.ts +46 -0
  44. package/src/json/html-string/string.ts +22 -0
  45. package/src/json/react/index.ts +2 -0
  46. package/src/json/react/react.example.ts +45 -0
  47. package/src/json/react/react.tsx +35 -0
  48. package/src/json/renderer.ts +242 -0
  49. package/src/pm/extensionRenderer.ts +230 -0
  50. package/src/pm/html-string/html-string.example.ts +225 -0
  51. package/src/pm/html-string/html-string.ts +121 -0
  52. package/src/pm/html-string/index.ts +2 -0
  53. package/src/pm/markdown/markdown.example.ts +296 -0
  54. package/src/pm/react/index.ts +2 -0
  55. package/src/pm/react/react.example.tsx +306 -0
  56. package/src/pm/react/react.tsx +133 -0
  57. package/src/types.ts +57 -0
@@ -0,0 +1,306 @@
1
+ /* eslint-disable no-plusplus */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+
4
+ import {
5
+ Node,
6
+ NodeViewContent,
7
+ ReactNodeViewContentProvider,
8
+ ReactNodeViewRenderer,
9
+ } from '@tiptap/react'
10
+ import StarterKit from '@tiptap/starter-kit'
11
+ import React from 'react'
12
+ import { renderToStaticMarkup } from 'react-dom/server'
13
+
14
+ import { renderToReactElement } from './react.jsx'
15
+
16
+ // This component does not have a NodeViewContent, so it does not render it's children's rich text content
17
+ function MyCustomComponentWithoutContent() {
18
+ const [count, setCount] = React.useState(200)
19
+
20
+ return (
21
+ <div className='custom-component-without-content' onClick={() => setCount(a => a + 1)}>
22
+ {count} This is a react component!
23
+ </div>
24
+ )
25
+ }
26
+
27
+ // This component does have a NodeViewContent, so it will render it's children's rich text content
28
+ function MyCustomComponentWithContent() {
29
+ return (
30
+ <div className='custom-component-with-content'>
31
+ Custom component with content in React!
32
+ <NodeViewContent />
33
+ </div>
34
+ )
35
+ }
36
+
37
+ /**
38
+ * This example demonstrates how to render a Prosemirror Node (or JSON Content) to a React Element.
39
+ * It will use your extensions to render the content based on each Node's/Mark's `renderHTML` method.
40
+ * This can be useful if you want to render content to React without having an actual editor instance.
41
+ *
42
+ * You have complete control over the rendering process. And can replace how each Node/Mark is rendered.
43
+ */
44
+
45
+ const CustomNodeExtensionWithContent = Node.create({
46
+ name: 'customNodeExtensionWithContent',
47
+ content: 'text*',
48
+ group: 'block',
49
+ renderHTML() {
50
+ return ['div', { class: 'my-custom-component-with-content' }, 0] as const
51
+ },
52
+ addNodeView() {
53
+ return ReactNodeViewRenderer(MyCustomComponentWithContent)
54
+ },
55
+ })
56
+
57
+ const CustomNodeExtensionWithoutContent = Node.create({
58
+ name: 'customNodeExtensionWithoutContent',
59
+ atom: true,
60
+ renderHTML() {
61
+ return ['div', { class: 'my-custom-component-without-content' }] as const
62
+ },
63
+ addNodeView() {
64
+ return ReactNodeViewRenderer(MyCustomComponentWithoutContent)
65
+ },
66
+ })
67
+
68
+ const Element = renderToReactElement({
69
+ extensions: [StarterKit, CustomNodeExtensionWithContent, CustomNodeExtensionWithoutContent],
70
+ options: {
71
+ nodeMapping: {
72
+ // You can replace the rendering of a node with a custom react component
73
+ heading({ node, children }) {
74
+ // eslint-disable-next-line react-hooks/rules-of-hooks
75
+ const [count, setCount] = React.useState(100)
76
+
77
+ return <h1 {...node.attrs} onClick={() => setCount(100)}>Can you use React hooks? {count}% {children}</h1>
78
+ },
79
+ // Node views are not supported in the static renderer, so you need to supply the custom component yourself
80
+ customNodeExtensionWithContent({ children }) {
81
+ return (
82
+ <ReactNodeViewContentProvider content={children}>
83
+ <MyCustomComponentWithContent />
84
+ </ReactNodeViewContentProvider>
85
+ )
86
+ },
87
+ customNodeExtensionWithoutContent() {
88
+ return <MyCustomComponentWithoutContent />
89
+ },
90
+ },
91
+ markMapping: {},
92
+ },
93
+ content: {
94
+ type: 'doc',
95
+ from: 0,
96
+ to: 574,
97
+ content: [
98
+ {
99
+ type: 'heading',
100
+ from: 0,
101
+ to: 11,
102
+ attrs: {
103
+ level: 2,
104
+ },
105
+ content: [
106
+ {
107
+ type: 'text',
108
+ from: 1,
109
+ to: 10,
110
+ text: 'Hi there,',
111
+ },
112
+ ],
113
+ },
114
+ // This is a custom node extension with content
115
+ {
116
+ type: 'customNodeExtensionWithContent',
117
+ content: [
118
+ {
119
+ type: 'text',
120
+ text: 'MY CUSTOM COMPONENT CONTENT!!!',
121
+ },
122
+ ],
123
+ },
124
+ // This is a custom node extension without content
125
+ {
126
+ type: 'customNodeExtensionWithoutContent',
127
+ },
128
+ {
129
+ type: 'paragraph',
130
+ from: 11,
131
+ to: 169,
132
+ content: [
133
+ {
134
+ type: 'text',
135
+ from: 12,
136
+ to: 22,
137
+ text: 'this is a ',
138
+ },
139
+ {
140
+ type: 'text',
141
+ from: 22,
142
+ to: 27,
143
+ marks: [
144
+ {
145
+ type: 'italic',
146
+ },
147
+ ],
148
+ text: 'basic',
149
+ },
150
+ {
151
+ type: 'text',
152
+ from: 27,
153
+ to: 39,
154
+ text: ' example of ',
155
+ },
156
+ {
157
+ type: 'text',
158
+ from: 39,
159
+ to: 45,
160
+ marks: [
161
+ {
162
+ type: 'bold',
163
+ },
164
+ ],
165
+ text: 'Tiptap',
166
+ },
167
+ {
168
+ type: 'text',
169
+ from: 45,
170
+ to: 168,
171
+ text: '. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:',
172
+ },
173
+ ],
174
+ },
175
+ {
176
+ type: 'bulletList',
177
+ from: 169,
178
+ to: 230,
179
+ content: [
180
+ {
181
+ type: 'listItem',
182
+ from: 170,
183
+ to: 205,
184
+ attrs: {
185
+ color: '',
186
+ },
187
+ content: [
188
+ {
189
+ type: 'paragraph',
190
+ from: 171,
191
+ to: 204,
192
+ content: [
193
+ {
194
+ type: 'text',
195
+ from: 172,
196
+ to: 203,
197
+ text: 'That’s a bullet list with one …',
198
+ },
199
+ ],
200
+ },
201
+ ],
202
+ },
203
+ {
204
+ type: 'listItem',
205
+ from: 205,
206
+ to: 229,
207
+ attrs: {
208
+ color: '',
209
+ },
210
+ content: [
211
+ {
212
+ type: 'paragraph',
213
+ from: 206,
214
+ to: 228,
215
+ content: [
216
+ {
217
+ type: 'text',
218
+ from: 207,
219
+ to: 227,
220
+ text: '… or two list items.',
221
+ },
222
+ ],
223
+ },
224
+ ],
225
+ },
226
+ ],
227
+ },
228
+ {
229
+ type: 'paragraph',
230
+ from: 230,
231
+ to: 326,
232
+ content: [
233
+ {
234
+ type: 'text',
235
+ from: 231,
236
+ to: 325,
237
+ text: 'Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:',
238
+ },
239
+ ],
240
+ },
241
+ {
242
+ type: 'codeBlock',
243
+ from: 326,
244
+ to: 353,
245
+ attrs: {
246
+ language: 'css',
247
+ },
248
+ content: [
249
+ {
250
+ type: 'text',
251
+ from: 327,
252
+ to: 352,
253
+ text: 'body {\n display: none;\n}',
254
+ },
255
+ ],
256
+ },
257
+ {
258
+ type: 'paragraph',
259
+ from: 353,
260
+ to: 522,
261
+ content: [
262
+ {
263
+ type: 'text',
264
+ from: 354,
265
+ to: 521,
266
+ text: 'I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too.',
267
+ },
268
+ ],
269
+ },
270
+ {
271
+ type: 'blockquote',
272
+ from: 522,
273
+ to: 572,
274
+ content: [
275
+ {
276
+ type: 'paragraph',
277
+ from: 523,
278
+ to: 571,
279
+ content: [
280
+ {
281
+ type: 'text',
282
+ from: 524,
283
+ to: 564,
284
+ text: 'Wow, that’s amazing. Good work, boy! 👏 ',
285
+ },
286
+ {
287
+ type: 'hardBreak',
288
+ from: 564,
289
+ to: 565,
290
+ },
291
+ {
292
+ type: 'text',
293
+ from: 565,
294
+ to: 570,
295
+ text: '— Mom',
296
+ },
297
+ ],
298
+ },
299
+ ],
300
+ },
301
+ ],
302
+ },
303
+ })
304
+
305
+ // eslint-disable-next-line no-console
306
+ console.log(renderToStaticMarkup(Element))
@@ -0,0 +1,133 @@
1
+ /* eslint-disable no-plusplus, @typescript-eslint/no-explicit-any */
2
+ import { 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 { TiptapStaticRendererOptions } from '../../json/renderer.js'
8
+ import type { DOMOutputSpecArray } from '../../types.js'
9
+ import { renderToElement } from '../extensionRenderer.js'
10
+
11
+ /**
12
+ * This function maps the attributes of a node or mark to HTML attributes
13
+ * @param attrs The attributes to map
14
+ * @param key The key to use for the React element
15
+ * @returns The mapped HTML attributes as an object
16
+ */
17
+ function mapAttrsToHTMLAttributes(attrs?: Record<string, any>, key?: string): Record<string, any> {
18
+ if (!attrs) {
19
+ return { key }
20
+ }
21
+ return Object.entries(attrs).reduce(
22
+ (acc, [name, value]) => {
23
+ if (name === 'class') {
24
+ return Object.assign(acc, { className: value })
25
+ }
26
+ return Object.assign(acc, { [name]: value })
27
+ },
28
+ { key },
29
+ )
30
+ }
31
+
32
+ /**
33
+ * Take a DOMOutputSpec and return a function that can render it to a React element
34
+ * @param content The DOMOutputSpec to convert to a React element
35
+ * @returns A function that can render the DOMOutputSpec to a React element
36
+ */
37
+ export function domOutputSpecToReactElement(
38
+ content: DOMOutputSpec,
39
+ key = 0,
40
+ ): (children?: React.ReactNode) => React.ReactNode {
41
+ if (typeof content === 'string') {
42
+ return () => content
43
+ }
44
+ if (typeof content === 'object' && 'length' in content) {
45
+ const [tag, attrs, children, ...rest] = content as DOMOutputSpecArray
46
+
47
+ if (attrs === undefined) {
48
+ return () => React.createElement(tag, mapAttrsToHTMLAttributes(undefined, key.toString()))
49
+ }
50
+ if (attrs === 0) {
51
+ return child => React.createElement(tag, mapAttrsToHTMLAttributes(undefined, key.toString()), child)
52
+ }
53
+ if (typeof attrs === 'object') {
54
+ if (Array.isArray(attrs)) {
55
+ if (children === undefined) {
56
+ return child => React.createElement(
57
+ tag,
58
+ mapAttrsToHTMLAttributes(undefined, key.toString()),
59
+ domOutputSpecToReactElement(attrs as DOMOutputSpecArray, key++)(child),
60
+ )
61
+ }
62
+ if (children === 0) {
63
+ return child => React.createElement(
64
+ tag,
65
+ mapAttrsToHTMLAttributes(undefined, key.toString()),
66
+ domOutputSpecToReactElement(attrs as DOMOutputSpecArray, key++)(child),
67
+ )
68
+ }
69
+ return child => React.createElement(
70
+ tag,
71
+ mapAttrsToHTMLAttributes(undefined, key.toString()),
72
+ domOutputSpecToReactElement(attrs as DOMOutputSpecArray)(child),
73
+ [children]
74
+ .concat(rest)
75
+ .map(outputSpec => domOutputSpecToReactElement(outputSpec, key++)(child)),
76
+ )
77
+ }
78
+ if (children === undefined) {
79
+ return () => React.createElement(tag, mapAttrsToHTMLAttributes(attrs, key.toString()))
80
+ }
81
+ if (children === 0) {
82
+ return child => React.createElement(tag, mapAttrsToHTMLAttributes(attrs, key.toString()), child)
83
+ }
84
+
85
+ return child => React.createElement(
86
+ tag,
87
+ mapAttrsToHTMLAttributes(attrs, key.toString()),
88
+ [children]
89
+ .concat(rest)
90
+ .map(outputSpec => domOutputSpecToReactElement(outputSpec, key++)(child)),
91
+ )
92
+ }
93
+ }
94
+
95
+ // TODO support DOM elements? How to handle them?
96
+ throw new Error(
97
+ '[tiptap error]: Unsupported DomOutputSpec type, check the `renderHTML` method output',
98
+ {
99
+ cause: content,
100
+ },
101
+ )
102
+ }
103
+
104
+ /**
105
+ * This function will statically render a Prosemirror Node to a React component using the given extensions
106
+ * @param content The content to render to a React component
107
+ * @param extensions The extensions to use for rendering
108
+ * @param options The options to use for rendering
109
+ * @returns The React element that represents the rendered content
110
+ */
111
+ export function renderToReactElement({
112
+ content,
113
+ extensions,
114
+ options,
115
+ }: {
116
+ content: Node | JSONContent;
117
+ extensions: Extensions;
118
+ options?: Partial<TiptapStaticRendererOptions<React.ReactNode, Mark, Node>>;
119
+ }): React.ReactNode {
120
+ return renderToElement<React.ReactNode>({
121
+ renderer: renderJSONContentToReactElement,
122
+ domOutputSpecToElement: domOutputSpecToReactElement,
123
+ mapDefinedTypes: {
124
+ // Map a doc node to concatenated children
125
+ doc: ({ children }) => <>{children}</>,
126
+ // Map a text node to its text content
127
+ text: ({ node }) => node.text ?? '',
128
+ },
129
+ content,
130
+ extensions,
131
+ options,
132
+ })
133
+ }
package/src/types.ts ADDED
@@ -0,0 +1,57 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ /**
4
+ * A mark type is either a JSON representation of a mark or a Prosemirror mark instance
5
+ */
6
+ export type MarkType<
7
+ Type extends string = any,
8
+ Attributes extends undefined | Record<string, any> = any,
9
+ > = {
10
+ type: Type;
11
+ attrs: Attributes;
12
+ };
13
+
14
+ /**
15
+ * A node type is either a JSON representation of a node or a Prosemirror node instance
16
+ */
17
+ export type NodeType<
18
+ Type extends string = any,
19
+ Attributes extends undefined | Record<string, any> = any,
20
+ NodeMarkType extends MarkType = any,
21
+ Content extends NodeType[] = any,
22
+ > = {
23
+ type: Type;
24
+ attrs: Attributes;
25
+ content?: Content;
26
+ marks?: NodeMarkType[];
27
+ text?: string;
28
+ };
29
+
30
+ /**
31
+ * A node type is either a JSON representation of a doc node or a Prosemirror doc node instance
32
+ */
33
+ export type DocumentType<
34
+ TNodeAttributes extends Record<string, any> = Record<string, any>,
35
+ TContentType extends NodeType[] = NodeType[],
36
+ > = NodeType<'doc', TNodeAttributes, never, TContentType>;
37
+
38
+ /**
39
+ * A node type is either a JSON representation of a text node or a Prosemirror text node instance
40
+ */
41
+ export type TextType<TMarkType extends MarkType = MarkType> = {
42
+ type: 'text';
43
+ text: string;
44
+ marks: TMarkType[];
45
+ };
46
+
47
+ /**
48
+ * Describes the output of a `renderHTML` function in prosemirror
49
+ * @see https://prosemirror.net/docs/ref/#model.DOMOutputSpec
50
+ */
51
+ export type DOMOutputSpecArray =
52
+ | [string]
53
+ | [string, Record<string, any>]
54
+ | [string, 0]
55
+ | [string, Record<string, any>, 0]
56
+ | [string, Record<string, any>, DOMOutputSpecArray | 0]
57
+ | [string, DOMOutputSpecArray];