File size: 3,296 Bytes
561e6f0
76fc93a
561e6f0
76fc93a
 
 
561e6f0
 
 
 
 
 
 
9b7d64e
7814104
76fc93a
0c69852
561e6f0
76fc93a
 
 
 
 
 
 
561e6f0
76fc93a
 
 
 
 
 
561e6f0
 
 
76fc93a
561e6f0
76fc93a
 
561e6f0
76fc93a
 
561e6f0
 
 
 
 
76fc93a
 
 
561e6f0
 
76fc93a
561e6f0
 
 
 
 
 
 
76fc93a
 
561e6f0
 
 
 
 
 
 
76fc93a
 
 
561e6f0
 
 
 
 
76fc93a
 
 
 
 
7814104
76fc93a
0c69852
76fc93a
9b7d64e
561e6f0
 
 
76fc93a
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// ---------------------------------------------------------------------------
// 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;