import { editorSchema } from '@dorfteich/shared'; import type { Attributes, NodeConfig } from '@tiptap/core'; import type { MarkSpec, NodeSpec } from 'prosemirror-model'; /** * Bridges the canonical ProseMirror schema (`packages/shared/editor-schema`, * issue #24) into TipTap node/mark extensions (issue #25). TipTap always * builds its own `Schema` instance from the extensions handed to `useEditor` * — it cannot take a premade `Schema` — so this module re-derives every * node/mark spec from `editorSchema` instead of duplicating attrs/parseDOM/ * toDOM by hand. That keeps the editor's schema byte-for-byte identical to * what the api decodes Yjs states against (`apps/api/src/pages/yjs-content.ts`); * see the schema-fidelity test in `schema-extensions.test.ts`. */ export function nodeSpec(name: string): NodeSpec { const spec = editorSchema.spec.nodes.get(name); if (!spec) throw new Error(`editorSchema has no node "${name}"`); return spec; } export function markSpec(name: string): MarkSpec { const spec = editorSchema.spec.marks.get(name); if (!spec) throw new Error(`editorSchema has no mark "${name}"`); return spec; } /** Converts a ProseMirror `attrs` map into TipTap's `addAttributes()` shape, * preserving "no default" (required attribute) instead of defaulting to * `undefined`, so e.g. `image.fileId` stays a required attribute. */ export function attributesFromSpec(spec: NodeSpec | MarkSpec): Attributes { const attrs = spec.attrs ?? {}; const result: Attributes = {}; for (const name of Object.keys(attrs)) { const attr = attrs[name]!; result[name] = 'default' in attr ? { default: attr.default, validate: attr.validate } : { isRequired: true, validate: attr.validate }; } return result; } /** `parseHTML()`/`renderHTML()` for nodes that need no extra TipTap-side * behavior (no custom commands, NodeView, or attrs beyond the spec). */ export function passthroughNodeIO(spec: NodeSpec): Pick { return { parseHTML: spec.parseDOM ? () => spec.parseDOM : undefined, renderHTML: spec.toDOM ? ({ node }) => spec.toDOM!(node) : undefined, }; } /** Marks a table node's built schema with the `tableRole` prosemirror-tables * needs (selection/keymap/commands key off this, not the node name), without * having to teach TipTap's schema builder a new field for every node. */ export function extendWithTableRole(name: string, tableRole: string) { return { extendNodeSchema: (extension: { name: string }) => extension.name === name ? { tableRole } : {}, }; }