UNPKG

@tiptap/core

Version:

headless rich text editor

120 lines (100 loc) 4.04 kB
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) }