export interface MarkdownNode { type: 'heading' | 'paragraph' | 'code' | 'codeblock' | 'list' | 'listitem' | 'blockquote' | 'hr' | 'link' | 'bold' | 'italic' | 'text'; content?: string; level?: number; // for headings (1-6) language?: string; // for code blocks url?: string; // for links children?: MarkdownNode[]; } export function parseMarkdown(markdown: string): MarkdownNode[] { const lines = markdown.split('\n'); const nodes: MarkdownNode[] = []; let i = 0; while (i < lines.length) { const line = lines[i]; // Skip empty lines if (!line?.trim()) { i++; continue; } // Horizontal rule if (/^---+\s*$/.test(line)) { nodes.push({ type: 'hr' }); i++; continue; } // Code block if (line.startsWith('```')) { const language = line.slice(3).trim(); const content: string[] = []; i++; while (i < lines.length && !lines[i]?.startsWith('```')) { content.push(lines[i] || ''); i++; } nodes.push({ type: 'codeblock', content: content.join('\n'), language: language || undefined }); i++; // Skip closing ``` continue; } // Blockquote if (line.startsWith('>')) { const content = line.replace(/^>\s?/, ''); nodes.push({ type: 'blockquote', content: content }); i++; continue; } // Heading const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { nodes.push({ type: 'heading', level: headingMatch[1]?.length || 1, content: headingMatch[2] || '' }); i++; continue; } // List item const listMatch = line.match(/^([\s]*)[-*+]\s+(.+)$/); if (listMatch) { const listNodes: MarkdownNode[] = []; while (i < lines.length) { const currentLine = lines[i]; const currentMatch = currentLine?.match(/^([\s]*)[-*+]\s+(.+)$/); if (!currentMatch) break; listNodes.push({ type: 'listitem', content: currentMatch[2] || '' }); i++; } nodes.push({ type: 'list', children: listNodes }); continue; } // Paragraph nodes.push(parseParagraph(line)); i++; } return nodes; } function parseParagraph(text: string): MarkdownNode { const children: MarkdownNode[] = []; let remaining = text; while (remaining) { // Check for code const codeMatch = remaining.match(/^`([^`]+)`/); if (codeMatch) { children.push({ type: 'code', content: codeMatch[1] }); remaining = remaining.slice(codeMatch[0].length); continue; } // Check for bold const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/); if (boldMatch) { children.push({ type: 'bold', content: boldMatch[1] }); remaining = remaining.slice(boldMatch[0].length); continue; } // Check for italic const italicMatch = remaining.match(/^\*([^*]+)\*/); if (italicMatch) { children.push({ type: 'italic', content: italicMatch[1] }); remaining = remaining.slice(italicMatch[0].length); continue; } // Check for link const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/); if (linkMatch) { children.push({ type: 'link', content: linkMatch[1], url: linkMatch[2] }); remaining = remaining.slice(linkMatch[0].length); continue; } // Regular text - find next special character const nextSpecial = remaining.search(/[`\*\[]/); if (nextSpecial === -1) { children.push({ type: 'text', content: remaining }); break; } else if (nextSpecial > 0) { children.push({ type: 'text', content: remaining.slice(0, nextSpecial) }); remaining = remaining.slice(nextSpecial); } else { // Special character at start but no match - treat as text children.push({ type: 'text', content: remaining[0] }); remaining = remaining.slice(1); } } return { type: 'paragraph', children }; } export function truncateMarkdown(nodes: MarkdownNode[], maxLines: number): MarkdownNode[] { const lines: MarkdownNode[] = []; function countLines(node: MarkdownNode): number { if (node.type === 'text' || node.type === 'code' || node.type === 'bold' || node.type === 'italic' || node.type === 'link') { return Math.ceil((node.content?.length || 0) / 80) || 1; } if (node.type === 'paragraph') { return 1; } if (node.type === 'heading') { return 1; } if (node.type === 'codeblock') { return (node.content?.split('\n').length || 0) + 2; // +2 for borders } if (node.type === 'list' && node.children) { return node.children.length; } if (node.type === 'listitem' || node.type === 'blockquote') { return 1; } if (node.type === 'hr') { return 1; } return 1; } function addNode(node: MarkdownNode): boolean { const linesNeeded = countLines(node); if (lines.length + linesNeeded > maxLines) { return false; } lines.push(node); return true; } for (const node of nodes) { if (!addNode(node)) { break; } } return lines; }