All checks were successful
CD / Build and push images (push) Successful in 2m0s
CI / Lint, typecheck, test (push) Successful in 1m42s
CI / Auth e2e pack (push) Successful in 1m50s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 10s
TipTap is bound to the canonical ProseMirror schema (packages/shared, #24) via a generic bridge (spec-utils.ts) that re-derives every node/mark's attrs/parseDOM/toDOM from editorSchema instead of duplicating them, so the editor's schema stays byte-for-byte identical to what the api decodes Yjs states against — guarded by a schema- fidelity + real Yjs round-trip test (@tiptap/y-tiptap client encoding against y-prosemirror server decoding). Route /p/:pondSlug/:pageSlug (RequireAuth) resolves the page via a new GET /ponds/:pondId/pages/:slug endpoint, binds a local Y.Doc via @tiptap/extension-collaboration (fragment "default"), and offers a view/edit mode toggle (sidebar auto-hides in edit mode via a small AppLayout context). Page state saves debounced to PUT /pages/:id/state with a truthful saving/saved/error(retrying) indicator; title saves separately via PATCH /pages/:id. Toolbar covers headings, marks, lists, blockquote, code block, hr, table (insert/row/column/header ops via prosemirror-tables), a minimal link mark, and an image placeholder (real upload is #27/#28). Closes #25 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
62 lines
2.6 KiB
TypeScript
62 lines
2.6 KiB
TypeScript
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<NodeConfig, 'parseHTML' | 'renderHTML'> {
|
|
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 } : {},
|
|
};
|
|
}
|