import { slugify } from "./slug.ts"; interface Heading { text: string; level: number; anchor: string; } /** * Allocate an anchor for `text`, disambiguating repeats by suffixing -1, -2, ... * `seen` tracks how many times each base slug has been used so far in the doc. */ export function makeAnchor(text: string, seen: Map): string { const base = slugify(text); if (!base) return base; const n = seen.get(base) ?? 0; seen.set(base, n + 1); return n === 0 ? base : `${base}-${n}`; } const HEADING_RE = /^(#{1,6})\s+(.+?)\s*#*\s*$/; const FENCE_RE = /^(`{3,}|~{3,})/; /** Extract ATX headings from markdown, skipping fenced code blocks. */ export function extractHeadings(md: string): Heading[] { const out: Heading[] = []; const seen = new Map(); let fence: string | null = null; for (const line of md.split("\n")) { const fenceMatch = line.match(FENCE_RE); if (fenceMatch) { const marker = fenceMatch[1]?.[0]; if (fence === null) { fence = marker ?? null; } else if (marker === fence) { fence = null; } continue; } if (fence !== null) continue; const m = line.match(HEADING_RE); if (!m) continue; const level = m[1]?.length ?? 0; const text = m[2]?.trim() ?? ""; if (!text) continue; const anchor = makeAnchor(text, seen); if (!anchor) continue; out.push({ text, level, anchor }); } return out; }