import type { ParsedEdge } from "./types"; const EDGE_HEADER_RE = /^\s*(>+)\s*\[!edge\]\s*(.+?)\s*$/i; const TARGET_LINK_RE = /\[\[([^\]]+?)\]\]/g; /** * Extract edges from any callout nesting: * - Edge starts with: > [!edge] (any number of '>' allowed) * - Collect targets from subsequent lines while quoteLevel >= edgeLevel * - Stop when: * a) next [!edge] header appears, OR * b) quoteLevel drops below edgeLevel (block ends), ignoring blank lines */ export function parseEdgesFromCallouts(markdown: string): ParsedEdge[] { const lines = markdown.split(/\r?\n/); const edges: ParsedEdge[] = []; let current: ParsedEdge | null = null; let currentEdgeLevel = 0; const getQuoteLevel = (line: string): number => { const m = line.match(/^\s*(>+)/); return m && m[1] ? m[1].length : 0; }; const flush = (endLine: number) => { if (!current) return; current.lineEnd = endLine; if (current.targets.length > 0) edges.push(current); current = null; currentEdgeLevel = 0; }; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line === undefined) continue; // Start of a new edge block const edgeMatch = line.match(EDGE_HEADER_RE); if (edgeMatch && edgeMatch[1] && edgeMatch[2]) { flush(i - 1); currentEdgeLevel = edgeMatch[1].length; current = { rawType: edgeMatch[2].trim(), targets: [], lineStart: i, lineEnd: i, }; continue; } if (!current) continue; const trimmed = line.trim(); const ql = getQuoteLevel(line); // End of the current edge block if quote level drops below the edge header level // (ignore blank lines) if (trimmed !== "" && ql < currentEdgeLevel) { flush(i - 1); continue; } // Collect targets (multiple per line allowed) TARGET_LINK_RE.lastIndex = 0; let m: RegExpExecArray | null; while ((m = TARGET_LINK_RE.exec(line)) !== null) { if (m[1]) { const t = m[1].trim(); if (t) current.targets.push(t); } } } flush(lines.length - 1); return edges; }