export interface BibEntry { type: string; key: string; fields: Record; } /** Minimal BibTeX parser (enough for key extraction and field checks). */ export function parseBib(text: string): BibEntry[] { const entries: BibEntry[] = []; const re = /@(\w+)\s*\{\s*([^,\s]+)\s*,/g; for (const m of text.matchAll(re)) { const type = (m[1] ?? "").toLowerCase(); if (type === "comment" || type === "preamble" || type === "string") continue; const start = (m.index ?? 0) + m[0].length; let depth = 1; let i = start; while (i < text.length && depth > 0) { if (text[i] === "{") depth++; else if (text[i] === "}") depth--; i++; } const body = text.slice(start, i - 1); const fields: Record = {}; const fre = /(\w+)\s*=\s*(\{((?:[^{}]|\{[^{}]*\})*)\}|"([^"]*)"|([^,\n]+))/g; for (const f of body.matchAll(fre)) fields[(f[1] ?? "").toLowerCase()] = (f[3] ?? f[4] ?? f[5] ?? "").trim(); entries.push({ type, key: m[2] ?? "", fields }); } return entries; } export function citeKeysInTex(tex: string): string[] { const keys = new Set(); const re = /\\(?:cite|citep|citet|citeauthor|citeyear|parencite|textcite|autocite|citealp|citealt|nocite)\*?(?:\[[^\]]*\]){0,2}\{([^}]+)\}/g; for (const m of tex.matchAll(re)) for (const k of (m[1] ?? "").split(",")) if (k.trim() && k.trim() !== "*") keys.add(k.trim()); return [...keys]; }