@tiptap/static-renderer 3.0.0-next.1 → 3.0.0-next.4

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 (60) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +1 -1
  3. package/dist/index.cjs +573 -6
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +307 -32
  6. package/dist/index.d.ts +307 -32
  7. package/dist/index.js +552 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/json/html-string/index.cjs +16 -5
  10. package/dist/json/html-string/index.cjs.map +1 -1
  11. package/dist/json/html-string/index.d.cts +18 -22
  12. package/dist/json/html-string/index.d.ts +18 -22
  13. package/dist/json/html-string/index.js +10 -1
  14. package/dist/json/html-string/index.js.map +1 -1
  15. package/dist/json/react/index.cjs +12 -2201
  16. package/dist/json/react/index.cjs.map +1 -1
  17. package/dist/json/react/index.d.cts +5 -22
  18. package/dist/json/react/index.d.ts +5 -22
  19. package/dist/json/react/index.js +9 -2221
  20. package/dist/json/react/index.js.map +1 -1
  21. package/dist/json/renderer.cjs.map +1 -1
  22. package/dist/json/renderer.d.cts +5 -21
  23. package/dist/json/renderer.d.ts +5 -21
  24. package/dist/json/renderer.js.map +1 -1
  25. package/dist/pm/html-string/index.cjs +22 -37
  26. package/dist/pm/html-string/index.cjs.map +1 -1
  27. package/dist/pm/html-string/index.d.cts +7 -24
  28. package/dist/pm/html-string/index.d.ts +7 -24
  29. package/dist/pm/html-string/index.js +19 -34
  30. package/dist/pm/html-string/index.js.map +1 -1
  31. package/dist/pm/markdown/index.cjs +473 -0
  32. package/dist/pm/markdown/index.cjs.map +1 -0
  33. package/dist/pm/markdown/index.d.cts +153 -0
  34. package/dist/pm/markdown/index.d.ts +153 -0
  35. package/dist/pm/markdown/index.js +449 -0
  36. package/dist/pm/markdown/index.js.map +1 -0
  37. package/dist/pm/react/index.cjs +41 -2230
  38. package/dist/pm/react/index.cjs.map +1 -1
  39. package/dist/pm/react/index.d.cts +5 -23
  40. package/dist/pm/react/index.d.ts +5 -23
  41. package/dist/pm/react/index.js +47 -2259
  42. package/dist/pm/react/index.js.map +1 -1
  43. package/package.json +27 -8
  44. package/src/helpers.ts +5 -16
  45. package/src/index.ts +5 -1
  46. package/src/json/html-string/string.ts +39 -13
  47. package/src/json/react/react.tsx +12 -15
  48. package/src/json/renderer.ts +50 -51
  49. package/src/pm/extensionRenderer.ts +16 -34
  50. package/src/pm/html-string/html-string.ts +29 -45
  51. package/src/pm/markdown/index.ts +2 -0
  52. package/src/pm/markdown/markdown.ts +142 -0
  53. package/src/pm/react/react.tsx +49 -30
  54. package/src/helpers.example.ts +0 -35
  55. package/src/json/html-string/string.example.ts +0 -46
  56. package/src/json/react/react.example.ts +0 -45
  57. package/src/pm/html-string/html-string.example.ts +0 -225
  58. package/src/pm/markdown/markdown.example.ts +0 -296
  59. package/src/pm/react/react.example.tsx +0 -306
  60. package/src/types.ts +0 -57
@@ -0,0 +1,142 @@
1
+ import { Extensions, JSONContent } from '@tiptap/core'
2
+ import type { Mark, Node } from '@tiptap/pm/model'
3
+
4
+ import { 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
+ }
@@ -1,11 +1,10 @@
1
1
  /* eslint-disable no-plusplus, @typescript-eslint/no-explicit-any */
2
- import { Extensions, JSONContent } from '@tiptap/core'
2
+ import type { DOMOutputSpecArray, Extensions, JSONContent } from '@tiptap/core'
3
3
  import type { DOMOutputSpec, Mark, Node } from '@tiptap/pm/model'
4
4
  import React from 'react'
5
5
 
6
6
  import { renderJSONContentToReactElement } from '../../json/react/react.js'
7
7
  import { TiptapStaticRendererOptions } from '../../json/renderer.js'
8
- import type { DOMOutputSpecArray } from '../../types.js'
9
8
  import { renderToElement } from '../extensionRenderer.js'
10
9
 
11
10
  /**
@@ -42,7 +41,27 @@ export function domOutputSpecToReactElement(
42
41
  return () => content
43
42
  }
44
43
  if (typeof content === 'object' && 'length' in content) {
45
- const [tag, attrs, children, ...rest] = content as DOMOutputSpecArray
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
+ }
46
65
 
47
66
  if (attrs === undefined) {
48
67
  return () => React.createElement(tag, mapAttrsToHTMLAttributes(undefined, key.toString()))
@@ -53,27 +72,28 @@ export function domOutputSpecToReactElement(
53
72
  if (typeof attrs === 'object') {
54
73
  if (Array.isArray(attrs)) {
55
74
  if (children === undefined) {
56
- return child => React.createElement(
57
- tag,
58
- mapAttrsToHTMLAttributes(undefined, key.toString()),
59
- domOutputSpecToReactElement(attrs as DOMOutputSpecArray, key++)(child),
60
- )
75
+ return child =>
76
+ React.createElement(
77
+ tag,
78
+ mapAttrsToHTMLAttributes(undefined, key.toString()),
79
+ domOutputSpecToReactElement(attrs as DOMOutputSpecArray, key++)(child),
80
+ )
61
81
  }
62
82
  if (children === 0) {
63
- return child => React.createElement(
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(
64
92
  tag,
65
93
  mapAttrsToHTMLAttributes(undefined, key.toString()),
66
- domOutputSpecToReactElement(attrs as DOMOutputSpecArray, key++)(child),
94
+ domOutputSpecToReactElement(attrs as DOMOutputSpecArray)(child),
95
+ [children].concat(rest).map(outputSpec => domOutputSpecToReactElement(outputSpec, key++)(child)),
67
96
  )
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
97
  }
78
98
  if (children === undefined) {
79
99
  return () => React.createElement(tag, mapAttrsToHTMLAttributes(attrs, key.toString()))
@@ -82,19 +102,18 @@ export function domOutputSpecToReactElement(
82
102
  return child => React.createElement(tag, mapAttrsToHTMLAttributes(attrs, key.toString()), child)
83
103
  }
84
104
 
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
- )
105
+ return child =>
106
+ React.createElement(
107
+ tag,
108
+ mapAttrsToHTMLAttributes(attrs, key.toString()),
109
+ [children].concat(rest).map(outputSpec => domOutputSpecToReactElement(outputSpec, key++)(child)),
110
+ )
92
111
  }
93
112
  }
94
113
 
95
114
  // TODO support DOM elements? How to handle them?
96
115
  throw new Error(
97
- '[tiptap error]: Unsupported DomOutputSpec type, check the `renderHTML` method output',
116
+ '[tiptap error]: Unsupported DomOutputSpec type, check the `renderHTML` method output or implement a node mapping',
98
117
  {
99
118
  cause: content,
100
119
  },
@@ -113,9 +132,9 @@ export function renderToReactElement({
113
132
  extensions,
114
133
  options,
115
134
  }: {
116
- content: Node | JSONContent;
117
- extensions: Extensions;
118
- options?: Partial<TiptapStaticRendererOptions<React.ReactNode, Mark, Node>>;
135
+ content: Node | JSONContent
136
+ extensions: Extensions
137
+ options?: Partial<TiptapStaticRendererOptions<React.ReactNode, Mark, Node>>
119
138
  }): React.ReactNode {
120
139
  return renderToElement<React.ReactNode>({
121
140
  renderer: renderJSONContentToReactElement,
@@ -1,35 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { getAttributesFromExtensions, resolveExtensions } from '@tiptap/core'
3
- import { TextAlign } from '@tiptap/extension-text-align'
4
- import { TextStyle } from '@tiptap/extension-text-style'
5
- import StarterKit from '@tiptap/starter-kit'
6
-
7
- import { getAttributes } from './helpers.js'
8
-
9
- const extensionAttributes = getAttributesFromExtensions(
10
- resolveExtensions([
11
- StarterKit,
12
- TextAlign.configure({
13
- types: ['paragraph', 'heading'],
14
- }),
15
- TextStyle,
16
- ]),
17
- )
18
- const attributes = getAttributes(
19
- {
20
- type: 'heading',
21
- attrs: {
22
- textAlign: 'right',
23
- },
24
- content: [
25
- {
26
- type: 'text',
27
- text: 'hello world',
28
- },
29
- ],
30
- },
31
- extensionAttributes,
32
- )
33
-
34
- // eslint-disable-next-line no-console
35
- console.log(attributes)
@@ -1,46 +0,0 @@
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
- )
@@ -1,45 +0,0 @@
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
- }))
@@ -1,225 +0,0 @@
1
- import StarterKit from '@tiptap/starter-kit'
2
-
3
- import { renderToHTMLString, serializeAttrsToHTMLString, serializeChildrenToHTMLString } from './html-string.js'
4
-
5
- /**
6
- * This example demonstrates how to render a Prosemirror Node (or JSON Content) to an HTML string.
7
- * It will use your extensions to render the content based on each Node's/Mark's `renderHTML` method.
8
- * This can be useful if you want to render content to HTML without having an actual editor instance.
9
- *
10
- * You have complete control over the rendering process. And can replace how each Node/Mark is rendered.
11
- */
12
-
13
- // eslint-disable-next-line no-console
14
- console.log(
15
- renderToHTMLString({
16
- extensions: [StarterKit],
17
- options: {
18
- nodeMapping: {
19
- heading({ node, children }) {
20
- const level = node.attrs.level
21
-
22
- return `<h${level}${serializeAttrsToHTMLString(node.attrs)}>THIS IS AN EXAMPLE OF CUSTOM HTML STRING RENDERING${serializeChildrenToHTMLString(children)}</h${level}>`
23
- },
24
- },
25
- markMapping: {},
26
- },
27
- content:
28
- {
29
- type: 'doc',
30
- from: 0,
31
- to: 574,
32
- content: [
33
- {
34
- type: 'heading',
35
- from: 0,
36
- to: 11,
37
- attrs: {
38
- level: 2,
39
- },
40
- content: [
41
- {
42
- type: 'text',
43
- from: 1,
44
- to: 10,
45
- text: 'Hi there,',
46
- },
47
- ],
48
- },
49
- {
50
- type: 'paragraph',
51
- from: 11,
52
- to: 169,
53
- content: [
54
- {
55
- type: 'text',
56
- from: 12,
57
- to: 22,
58
- text: 'this is a ',
59
- },
60
- {
61
- type: 'text',
62
- from: 22,
63
- to: 27,
64
- marks: [
65
- {
66
- type: 'italic',
67
- },
68
- ],
69
- text: 'basic',
70
- },
71
- {
72
- type: 'text',
73
- from: 27,
74
- to: 39,
75
- text: ' example of ',
76
- },
77
- {
78
- type: 'text',
79
- from: 39,
80
- to: 45,
81
- marks: [
82
- {
83
- type: 'bold',
84
- },
85
- ],
86
- text: 'Tiptap',
87
- },
88
- {
89
- type: 'text',
90
- from: 45,
91
- to: 168,
92
- 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:',
93
- },
94
- ],
95
- },
96
- {
97
- type: 'bulletList',
98
- from: 169,
99
- to: 230,
100
- content: [
101
- {
102
- type: 'listItem',
103
- from: 170,
104
- to: 205,
105
- attrs: {
106
- color: '',
107
- },
108
- content: [
109
- {
110
- type: 'paragraph',
111
- from: 171,
112
- to: 204,
113
- content: [
114
- {
115
- type: 'text',
116
- from: 172,
117
- to: 203,
118
- text: 'That’s a bullet list with one …',
119
- },
120
- ],
121
- },
122
- ],
123
- },
124
- {
125
- type: 'listItem',
126
- from: 205,
127
- to: 229,
128
- attrs: {
129
- color: '',
130
- },
131
- content: [
132
- {
133
- type: 'paragraph',
134
- from: 206,
135
- to: 228,
136
- content: [
137
- {
138
- type: 'text',
139
- from: 207,
140
- to: 227,
141
- text: '… or two list items.',
142
- },
143
- ],
144
- },
145
- ],
146
- },
147
- ],
148
- },
149
- {
150
- type: 'paragraph',
151
- from: 230,
152
- to: 326,
153
- content: [
154
- {
155
- type: 'text',
156
- from: 231,
157
- to: 325,
158
- text: 'Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:',
159
- },
160
- ],
161
- },
162
- {
163
- type: 'codeBlock',
164
- from: 326,
165
- to: 353,
166
- attrs: {
167
- language: 'css',
168
- },
169
- content: [
170
- {
171
- type: 'text',
172
- from: 327,
173
- to: 352,
174
- text: 'body {\n display: none;\n}',
175
- },
176
- ],
177
- },
178
- {
179
- type: 'paragraph',
180
- from: 353,
181
- to: 522,
182
- content: [
183
- {
184
- type: 'text',
185
- from: 354,
186
- to: 521,
187
- 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.',
188
- },
189
- ],
190
- },
191
- {
192
- type: 'blockquote',
193
- from: 522,
194
- to: 572,
195
- content: [
196
- {
197
- type: 'paragraph',
198
- from: 523,
199
- to: 571,
200
- content: [
201
- {
202
- type: 'text',
203
- from: 524,
204
- to: 564,
205
- text: 'Wow, that’s amazing. Good work, boy! 👏 ',
206
- },
207
- {
208
- type: 'hardBreak',
209
- from: 564,
210
- to: 565,
211
- },
212
- {
213
- type: 'text',
214
- from: 565,
215
- to: 570,
216
- text: '— Mom',
217
- },
218
- ],
219
- },
220
- ],
221
- },
222
- ],
223
- },
224
- }),
225
- )