File size: 3,056 Bytes
561e6f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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 };