import type { ParsedEdge } from "./types"; // Match edge header: allow spaces between '>' characters (Obsidian uses "> >" for nesting) const EDGE_HEADER_RE = /^\s*((?:>\s*)+)\s*\[!edge\]\s*(.+?)\s*$/i; // Match edge header WITHOUT leading '>' (plain line) – so edges outside callout blocks are also found const EDGE_HEADER_PLAIN_RE = /^\s*\[!edge\]\s+(.+?)\s*$/i; const TARGET_LINK_RE = /\[\[([^\]]+?)\]\]/g; /** Sentinel: edge block has no quote level (plain lines); only ends on next edge line or end. */ const PLAIN_EDGE_LEVEL = -1; /** * Extract edges from any callout nesting and from plain lines: * - Edge with callout: > [!edge] (any number of '>' allowed) * - Edge without callout: [!edge] (so edges outside abstract block are found) * - Collect targets from subsequent lines while quoteLevel >= edgeLevel (or plain mode) * - Stop when: * a) next [!edge] header appears, OR * b) quoteLevel drops below edgeLevel (block ends), ignoring blank lines, OR * c) in plain mode: next [!edge] line */ 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*(?:(?:>\s*)+)/); if (!m || !m[0]) return 0; return (m[0].match(/>/g) || []).length; }; 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 (with callout quote) const edgeMatch = line.match(EDGE_HEADER_RE); if (edgeMatch && edgeMatch[1] && edgeMatch[2]) { flush(i - 1); currentEdgeLevel = (edgeMatch[1].match(/>/g) || []).length; current = { rawType: edgeMatch[2].trim(), targets: [], lineStart: i, lineEnd: i, }; continue; } // Start of edge block without '>' (plain line) – e.g. edge written outside abstract block const plainMatch = line.match(EDGE_HEADER_PLAIN_RE); if (plainMatch && plainMatch[1]) { flush(i - 1); currentEdgeLevel = PLAIN_EDGE_LEVEL; current = { rawType: plainMatch[1].trim(), targets: [], lineStart: i, lineEnd: i, }; continue; } if (!current) continue; const trimmed = line.trim(); const ql = getQuoteLevel(line); // In plain mode, only end on next edge line (already handled above); collect [[links]] from any line if (currentEdgeLevel === PLAIN_EDGE_LEVEL) { 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); } } continue; } // End of quoted edge block if quote level drops below the edge header level 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; }