import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; import type { Transaction } from "@tiptap/pm/state"; import type { Editor } from "@tiptap/react"; import { type MutableRefObject } from "react"; import type { Awareness } from "y-protocols/awareness"; import type { Doc as YDoc } from "yjs"; import { RICH_MARKDOWN_PROGRAMMATIC_TRANSACTION } from "./surgical-apply.js"; export { RICH_MARKDOWN_PROGRAMMATIC_TRANSACTION }; /** Reads the current markdown out of the tiptap-markdown storage. */ export declare function getEditorMarkdown(editor: Editor): string; export interface UseCollabReconcileOptions { /** The live editor, or null until it mounts. */ editor: Editor | null; /** Shared Y.Doc when collaborating; null disables all collab paths. */ ydoc?: YDoc | null; /** True after the collab provider has loaded the persisted initial Y.Doc state. */ collabSynced?: boolean; /** Shared awareness; null keeps the sole-client lead path. */ awareness?: Awareness | null; /** Authoritative markdown value (SQL source of truth). */ value: string; /** Timestamp of the authoritative value; gates newer-than reconcile. */ contentUpdatedAt?: string | null; /** Whether the editor accepts edits. Reconcile/seed only run for the live editor. */ editable: boolean; /** * Reports whether focus is inside any editing surface owned by this editor. * Defaults to TipTap's contenteditable focus. Apps with editable NodeView * controls outside the contenteditable surface can include those controls so * a partial save echo cannot interrupt an active edit. */ isEditorFocused?: (editor: Editor) => boolean; /** * Reads the current markdown from the editor. Injected so a dialect could * swap serializers; defaults to the tiptap-markdown storage reader. For an app * with a custom serializer (e.g. Content's `docToNfm(editor.getJSON())`), pass * it here so the seed/reconcile equality checks compare like-for-like. */ getMarkdown?: (editor: Editor) => string; /** * Applies the authoritative `value` into the editor. Defaults to passing the * raw markdown string to `editor.commands.setContent`. Apps whose serializer * is NOT tiptap-markdown (Content parses `nfmToDoc(value)` into a PM doc) * override this so seed + reconcile write the correct content shape. The * supplied `options` carry the history/whitespace flags the default path uses; * a custom implementation should forward them when relevant. */ setContent?: (editor: Editor, value: string, options: { emitUpdate?: boolean; addToHistory?: boolean; }) => void; /** * Parses the authoritative `value` into a full ProseMirror document for the * SURGICAL reconcile path: instead of a whole-document `setContent` (which * under Collaboration rewrites the entire Y.XmlFragment and tears down every * block NodeView), the reconcile diffs the parsed doc against the live doc * and replaces only the changed top-level run. Return null to skip the * surgical path for a given value (falls back to `setContent`). * * Defaults to a best-effort tiptap-markdown parse; apps with their own * serializers (Content's NFM, Plan's blocks[] doc) should supply the exact * doc their `setContent` would write. Pass `false` to disable surgical * application entirely. */ parseValue?: ((editor: Editor, value: string) => ProseMirrorNode | null) | false; /** * Normalizes the authoritative `value` to the canonical markdown the editor * would emit, so the "already in sync / our own echo" equality checks match a * serializer that re-canonicalizes (Content's `canonicalizeNfm`). Defaults to * identity (GFM already round-trips byte-stably). */ normalizeValue?: (value: string) => string; /** * Decides whether the empty-doc seed should run for the current shared * fragment. Defaults to "fragment has no nodes, or the editor holds no * semantic markdown". Apps with sentinel-empty content (Content's * `` filler) override this. Receives the live fragment length * and the editor's current markdown. */ shouldSeed?: (info: { value: string; currentMarkdown: string; fragmentLength: number; }) => boolean; /** * The initial "applied" watermark. Default mirrors `contentUpdatedAt`, so a * fresh mount whose Y.Doc already matches SQL doesn't re-apply. Pass `null` * to force the first reconcile pass to adopt authoritative SQL even at the * same timestamp — Content does this so a stale persisted Y.Doc (an agent that * edited the CLOSED doc) is corrected on open. The editor is keyed per * document upstream, so this only affects the first mount of each doc. */ initialAppliedUpdatedAt?: string | null; } export interface UseCollabReconcileResult { /** True when a Y.Doc is bound (collaborative editing active). */ collab: boolean; /** * Set true around any programmatic `setContent` so the editor's `onUpdate` * can ignore the resulting transaction (it isn't a user edit). */ isSettingContentRef: MutableRefObject; /** * Call from `onUpdate` BEFORE serializing. Returns true when the update must * be ignored: editor not editable, mid-programmatic-setContent, or (in collab * mode) a remote-origin transaction. Also records the local typing time. */ shouldIgnoreUpdate: (transaction: Transaction) => boolean; /** * Call from `onUpdate` AFTER computing the markdown to emit. Returns false * when the value must NOT be persisted yet (an empty collab doc before the * seed has run); records it as the last-emitted value otherwise. */ registerEmitted: (markdown: string) => boolean; } export declare function useCollabReconcile({ editor, ydoc, collabSynced, awareness, value, contentUpdatedAt, editable, isEditorFocused, getMarkdown, setContent, parseValue, normalizeValue, shouldSeed, initialAppliedUpdatedAt, }: UseCollabReconcileOptions): UseCollabReconcileResult; //# sourceMappingURL=useCollabReconcile.d.ts.map