mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-01-02 10:58:10 -05:00
123 lines
3.3 KiB
TypeScript
123 lines
3.3 KiB
TypeScript
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<string, string | number | undefined>,
|
|
...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)
|
|
}
|