All files / src/agent content-serializer.ts

4.76% Statements 2/42
0% Branches 0/24
0% Functions 0/7
5.26% Lines 2/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127                                                      21x                                 21x                                                                                                                                                                    
/**
 * 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 <!-- page:$id --> 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<string | null, PageForSerialization[]>();
  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 = `<!-- page:${page.id} -->\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');
}