Spaces:
Runtime error
Runtime error
| // --------------------------------------------------------------------------- | |
| // Extension factory | |
| // | |
| // Generates TipTap Node extensions from ComponentDef declarations. | |
| // A single factory handles both "wrapper" (editable children) and | |
| // "atomic" (self-closing) components based on def.kind. | |
| // --------------------------------------------------------------------------- | |
| import { Node, mergeAttributes } from "@tiptap/core"; | |
| import { ReactNodeViewRenderer } from "@tiptap/react"; | |
| import type { ComponentDef } from "./registry"; | |
| import { makeWrapperView } from "./WrapperView"; | |
| import { makeAtomicView } from "./AtomicView"; | |
| import { MermaidNodeView } from "./MermaidView"; | |
| import { HfUserNodeView } from "./HfUserView"; | |
| import { makeHtmlEmbedView } from "../embeds/HtmlEmbedView"; | |
| import { makeIframeEmbedView } from "../embeds/IframeEmbedView"; | |
| function buildAttrSchema(fields: ComponentDef["fields"]) { | |
| const attrs: Record<string, { default: unknown }> = {}; | |
| for (const f of fields) { | |
| attrs[f.name] = { default: f.default ?? null }; | |
| } | |
| return attrs; | |
| } | |
| function buildDefaultAttrs(fields: ComponentDef["fields"]) { | |
| const attrs: Record<string, unknown> = {}; | |
| for (const f of fields) { | |
| attrs[f.name] = f.default ?? null; | |
| } | |
| return attrs; | |
| } | |
| /** | |
| * Build a TipTap Node extension for any component definition. | |
| * | |
| * - "wrapper" components have editable ProseMirror content (block+). | |
| * - "atomic" components are self-closing atoms with no inner editing. | |
| */ | |
| export function createComponentExtension(def: ComponentDef) { | |
| const isWrapper = def.kind === "wrapper"; | |
| const commandName = `insert${def.tag}`; | |
| return Node.create({ | |
| name: def.name, | |
| group: "block", | |
| ...(isWrapper | |
| ? { content: def.content || "block+", defining: true, isolating: true } | |
| : { atom: true, draggable: true, selectable: true }), | |
| addAttributes() { | |
| return buildAttrSchema(def.fields); | |
| }, | |
| parseHTML() { | |
| return [{ tag: `div[data-component="${def.name}"]` }]; | |
| }, | |
| renderHTML({ HTMLAttributes }) { | |
| const base = mergeAttributes(HTMLAttributes, { "data-component": def.name }); | |
| return isWrapper ? ["div", base, 0] : ["div", base]; | |
| }, | |
| addCommands() { | |
| return { | |
| [commandName]: | |
| () => | |
| ({ commands }: { commands: any }) => { | |
| const content: any = { type: def.name, attrs: buildDefaultAttrs(def.fields) }; | |
| if (isWrapper) content.content = [{ type: "paragraph" }]; | |
| return commands.insertContent(content); | |
| }, | |
| } as any; | |
| }, | |
| addNodeView() { | |
| if (isWrapper) { | |
| return ReactNodeViewRenderer(makeWrapperView(def)); | |
| } | |
| let View; | |
| if (def.name === "mermaid") View = MermaidNodeView; | |
| else if (def.name === "hfUser") View = HfUserNodeView; | |
| else if (def.name === "htmlEmbed") View = makeHtmlEmbedView(def); | |
| else if (def.name === "iframe") View = makeIframeEmbedView(def); | |
| else View = makeAtomicView(def); | |
| return ReactNodeViewRenderer(View); | |
| }, | |
| }); | |
| } | |
| /** @deprecated Use createComponentExtension instead */ | |
| export const createWrapperExtension = createComponentExtension; | |
| /** @deprecated Use createComponentExtension instead */ | |
| export const createAtomicExtension = createComponentExtension; | |