-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Expand file tree
/
Copy pathhtml-string.ts
More file actions
145 lines (137 loc) · 5.23 KB
/
Copy pathhtml-string.ts
File metadata and controls
145 lines (137 loc) · 5.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/* oslint-disableno-explicit-any */
import type { DOMOutputSpecArray, Extensions, JSONContent } from '@tiptap/core'
import type { DOMOutputSpec, Mark, Node } from '@tiptap/pm/model'
import {
escapeHTML,
renderJSONContentToString,
serializeAttrsToHTMLString,
serializeChildrenToHTMLString,
} from '../../json/html-string/string.js'
import type { TiptapStaticRendererOptions } from '../../json/renderer.js'
import type { StaticEditorOptions } from '../extensionRenderer.js'
import { applyStaticEditorOptionsToExtensions, renderToElement } from '../extensionRenderer.js'
export {
serializeAttrsToHTMLString,
serializeChildrenToHTMLString,
} from '../../json/html-string/string.js'
/**
* HTML elements that cannot be self-closing and must always have a closing tag.
* These elements must be rendered as <tag></tag> even when empty, not <tag />.
*/
const NON_SELF_CLOSING_TAGS = new Set([
'iframe',
'script',
'style',
'title',
'textarea',
'div',
'span',
'a',
'button',
])
/**
* Take a DOMOutputSpec and return a function that can render it to a string
* @param content The DOMOutputSpec to convert to a string
* @returns A function that can render the DOMOutputSpec to a string
*/
export function domOutputSpecToHTMLString(
content: DOMOutputSpec,
): (children?: string | string[]) => string {
if (typeof content === 'string') {
return () => escapeHTML(content)
}
if (typeof content === 'object' && 'length' in content) {
const [_tag, attrs, children, ...rest] = content as DOMOutputSpecArray
// ProseMirror encodes a namespaced spec as `"<namespace> <localName>"`.
// Only the opening tag carries the `xmlns` attribute: the closing tag and the
// `NON_SELF_CLOSING_TAGS` lookup below both need the bare local name.
const parts = _tag.split(' ')
const tag = parts.length > 1 ? parts[1] : _tag
const openTag = parts.length > 1 ? `${parts[1]} xmlns="${parts[0]}"` : _tag
if (attrs === undefined) {
return () => `<${openTag}/>`
}
if (attrs === 0) {
return child => `<${openTag}>${serializeChildrenToHTMLString(child)}</${tag}>`
}
if (typeof attrs === 'object') {
if (Array.isArray(attrs)) {
if (children === undefined) {
return child =>
`<${openTag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}</${tag}>`
}
if (children === 0) {
return child =>
`<${openTag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}</${tag}>`
}
return child =>
`<${openTag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}${[children]
.concat(rest)
.map(a => domOutputSpecToHTMLString(a)(child))}</${tag}>`
}
if (children === undefined) {
if (NON_SELF_CLOSING_TAGS.has(tag)) {
return () => `<${openTag}${serializeAttrsToHTMLString(attrs)}></${tag}>`
}
return () => `<${openTag}${serializeAttrsToHTMLString(attrs)}/>`
}
if (children === 0) {
return child =>
`<${openTag}${serializeAttrsToHTMLString(attrs)}>${serializeChildrenToHTMLString(child)}</${tag}>`
}
return child =>
`<${openTag}${serializeAttrsToHTMLString(attrs)}>${[children]
.concat(rest)
.map(a => domOutputSpecToHTMLString(a)(child))
.join('')}</${tag}>`
}
}
// TODO support DOM elements? How to handle them?
throw new Error(
'[tiptap error]: Unsupported DomOutputSpec type, check the `renderHTML` method output or implement a node mapping',
{
cause: content,
},
)
}
/**
* This function will statically render a Prosemirror Node to HTML using the provided extensions and options.
*
* Limitations: this function builds the schema and runs each extension's
* `renderHTML`, but does not instantiate an `Editor`. Extensions that mutate
* the document inside `addProseMirrorPlugins`, `onCreate`, or transaction
* hooks will not run. For UniqueID, pre-process the JSON with
* `generateUniqueIds` from `@tiptap/extension-unique-id`; for TableOfContents,
* pre-process with `generateTocIds` from `@tiptap/extension-table-of-contents`.
*
* @param content The content to render to HTML
* @param extensions The extensions to use for rendering
* @param staticEditorOptions Optional editor-level options that affect rendered output, currently `{ textDirection }`. Mirrors a subset of `EditorOptions`.
* @param options The options to use for rendering
* @returns The rendered HTML string
*/
export function renderToHTMLString({
content,
extensions,
staticEditorOptions,
options,
}: {
content: Node | JSONContent
extensions: Extensions
staticEditorOptions?: StaticEditorOptions
options?: Partial<TiptapStaticRendererOptions<string, Mark, Node>>
}): string {
return renderToElement<string>({
renderer: renderJSONContentToString,
domOutputSpecToElement: domOutputSpecToHTMLString,
mapDefinedTypes: {
// Map a doc node to concatenated children
doc: ({ children }) => serializeChildrenToHTMLString(children),
// Map a text node to its text content
text: ({ node }) => escapeHTML(node.text ?? ''),
},
content,
extensions: applyStaticEditorOptionsToExtensions(extensions, staticEditorOptions),
options,
})
}