# 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');
    }