import { Editor, Content, JSONContent, Extensions, getExtensionField, AnyExtension, } from '@tiptap/react' import { keyBy } from 'lodash' import { createElement, Fragment, ReactNode } from 'react' export function insertContent(editor: Editor | null, ...contents: Content[]) { if (!editor) { return } let e = editor.chain() for (const content of contents) { e = e.createParagraphNear().insertContent(content) } // If you're getting an "undefined" error here, make sure the editor has a unique key e.run() } // stricter version of DOMOutputSpec from prosemirror-model TODO: attrs is optional type ProsemirrorDOM = | 0 | string | [ tag: string, attrs: Record, ...content: ProsemirrorDOM[] ] const pmdToJSX = (dom: ProsemirrorDOM, children: ReactNode): ReactNode => { if (Array.isArray(dom)) { const [tag, attrs, ...content] = dom const { class: className, ...rest } = attrs return createElement( tag, { className, ...rest }, ...content.map((c) => pmdToJSX(c, children)) ) } else if (dom === 0) { if (Array.isArray(children)) { // wrap in fragment to stop missing key warnings return createElement(Fragment, {}, ...children) } return children } else { return dom } } export function getField(extension: AnyExtension, field: string) { const { name, options, storage } = extension return getExtensionField(extension, field, { name, options, storage }) } /** * Generate jsx from the json content without an Editor. * If you want to render an actual text editor you should probably use Editor instead. This is for ssr. * * We can't use prosemirror node views (this is what tiptap `generateHTML` does) because it uses document which is on the client. */ export const generateReact = (doc: JSONContent, extensions: Extensions) => { if (!doc) { return null } const extensionsIncludingStarterKit = extensions.flatMap( (e) => getField(e, 'addExtensions')?.() ?? e ) const exts = keyBy(extensionsIncludingStarterKit, 'name') const recurse = (content: JSONContent): ReactNode => { if (!content.type) { return content.text } const extension = exts[content.type] if (!extension) { return '' // throw new Error(`No extension for type "${content.type}" exists.`) } let children: ReactNode = content.content?.map(recurse) ?? content.text // TODO: collapse adjacent marks of the same type if (content.marks) { const reversedMarks = [...content.marks].reverse() reversedMarks.forEach((m) => { const e = exts[m.type] const renderReact = getField(e, 'renderReact') if (renderReact) { children = renderReact(m.attrs, children) } else { const renderHTML = getField(e, 'renderHTML') children = pmdToJSX(renderHTML({ HTMLAttributes: m.attrs }), children) } }) } const renderReact = getField(extension, 'renderReact') if (renderReact) { return renderReact(content.attrs) } const renderHTML = getField(extension, 'renderHTML') return pmdToJSX( renderHTML?.({ node: { attrs: content.attrs }, HTMLAttributes: content.attrs, }) ?? 0, children ) } return recurse(doc) }