import { Router, type Request, type Response } from "express"; import { Cite } from "@citation-js/core"; import "@citation-js/plugin-bibtex"; import "@citation-js/plugin-doi"; import "@citation-js/plugin-csl"; const router = Router(); /** * POST /api/citations/resolve * Body: { input: string } – DOI, DOI URL, or BibTeX string * Returns: { entries: CSL-JSON[] } */ router.post("/resolve", async (req: Request, res: Response) => { try { const { input } = req.body; if (!input || typeof input !== "string") { res.status(400).json({ error: "Missing 'input' string" }); return; } const cite = await Cite.async(input.trim()); const entries = cite.get({ format: "real", type: "json", style: "csl" }); if (!entries.length) { res.status(404).json({ error: "Could not resolve any reference" }); return; } const enriched = entries.map((entry: any) => ({ ...entry, id: entry.id || generateKey(entry), })); res.json({ entries: enriched }); } catch (err: any) { console.error("[citations] resolve error:", err.message); res.status(422).json({ error: err.message || "Failed to resolve" }); } }); /** * POST /api/citations/format * Body: { entries: CSL-JSON[], style?: string, locale?: string } * Returns: { html: string } */ router.post("/format", async (req: Request, res: Response) => { try { const { entries, style = "apa", locale = "en-US" } = req.body; if (!Array.isArray(entries) || !entries.length) { res.status(400).json({ error: "Missing 'entries' array" }); return; } const cite = new Cite(entries); const html = cite.format("bibliography", { format: "html", template: style, lang: locale, }); res.json({ html }); } catch (err: any) { console.error("[citations] format error:", err.message); res.status(422).json({ error: err.message || "Failed to format" }); } }); /** * POST /api/citations/import-bib * Body: { bibtex: string } * Returns: { entries: CSL-JSON[] } */ router.post("/import-bib", async (req: Request, res: Response) => { try { const { bibtex } = req.body; if (!bibtex || typeof bibtex !== "string") { res.status(400).json({ error: "Missing 'bibtex' string" }); return; } const cite = new Cite(bibtex.trim()); const entries = cite.get({ format: "real", type: "json", style: "csl" }); const enriched = entries.map((entry: any) => ({ ...entry, id: entry.id || generateKey(entry), })); res.json({ entries: enriched }); } catch (err: any) { console.error("[citations] import-bib error:", err.message); res.status(422).json({ error: err.message || "Failed to parse BibTeX" }); } }); function generateKey(entry: any): string { const firstAuthor = entry.author?.[0]?.family || "unknown"; const year = entry.issued?.["date-parts"]?.[0]?.[0] || "nd"; const base = `${firstAuthor.toLowerCase()}${year}`; return base.replace(/[^a-z0-9]/gi, ""); } export { router as citationsRouter };