// --------------------------------------------------------------------------- // 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 = {}; for (const f of fields) { attrs[f.name] = { default: f.default ?? null }; } return attrs; } function buildDefaultAttrs(fields: ComponentDef["fields"]) { const attrs: Record = {}; 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;