mindnet_obsidian/src/parser/parseEdgesFromCallouts.ts

79 lines
2.1 KiB
TypeScript

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] <type> (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;
}