@tiptap/core
Version:
headless rich text editor
120 lines (100 loc) • 4.04 kB
text/typescript
import type { ParseOptions } from '@tiptap/pm/model'
import { DOMParser, Fragment, Node as ProseMirrorNode, Schema } from '@tiptap/pm/model'
import type { Content } from '../types.js'
import { elementFromString } from '../utilities/elementFromString.js'
export type CreateNodeFromContentOptions = {
slice?: boolean
parseOptions?: ParseOptions
errorOnInvalidContent?: boolean
}
/**
* Takes a JSON or HTML content and creates a Prosemirror node or fragment from it.
* @param content The JSON or HTML content to create the node from
* @param schema The Prosemirror schema to use for the node
* @param options Options for the parser
* @returns The created Prosemirror node or fragment
*/
export function createNodeFromContent(
content: Content | ProseMirrorNode | Fragment,
schema: Schema,
options?: CreateNodeFromContentOptions,
): ProseMirrorNode | Fragment {
if (content instanceof ProseMirrorNode || content instanceof Fragment) {
return content
}
options = {
slice: true,
parseOptions: {},
...options,
}
const isJSONContent = typeof content === 'object' && content !== null
const isTextContent = typeof content === 'string'
if (isJSONContent) {
try {
const isArrayContent = Array.isArray(content) && content.length > 0
// if the JSON Content is an array of nodes, create a fragment for each node
if (isArrayContent) {
return Fragment.fromArray(content.map(item => schema.nodeFromJSON(item)))
}
const node = schema.nodeFromJSON(content)
if (options.errorOnInvalidContent) {
node.check()
}
return node
} catch (error) {
if (options.errorOnInvalidContent) {
throw new Error('[tiptap error]: Invalid JSON content', { cause: error as Error })
}
console.warn('[tiptap warn]: Invalid content.', 'Passed value:', content, 'Error:', error)
return createNodeFromContent('', schema, options)
}
}
if (isTextContent) {
// Check for invalid content
if (options.errorOnInvalidContent) {
let hasInvalidContent = false
let invalidContent = ''
// A copy of the current schema with a catch-all node at the end
const contentCheckSchema = new Schema({
topNode: schema.spec.topNode,
marks: schema.spec.marks,
// Prosemirror's schemas are executed such that: the last to execute, matches last
// This means that we can add a catch-all node at the end of the schema to catch any content that we don't know how to handle
nodes: schema.spec.nodes.append({
__tiptap__private__unknown__catch__all__node: {
content: 'inline*',
group: 'block',
parseDOM: [
{
tag: '*',
getAttrs: e => {
// If this is ever called, we know that the content has something that we don't know how to handle in the schema
hasInvalidContent = true
// Try to stringify the element for a more helpful error message
invalidContent = typeof e === 'string' ? e : e.outerHTML
return null
},
},
],
},
}),
})
if (options.slice) {
DOMParser.fromSchema(contentCheckSchema).parseSlice(elementFromString(content), options.parseOptions)
} else {
DOMParser.fromSchema(contentCheckSchema).parse(elementFromString(content), options.parseOptions)
}
if (options.errorOnInvalidContent && hasInvalidContent) {
throw new Error('[tiptap error]: Invalid HTML content', {
cause: new Error(`Invalid element found: ${invalidContent}`),
})
}
}
const parser = DOMParser.fromSchema(schema)
if (options.slice) {
return parser.parseSlice(elementFromString(content), options.parseOptions).content
}
return parser.parse(elementFromString(content), options.parseOptions)
}
return createNodeFromContent('', schema, options)
}