/* 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 even when empty, not . */ 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 let tag = _tag const parts = tag.split(' ') if (parts.length > 1) { tag = `${parts[1]} xmlns="${parts[0]}"` } if (attrs === undefined) { return () => `<${tag}/>` } if (attrs === 0) { return child => `<${tag}>${serializeChildrenToHTMLString(child)}` } if (typeof attrs === 'object') { if (Array.isArray(attrs)) { if (children === undefined) { return child => `<${tag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}` } if (children === 0) { return child => `<${tag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}` } return child => `<${tag}>${domOutputSpecToHTMLString(attrs as DOMOutputSpecArray)(child)}${[children] .concat(rest) .map(a => domOutputSpecToHTMLString(a)(child))}` } if (children === undefined) { if (NON_SELF_CLOSING_TAGS.has(tag)) { return () => `<${tag}${serializeAttrsToHTMLString(attrs)}>` } return () => `<${tag}${serializeAttrsToHTMLString(attrs)}/>` } if (children === 0) { return child => `<${tag}${serializeAttrsToHTMLString(attrs)}>${serializeChildrenToHTMLString(child)}` } return child => `<${tag}${serializeAttrsToHTMLString(attrs)}>${[children] .concat(rest) .map(a => domOutputSpecToHTMLString(a)(child)) .join('')}` } } // 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> }): string { return renderToElement({ 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, }) }