/** * Content Serializer * * Converts Tiptap JSON page tree into markdown for agent prompts. * Uses @tiptap/markdown's MarkdownManager for standard node serialization, * with custom handling only for pageLink nodes. */ import { Node, type JSONContent } from '@tiptap/core'; import StarterKit from '@tiptap/starter-kit'; import Link from '@tiptap/extension-link'; import { MarkdownManager } from '@tiptap/markdown'; /** * Minimal page shape needed for serialization. */ export interface PageForSerialization { id: string; parentPageId: string | null; title: string; content: string | null; // JSON string from Tiptap sortOrder: number; } /** * Server-side pageLink node — only needs schema definition + markdown rendering. */ const ServerPageLink = Node.create({ name: 'pageLink', group: 'block', atom: true, addAttributes() { return { pageId: { default: null }, }; }, renderMarkdown(node: JSONContent) { const pageId = (node.attrs?.pageId as string) ?? ''; return `[[page:${pageId}]]\n\n`; }, }); let _manager: MarkdownManager | null = null; function getManager(): MarkdownManager { if (!_manager) { _manager = new MarkdownManager({ extensions: [StarterKit, Link, ServerPageLink], }); } return _manager; } /** * Convert a Tiptap JSON document to markdown. */ export function tiptapJsonToMarkdown(json: unknown): string { if (!json || typeof json !== 'object') return ''; const doc = json as JSONContent; if (doc.type !== 'doc' || !Array.isArray(doc.content)) return ''; return getManager().serialize(doc).trim(); } /** * Serialize an array of pages into a single markdown document. * Pages are organized as a tree (root first, then children by sortOrder). * * Each page is marked with so the agent can reference them. */ export function serializePageTree(pages: PageForSerialization[]): string { if (pages.length === 0) return ''; // Build parent→children map const childrenMap = new Map(); for (const page of pages) { const parentKey = page.parentPageId; if (!childrenMap.has(parentKey)) { childrenMap.set(parentKey, []); } childrenMap.get(parentKey)!.push(page); } // Sort children by sortOrder for (const children of childrenMap.values()) { children.sort((a, b) => a.sortOrder - b.sortOrder); } // Render tree depth-first const sections: string[] = []; function renderPage(page: PageForSerialization, depth: number): void { const headerPrefix = '#'.repeat(Math.min(depth + 1, 6)); let section = `\n${headerPrefix} ${page.title}`; if (page.content) { try { const parsed = JSON.parse(page.content); const md = tiptapJsonToMarkdown(parsed); if (md.trim()) { section += `\n\n${md}`; } } catch { // Invalid JSON — skip content } } sections.push(section); const children = childrenMap.get(page.id) ?? []; for (const child of children) { renderPage(child, depth + 1); } } // Start from root pages (parentPageId is null) const roots = childrenMap.get(null) ?? []; for (const root of roots) { renderPage(root, 1); } return sections.join('\n\n'); }