@ncukondo/reference-manager 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/chunks/{file-watcher-Dqkw6R7-.js → file-watcher-CBAbblss.js} +120 -19
- package/dist/chunks/file-watcher-CBAbblss.js.map +1 -0
- package/dist/chunks/index-Bl_mOQRe.js +1657 -0
- package/dist/chunks/index-Bl_mOQRe.js.map +1 -0
- package/dist/cli/commands/fulltext.d.ts +4 -3
- package/dist/cli/commands/fulltext.d.ts.map +1 -1
- package/dist/cli/commands/remove.d.ts +2 -1
- package/dist/cli/commands/remove.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts +2 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/server-client.d.ts +5 -4
- package/dist/cli/server-client.d.ts.map +1 -1
- package/dist/cli.js +49 -42
- package/dist/cli.js.map +1 -1
- package/dist/core/library-interface.d.ts +22 -4
- package/dist/core/library-interface.d.ts.map +1 -1
- package/dist/core/library.d.ts +2 -10
- package/dist/core/library.d.ts.map +1 -1
- package/dist/core/reference.d.ts +1 -0
- package/dist/core/reference.d.ts.map +1 -1
- package/dist/features/duplicate/detector.d.ts.map +1 -1
- package/dist/features/duplicate/types.d.ts +2 -1
- package/dist/features/duplicate/types.d.ts.map +1 -1
- package/dist/features/import/cache.d.ts +8 -0
- package/dist/features/import/cache.d.ts.map +1 -1
- package/dist/features/import/detector.d.ts +11 -3
- package/dist/features/import/detector.d.ts.map +1 -1
- package/dist/features/import/fetcher.d.ts +8 -0
- package/dist/features/import/fetcher.d.ts.map +1 -1
- package/dist/features/import/importer.d.ts.map +1 -1
- package/dist/features/import/normalizer.d.ts +26 -0
- package/dist/features/import/normalizer.d.ts.map +1 -1
- package/dist/features/import/rate-limiter.d.ts +1 -1
- package/dist/features/import/rate-limiter.d.ts.map +1 -1
- package/dist/features/operations/cite.d.ts +3 -3
- package/dist/features/operations/cite.d.ts.map +1 -1
- package/dist/features/operations/fulltext/attach.d.ts +3 -3
- package/dist/features/operations/fulltext/attach.d.ts.map +1 -1
- package/dist/features/operations/fulltext/detach.d.ts +3 -3
- package/dist/features/operations/fulltext/detach.d.ts.map +1 -1
- package/dist/features/operations/fulltext/get.d.ts +3 -3
- package/dist/features/operations/fulltext/get.d.ts.map +1 -1
- package/dist/features/operations/remove.d.ts +3 -3
- package/dist/features/operations/remove.d.ts.map +1 -1
- package/dist/features/operations/update.d.ts +3 -3
- package/dist/features/operations/update.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/server/routes/references.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/package.json +2 -1
- package/dist/chunks/file-watcher-Dqkw6R7-.js.map +0 -1
- package/dist/chunks/index-9dyK2f9_.js +0 -29851
- package/dist/chunks/index-9dyK2f9_.js.map +0 -1
|
@@ -0,0 +1,1657 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { e as CslItemSchema, d as detectDuplicate, t as tokenize, s as search$1, b as sortResults, L as Library, F as FileWatcher } from "./file-watcher-CBAbblss.js";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { Cite } from "@citation-js/core";
|
|
5
|
+
import "@citation-js/plugin-doi";
|
|
6
|
+
import "@citation-js/plugin-isbn";
|
|
7
|
+
import "@citation-js/plugin-bibtex";
|
|
8
|
+
import "@citation-js/plugin-ris";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import "node:path";
|
|
11
|
+
import "@citation-js/plugin-csl";
|
|
12
|
+
async function updateReference(library, options) {
|
|
13
|
+
const { identifier, idType = "id", updates, onIdCollision = "fail" } = options;
|
|
14
|
+
const updateResult = await library.update(identifier, updates, { idType, onIdCollision });
|
|
15
|
+
if (!updateResult.updated) {
|
|
16
|
+
const result2 = { updated: false };
|
|
17
|
+
if (updateResult.idCollision) {
|
|
18
|
+
result2.idCollision = true;
|
|
19
|
+
}
|
|
20
|
+
return result2;
|
|
21
|
+
}
|
|
22
|
+
await library.save();
|
|
23
|
+
const result = { updated: true };
|
|
24
|
+
if (updateResult.item) {
|
|
25
|
+
result.item = updateResult.item;
|
|
26
|
+
}
|
|
27
|
+
if (updateResult.idChanged && updateResult.newId) {
|
|
28
|
+
result.idChanged = true;
|
|
29
|
+
result.newId = updateResult.newId;
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
const BUILTIN_STYLES = ["apa", "vancouver", "harvard"];
|
|
34
|
+
function isBuiltinStyle(styleName) {
|
|
35
|
+
return BUILTIN_STYLES.includes(styleName);
|
|
36
|
+
}
|
|
37
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
38
|
+
const pmidCache = /* @__PURE__ */ new Map();
|
|
39
|
+
const doiCache = /* @__PURE__ */ new Map();
|
|
40
|
+
const isbnCache = /* @__PURE__ */ new Map();
|
|
41
|
+
function isEntryValid(entry) {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
return now - entry.cachedAt < entry.ttlMs;
|
|
44
|
+
}
|
|
45
|
+
function getFromCache(cache, key) {
|
|
46
|
+
const entry = cache.get(key);
|
|
47
|
+
if (!entry) {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
if (!isEntryValid(entry)) {
|
|
51
|
+
cache.delete(key);
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
return entry.item;
|
|
55
|
+
}
|
|
56
|
+
function storeInCache(cache, key, item, config) {
|
|
57
|
+
const ttlMs = DEFAULT_TTL_MS;
|
|
58
|
+
cache.set(key, {
|
|
59
|
+
item,
|
|
60
|
+
cachedAt: Date.now(),
|
|
61
|
+
ttlMs
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function getPmidFromCache(pmid) {
|
|
65
|
+
return getFromCache(pmidCache, pmid);
|
|
66
|
+
}
|
|
67
|
+
function cachePmidResult(pmid, item, config) {
|
|
68
|
+
storeInCache(pmidCache, pmid, item);
|
|
69
|
+
}
|
|
70
|
+
function getDoiFromCache(doi) {
|
|
71
|
+
return getFromCache(doiCache, doi);
|
|
72
|
+
}
|
|
73
|
+
function cacheDoiResult(doi, item, config) {
|
|
74
|
+
storeInCache(doiCache, doi, item);
|
|
75
|
+
}
|
|
76
|
+
function getIsbnFromCache(isbn) {
|
|
77
|
+
return getFromCache(isbnCache, isbn);
|
|
78
|
+
}
|
|
79
|
+
function cacheIsbnResult(isbn, item, config) {
|
|
80
|
+
storeInCache(isbnCache, isbn, item);
|
|
81
|
+
}
|
|
82
|
+
const DOI_URL_PREFIXES$1 = [
|
|
83
|
+
"https://doi.org/",
|
|
84
|
+
"http://doi.org/",
|
|
85
|
+
"https://dx.doi.org/",
|
|
86
|
+
"http://dx.doi.org/"
|
|
87
|
+
];
|
|
88
|
+
function normalizeDoi(doi) {
|
|
89
|
+
const trimmed = doi.trim();
|
|
90
|
+
if (!trimmed) {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
const lowerInput = trimmed.toLowerCase();
|
|
94
|
+
for (const prefix of DOI_URL_PREFIXES$1) {
|
|
95
|
+
if (lowerInput.startsWith(prefix.toLowerCase())) {
|
|
96
|
+
return trimmed.slice(prefix.length);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return trimmed;
|
|
100
|
+
}
|
|
101
|
+
function normalizePmid(pmid) {
|
|
102
|
+
const trimmed = pmid.trim();
|
|
103
|
+
if (!trimmed) {
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
106
|
+
const normalized = trimmed.replace(/^pmid:\s*/i, "");
|
|
107
|
+
return normalized.trim();
|
|
108
|
+
}
|
|
109
|
+
function normalizeIsbn(isbn) {
|
|
110
|
+
const trimmed = isbn.trim();
|
|
111
|
+
if (!trimmed) {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
if (!/^isbn:/i.test(trimmed)) {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
let normalized = trimmed.replace(/^isbn:\s*/i, "");
|
|
118
|
+
normalized = normalized.replace(/[-\s]/g, "");
|
|
119
|
+
normalized = normalized.toUpperCase();
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
const EXTENSION_MAP = {
|
|
123
|
+
".json": "json",
|
|
124
|
+
".bib": "bibtex",
|
|
125
|
+
".ris": "ris"
|
|
126
|
+
};
|
|
127
|
+
const DOI_URL_PREFIXES = [
|
|
128
|
+
"https://doi.org/",
|
|
129
|
+
"http://doi.org/",
|
|
130
|
+
"https://dx.doi.org/",
|
|
131
|
+
"http://dx.doi.org/"
|
|
132
|
+
];
|
|
133
|
+
function detectByExtension(input) {
|
|
134
|
+
if (!input) return "unknown";
|
|
135
|
+
const dotIndex = input.lastIndexOf(".");
|
|
136
|
+
if (dotIndex === -1 || dotIndex === input.length - 1) {
|
|
137
|
+
return "unknown";
|
|
138
|
+
}
|
|
139
|
+
const ext = input.slice(dotIndex).toLowerCase();
|
|
140
|
+
return EXTENSION_MAP[ext] ?? "unknown";
|
|
141
|
+
}
|
|
142
|
+
function detectByContent(content) {
|
|
143
|
+
const trimmed = content.trim();
|
|
144
|
+
if (!trimmed) return "unknown";
|
|
145
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
146
|
+
return "json";
|
|
147
|
+
}
|
|
148
|
+
if (trimmed.startsWith("@")) {
|
|
149
|
+
return "bibtex";
|
|
150
|
+
}
|
|
151
|
+
if (trimmed.startsWith("TY -")) {
|
|
152
|
+
return "ris";
|
|
153
|
+
}
|
|
154
|
+
return detectIdentifier(trimmed);
|
|
155
|
+
}
|
|
156
|
+
function detectIdentifier(input) {
|
|
157
|
+
const parts = input.split(/\s+/).filter((p) => p.length > 0);
|
|
158
|
+
if (parts.length === 0) {
|
|
159
|
+
return "unknown";
|
|
160
|
+
}
|
|
161
|
+
const formats = [];
|
|
162
|
+
for (const part of parts) {
|
|
163
|
+
const format = detectSingleIdentifier(part);
|
|
164
|
+
if (format === "unknown") {
|
|
165
|
+
return "unknown";
|
|
166
|
+
}
|
|
167
|
+
formats.push(format);
|
|
168
|
+
}
|
|
169
|
+
if (formats.length === 1) {
|
|
170
|
+
return formats[0];
|
|
171
|
+
}
|
|
172
|
+
return "identifiers";
|
|
173
|
+
}
|
|
174
|
+
function detectSingleIdentifier(input) {
|
|
175
|
+
if (isDoi(input)) {
|
|
176
|
+
return "doi";
|
|
177
|
+
}
|
|
178
|
+
if (isIsbn(input)) {
|
|
179
|
+
return "isbn";
|
|
180
|
+
}
|
|
181
|
+
if (isPmid(input)) {
|
|
182
|
+
return "pmid";
|
|
183
|
+
}
|
|
184
|
+
return "unknown";
|
|
185
|
+
}
|
|
186
|
+
function isDoi(input) {
|
|
187
|
+
for (const prefix of DOI_URL_PREFIXES) {
|
|
188
|
+
if (input.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
189
|
+
const remainder = input.slice(prefix.length);
|
|
190
|
+
return isDoiFormat(remainder);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return isDoiFormat(input);
|
|
194
|
+
}
|
|
195
|
+
function isDoiFormat(input) {
|
|
196
|
+
if (!input.startsWith("10.")) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (input.length <= 3) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
const slashIndex = input.indexOf("/");
|
|
203
|
+
if (slashIndex === -1 || slashIndex <= 3) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
function isPmid(input) {
|
|
209
|
+
if (!input || input.length === 0) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const normalized = normalizePmid(input);
|
|
213
|
+
if (!normalized) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
return /^\d+$/.test(normalized);
|
|
217
|
+
}
|
|
218
|
+
function isIsbn(input) {
|
|
219
|
+
if (!input || input.length === 0) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const normalized = normalizeIsbn(input);
|
|
223
|
+
if (!normalized) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
if (normalized.length !== 10 && normalized.length !== 13) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (normalized.length === 10) {
|
|
230
|
+
if (!/^\d{9}[\dX]$/.test(normalized)) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
if (!/^\d{13}$/.test(normalized)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
const RATE_LIMITS = {
|
|
241
|
+
pubmed: {
|
|
242
|
+
withoutApiKey: 3,
|
|
243
|
+
// 3 req/sec
|
|
244
|
+
withApiKey: 10
|
|
245
|
+
// 10 req/sec
|
|
246
|
+
},
|
|
247
|
+
crossref: 50,
|
|
248
|
+
// 50 req/sec
|
|
249
|
+
isbn: 10
|
|
250
|
+
// 10 req/sec (conservative for Google Books API daily limit)
|
|
251
|
+
};
|
|
252
|
+
class RateLimiterImpl {
|
|
253
|
+
requestsPerSecond;
|
|
254
|
+
intervalMs;
|
|
255
|
+
_lastRequestTime = 0;
|
|
256
|
+
_pending = Promise.resolve();
|
|
257
|
+
constructor(requestsPerSecond) {
|
|
258
|
+
this.requestsPerSecond = requestsPerSecond;
|
|
259
|
+
this.intervalMs = 1e3 / requestsPerSecond;
|
|
260
|
+
}
|
|
261
|
+
get lastRequestTime() {
|
|
262
|
+
return this._lastRequestTime;
|
|
263
|
+
}
|
|
264
|
+
async acquire() {
|
|
265
|
+
this._pending = this._pending.then(() => this._acquireInternal());
|
|
266
|
+
return this._pending;
|
|
267
|
+
}
|
|
268
|
+
async _acquireInternal() {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
const elapsed = now - this._lastRequestTime;
|
|
271
|
+
const waitTime = Math.max(0, this.intervalMs - elapsed);
|
|
272
|
+
if (waitTime > 0 && this._lastRequestTime > 0) {
|
|
273
|
+
await this._delay(waitTime);
|
|
274
|
+
}
|
|
275
|
+
this._lastRequestTime = Date.now();
|
|
276
|
+
}
|
|
277
|
+
_delay(ms) {
|
|
278
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const limiters = /* @__PURE__ */ new Map();
|
|
282
|
+
function createRateLimiter(options) {
|
|
283
|
+
return new RateLimiterImpl(options.requestsPerSecond);
|
|
284
|
+
}
|
|
285
|
+
function getRateLimiter(api, config) {
|
|
286
|
+
const existing = limiters.get(api);
|
|
287
|
+
if (existing) {
|
|
288
|
+
return existing;
|
|
289
|
+
}
|
|
290
|
+
const requestsPerSecond = getRequestsPerSecond(api, config);
|
|
291
|
+
const limiter = createRateLimiter({ requestsPerSecond });
|
|
292
|
+
limiters.set(api, limiter);
|
|
293
|
+
return limiter;
|
|
294
|
+
}
|
|
295
|
+
function getRequestsPerSecond(api, config) {
|
|
296
|
+
switch (api) {
|
|
297
|
+
case "pubmed":
|
|
298
|
+
return config.pubmedApiKey ? RATE_LIMITS.pubmed.withApiKey : RATE_LIMITS.pubmed.withoutApiKey;
|
|
299
|
+
case "crossref":
|
|
300
|
+
return RATE_LIMITS.crossref;
|
|
301
|
+
case "isbn":
|
|
302
|
+
return RATE_LIMITS.isbn;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const PMC_API_BASE = "https://pmc.ncbi.nlm.nih.gov/api/ctxp/v1/pubmed/";
|
|
306
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
307
|
+
const DOI_PATTERN = /^10\.\d{4,}(?:\.\d+)*\/\S+$/;
|
|
308
|
+
function buildPmcUrl(pmids, config) {
|
|
309
|
+
const url = new URL(PMC_API_BASE);
|
|
310
|
+
url.searchParams.set("format", "csl");
|
|
311
|
+
for (const pmid of pmids) {
|
|
312
|
+
url.searchParams.append("id", pmid);
|
|
313
|
+
}
|
|
314
|
+
if (config.email) {
|
|
315
|
+
url.searchParams.set("email", config.email);
|
|
316
|
+
}
|
|
317
|
+
if (config.apiKey) {
|
|
318
|
+
url.searchParams.set("api_key", config.apiKey);
|
|
319
|
+
}
|
|
320
|
+
return url.toString();
|
|
321
|
+
}
|
|
322
|
+
function extractPmidFromId(id) {
|
|
323
|
+
if (!id) return void 0;
|
|
324
|
+
const match = id.match(/^pmid:(\d+)$/);
|
|
325
|
+
return match?.[1];
|
|
326
|
+
}
|
|
327
|
+
function parseRawItems(rawItems) {
|
|
328
|
+
const foundItems = /* @__PURE__ */ new Map();
|
|
329
|
+
const validationErrors = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const rawItem of rawItems) {
|
|
331
|
+
const parseResult = CslItemSchema.safeParse(rawItem);
|
|
332
|
+
if (parseResult.success) {
|
|
333
|
+
const pmid = extractPmidFromId(parseResult.data.id);
|
|
334
|
+
if (pmid) {
|
|
335
|
+
foundItems.set(pmid, parseResult.data);
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
const maybeId = rawItem?.id;
|
|
339
|
+
const pmid = extractPmidFromId(maybeId);
|
|
340
|
+
if (pmid) {
|
|
341
|
+
validationErrors.set(pmid, parseResult.error.message);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { foundItems, validationErrors };
|
|
346
|
+
}
|
|
347
|
+
function buildPmidResult(pmid, foundItems, validationErrors) {
|
|
348
|
+
const item = foundItems.get(pmid);
|
|
349
|
+
if (item) {
|
|
350
|
+
return { pmid, success: true, item };
|
|
351
|
+
}
|
|
352
|
+
const validationError = validationErrors.get(pmid);
|
|
353
|
+
if (validationError) {
|
|
354
|
+
return {
|
|
355
|
+
pmid,
|
|
356
|
+
success: false,
|
|
357
|
+
error: `Invalid CSL-JSON data: ${validationError}`
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
pmid,
|
|
362
|
+
success: false,
|
|
363
|
+
error: `PMID ${pmid} not found`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async function fetchPmids(pmids, config) {
|
|
367
|
+
if (pmids.length === 0) {
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
const rateLimiterConfig = config.apiKey ? { pubmedApiKey: config.apiKey } : {};
|
|
371
|
+
const rateLimiter = getRateLimiter("pubmed", rateLimiterConfig);
|
|
372
|
+
await rateLimiter.acquire();
|
|
373
|
+
const url = buildPmcUrl(pmids, config);
|
|
374
|
+
try {
|
|
375
|
+
const response = await fetch(url, {
|
|
376
|
+
method: "GET",
|
|
377
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
378
|
+
});
|
|
379
|
+
if (!response.ok) {
|
|
380
|
+
const errorMsg = `HTTP ${response.status}: ${response.statusText}`;
|
|
381
|
+
return pmids.map((pmid) => ({
|
|
382
|
+
pmid,
|
|
383
|
+
success: false,
|
|
384
|
+
error: errorMsg
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
const data = await response.json();
|
|
388
|
+
const rawItems = Array.isArray(data) ? data : [data];
|
|
389
|
+
const { foundItems, validationErrors } = parseRawItems(rawItems);
|
|
390
|
+
return pmids.map((pmid) => buildPmidResult(pmid, foundItems, validationErrors));
|
|
391
|
+
} catch (error) {
|
|
392
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
393
|
+
return pmids.map((pmid) => ({
|
|
394
|
+
pmid,
|
|
395
|
+
success: false,
|
|
396
|
+
error: errorMsg
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function fetchDoi(doi) {
|
|
401
|
+
if (!DOI_PATTERN.test(doi)) {
|
|
402
|
+
return {
|
|
403
|
+
success: false,
|
|
404
|
+
error: `Invalid DOI format: ${doi}`
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const rateLimiter = getRateLimiter("crossref", {});
|
|
408
|
+
await rateLimiter.acquire();
|
|
409
|
+
try {
|
|
410
|
+
const cite2 = await Cite.async(doi);
|
|
411
|
+
const rawItems = cite2.get({ format: "real", type: "json" });
|
|
412
|
+
if (!rawItems || !Array.isArray(rawItems) || rawItems.length === 0) {
|
|
413
|
+
return {
|
|
414
|
+
success: false,
|
|
415
|
+
error: `No data returned for DOI ${doi}`
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const parseResult = CslItemSchema.safeParse(rawItems[0]);
|
|
419
|
+
if (!parseResult.success) {
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
error: `Invalid CSL-JSON data for DOI ${doi}: ${parseResult.error.message}`
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return { success: true, item: parseResult.data };
|
|
426
|
+
} catch (error) {
|
|
427
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
428
|
+
return {
|
|
429
|
+
success: false,
|
|
430
|
+
error: errorMsg
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const ISBN10_PATTERN = /^\d{9}[\dX]$/;
|
|
435
|
+
const ISBN13_PATTERN = /^\d{13}$/;
|
|
436
|
+
async function fetchIsbn(isbn) {
|
|
437
|
+
if (!ISBN10_PATTERN.test(isbn) && !ISBN13_PATTERN.test(isbn)) {
|
|
438
|
+
return {
|
|
439
|
+
success: false,
|
|
440
|
+
error: `Invalid ISBN format: ${isbn}`
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const rateLimiter = getRateLimiter("isbn", {});
|
|
444
|
+
await rateLimiter.acquire();
|
|
445
|
+
try {
|
|
446
|
+
const cite2 = await Cite.async(isbn);
|
|
447
|
+
const rawItems = cite2.get({ format: "real", type: "json" });
|
|
448
|
+
if (!rawItems || !Array.isArray(rawItems) || rawItems.length === 0) {
|
|
449
|
+
return {
|
|
450
|
+
success: false,
|
|
451
|
+
error: `No data returned for ISBN ${isbn}`
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const parseResult = CslItemSchema.safeParse(rawItems[0]);
|
|
455
|
+
if (!parseResult.success) {
|
|
456
|
+
return {
|
|
457
|
+
success: false,
|
|
458
|
+
error: `Invalid CSL-JSON data for ISBN ${isbn}: ${parseResult.error.message}`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return { success: true, item: parseResult.data };
|
|
462
|
+
} catch (error) {
|
|
463
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
464
|
+
return {
|
|
465
|
+
success: false,
|
|
466
|
+
error: errorMsg
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function parseBibtex(content) {
|
|
471
|
+
return parseWithCitationJs(content, "bibtex");
|
|
472
|
+
}
|
|
473
|
+
function parseRis(content) {
|
|
474
|
+
return parseWithCitationJs(content, "ris");
|
|
475
|
+
}
|
|
476
|
+
function parseWithCitationJs(content, format) {
|
|
477
|
+
const trimmed = content.trim();
|
|
478
|
+
if (!trimmed) {
|
|
479
|
+
return { success: true, items: [] };
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const cite2 = new Cite(trimmed);
|
|
483
|
+
const items = cite2.get({ format: "real", type: "json" });
|
|
484
|
+
if (!items || items.length === 0) {
|
|
485
|
+
if (isEmptyFormat(trimmed, format)) {
|
|
486
|
+
return { success: true, items: [] };
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
items: [],
|
|
491
|
+
error: `No valid ${format.toUpperCase()} entries found`
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return { success: true, items };
|
|
495
|
+
} catch (error) {
|
|
496
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
497
|
+
if (isEmptyFormat(trimmed, format)) {
|
|
498
|
+
return { success: true, items: [] };
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
success: false,
|
|
502
|
+
items: [],
|
|
503
|
+
error: `Failed to parse ${format.toUpperCase()}: ${errorMessage}`
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function isEmptyFormat(content, format) {
|
|
508
|
+
if (format === "bibtex") {
|
|
509
|
+
const lines = content.split("\n");
|
|
510
|
+
return lines.every((line) => {
|
|
511
|
+
const trimmed = line.trim();
|
|
512
|
+
return trimmed === "" || trimmed.startsWith("%");
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
function classifyIdentifiers(identifiers) {
|
|
518
|
+
const pmids = [];
|
|
519
|
+
const dois = [];
|
|
520
|
+
const isbns = [];
|
|
521
|
+
const unknowns = [];
|
|
522
|
+
for (const id of identifiers) {
|
|
523
|
+
if (isPmid(id)) {
|
|
524
|
+
pmids.push(normalizePmid(id));
|
|
525
|
+
} else if (isDoi(id)) {
|
|
526
|
+
dois.push(normalizeDoi(id));
|
|
527
|
+
} else if (isIsbn(id)) {
|
|
528
|
+
isbns.push(normalizeIsbn(id));
|
|
529
|
+
} else {
|
|
530
|
+
unknowns.push(id);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return { pmids, dois, isbns, unknowns };
|
|
534
|
+
}
|
|
535
|
+
function buildUnknownResults(unknowns) {
|
|
536
|
+
return unknowns.map((unknown) => ({
|
|
537
|
+
success: false,
|
|
538
|
+
error: `Cannot interpret '${unknown}' as identifier (not a valid PMID or DOI)`,
|
|
539
|
+
source: unknown
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
async function fetchPmidsWithCache(pmids, pubmedConfig) {
|
|
543
|
+
const results = [];
|
|
544
|
+
const pmidsToFetch = [];
|
|
545
|
+
for (const pmid of pmids) {
|
|
546
|
+
const cached = getPmidFromCache(pmid);
|
|
547
|
+
if (cached) {
|
|
548
|
+
results.push({ success: true, item: cached, source: pmid });
|
|
549
|
+
} else {
|
|
550
|
+
pmidsToFetch.push(pmid);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (pmidsToFetch.length > 0) {
|
|
554
|
+
const fetchResults = await fetchPmids(pmidsToFetch, pubmedConfig);
|
|
555
|
+
for (const fetchResult of fetchResults) {
|
|
556
|
+
if (fetchResult.success) {
|
|
557
|
+
cachePmidResult(fetchResult.pmid, fetchResult.item);
|
|
558
|
+
results.push({
|
|
559
|
+
success: true,
|
|
560
|
+
item: fetchResult.item,
|
|
561
|
+
source: fetchResult.pmid
|
|
562
|
+
});
|
|
563
|
+
} else {
|
|
564
|
+
results.push({
|
|
565
|
+
success: false,
|
|
566
|
+
error: fetchResult.error,
|
|
567
|
+
source: fetchResult.pmid
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return results;
|
|
573
|
+
}
|
|
574
|
+
async function fetchDoisWithCache(dois) {
|
|
575
|
+
const results = [];
|
|
576
|
+
for (const doi of dois) {
|
|
577
|
+
const cached = getDoiFromCache(doi);
|
|
578
|
+
if (cached) {
|
|
579
|
+
results.push({ success: true, item: cached, source: doi });
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const fetchResult = await fetchDoi(doi);
|
|
583
|
+
if (fetchResult.success) {
|
|
584
|
+
cacheDoiResult(doi, fetchResult.item);
|
|
585
|
+
results.push({ success: true, item: fetchResult.item, source: doi });
|
|
586
|
+
} else {
|
|
587
|
+
results.push({ success: false, error: fetchResult.error, source: doi });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return results;
|
|
591
|
+
}
|
|
592
|
+
async function fetchIsbnsWithCache(isbns) {
|
|
593
|
+
const results = [];
|
|
594
|
+
for (const isbn of isbns) {
|
|
595
|
+
const cached = getIsbnFromCache(isbn);
|
|
596
|
+
if (cached) {
|
|
597
|
+
results.push({ success: true, item: cached, source: isbn });
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const fetchResult = await fetchIsbn(isbn);
|
|
601
|
+
if (fetchResult.success) {
|
|
602
|
+
cacheIsbnResult(isbn, fetchResult.item);
|
|
603
|
+
results.push({ success: true, item: fetchResult.item, source: isbn });
|
|
604
|
+
} else {
|
|
605
|
+
results.push({ success: false, error: fetchResult.error, source: isbn });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return results;
|
|
609
|
+
}
|
|
610
|
+
function parseJsonContent(content) {
|
|
611
|
+
try {
|
|
612
|
+
const parsed = JSON.parse(content);
|
|
613
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
614
|
+
if (items.length === 0) {
|
|
615
|
+
return { results: [] };
|
|
616
|
+
}
|
|
617
|
+
const results = [];
|
|
618
|
+
for (const item of items) {
|
|
619
|
+
const parseResult = CslItemSchema.safeParse(item);
|
|
620
|
+
if (parseResult.success) {
|
|
621
|
+
results.push({ success: true, item: parseResult.data, source: "json" });
|
|
622
|
+
} else {
|
|
623
|
+
results.push({
|
|
624
|
+
success: false,
|
|
625
|
+
error: `Invalid CSL-JSON: ${parseResult.error.message}`,
|
|
626
|
+
source: "json"
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return { results };
|
|
631
|
+
} catch (error) {
|
|
632
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
633
|
+
return {
|
|
634
|
+
results: [{ success: false, error: `Failed to parse JSON: ${message}`, source: "json" }]
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function parseBibtexContent(content) {
|
|
639
|
+
const parseResult = parseBibtex(content);
|
|
640
|
+
if (!parseResult.success) {
|
|
641
|
+
return {
|
|
642
|
+
results: [
|
|
643
|
+
{ success: false, error: parseResult.error ?? "Failed to parse BibTeX", source: "bibtex" }
|
|
644
|
+
]
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (parseResult.items.length === 0) {
|
|
648
|
+
return { results: [] };
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
results: parseResult.items.map((item) => ({
|
|
652
|
+
success: true,
|
|
653
|
+
item,
|
|
654
|
+
source: "bibtex"
|
|
655
|
+
}))
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function parseRisContent(content) {
|
|
659
|
+
const parseResult = parseRis(content);
|
|
660
|
+
if (!parseResult.success) {
|
|
661
|
+
return {
|
|
662
|
+
results: [
|
|
663
|
+
{ success: false, error: parseResult.error ?? "Failed to parse RIS", source: "ris" }
|
|
664
|
+
]
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (parseResult.items.length === 0) {
|
|
668
|
+
return { results: [] };
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
results: parseResult.items.map((item) => ({
|
|
672
|
+
success: true,
|
|
673
|
+
item,
|
|
674
|
+
source: "ris"
|
|
675
|
+
}))
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
async function importFromContent(content, format, _options) {
|
|
679
|
+
let actualFormat;
|
|
680
|
+
if (format === "auto") {
|
|
681
|
+
actualFormat = detectByContent(content);
|
|
682
|
+
if (actualFormat === "unknown") {
|
|
683
|
+
return {
|
|
684
|
+
results: [
|
|
685
|
+
{
|
|
686
|
+
success: false,
|
|
687
|
+
error: "Cannot detect input format. Use --format to specify explicitly.",
|
|
688
|
+
source: "content"
|
|
689
|
+
}
|
|
690
|
+
]
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
actualFormat = format;
|
|
695
|
+
}
|
|
696
|
+
switch (actualFormat) {
|
|
697
|
+
case "json":
|
|
698
|
+
return parseJsonContent(content);
|
|
699
|
+
case "bibtex":
|
|
700
|
+
return parseBibtexContent(content);
|
|
701
|
+
case "ris":
|
|
702
|
+
return parseRisContent(content);
|
|
703
|
+
default:
|
|
704
|
+
return {
|
|
705
|
+
results: [
|
|
706
|
+
{
|
|
707
|
+
success: false,
|
|
708
|
+
error: `Unsupported format for content parsing: ${actualFormat}`,
|
|
709
|
+
source: "content"
|
|
710
|
+
}
|
|
711
|
+
]
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async function importFromIdentifiers(identifiers, options) {
|
|
716
|
+
if (identifiers.length === 0) {
|
|
717
|
+
return { results: [] };
|
|
718
|
+
}
|
|
719
|
+
const { pmids, dois, isbns, unknowns } = classifyIdentifiers(identifiers);
|
|
720
|
+
const results = [];
|
|
721
|
+
results.push(...buildUnknownResults(unknowns));
|
|
722
|
+
const pmidResults = await fetchPmidsWithCache(pmids, options.pubmedConfig ?? {});
|
|
723
|
+
results.push(...pmidResults);
|
|
724
|
+
const doiResults = await fetchDoisWithCache(dois);
|
|
725
|
+
results.push(...doiResults);
|
|
726
|
+
const isbnResults = await fetchIsbnsWithCache(isbns);
|
|
727
|
+
results.push(...isbnResults);
|
|
728
|
+
return { results };
|
|
729
|
+
}
|
|
730
|
+
function looksLikeFilePath(input) {
|
|
731
|
+
const fileExtensions = [".json", ".bib", ".ris", ".txt", ".xml", ".yaml", ".yml"];
|
|
732
|
+
const lowerInput = input.toLowerCase();
|
|
733
|
+
return fileExtensions.some((ext) => lowerInput.endsWith(ext));
|
|
734
|
+
}
|
|
735
|
+
async function processFile(filePath, options) {
|
|
736
|
+
try {
|
|
737
|
+
const content = readFileSync(filePath, "utf-8");
|
|
738
|
+
let format;
|
|
739
|
+
if (options.format && options.format !== "auto") {
|
|
740
|
+
format = options.format;
|
|
741
|
+
} else {
|
|
742
|
+
const extFormat = detectByExtension(filePath);
|
|
743
|
+
format = extFormat !== "unknown" ? extFormat : "auto";
|
|
744
|
+
}
|
|
745
|
+
const result = await importFromContent(content, format, options);
|
|
746
|
+
return result.results.map((r) => ({
|
|
747
|
+
...r,
|
|
748
|
+
source: filePath
|
|
749
|
+
}));
|
|
750
|
+
} catch (error) {
|
|
751
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
752
|
+
return [
|
|
753
|
+
{
|
|
754
|
+
success: false,
|
|
755
|
+
error: `Failed to read file: ${message}`,
|
|
756
|
+
source: filePath
|
|
757
|
+
}
|
|
758
|
+
];
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async function processIdentifiers(inputs, options) {
|
|
762
|
+
const results = [];
|
|
763
|
+
const validIdentifiers = [];
|
|
764
|
+
for (const input of inputs) {
|
|
765
|
+
const isValidPmid = isPmid(input);
|
|
766
|
+
const isValidDoi = isDoi(input);
|
|
767
|
+
if (isValidPmid || isValidDoi) {
|
|
768
|
+
validIdentifiers.push(input);
|
|
769
|
+
} else {
|
|
770
|
+
const hint = looksLikeFilePath(input) ? " Hint: If this is a file path, check that the file exists." : "";
|
|
771
|
+
results.push({
|
|
772
|
+
success: false,
|
|
773
|
+
error: `Cannot interpret '${input}' as identifier (not a valid PMID or DOI).${hint}`,
|
|
774
|
+
source: input
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (validIdentifiers.length > 0) {
|
|
779
|
+
const fetchResult = await importFromIdentifiers(validIdentifiers, options);
|
|
780
|
+
results.push(...fetchResult.results);
|
|
781
|
+
}
|
|
782
|
+
return results;
|
|
783
|
+
}
|
|
784
|
+
async function importFromInputs(inputs, options) {
|
|
785
|
+
const allResults = [];
|
|
786
|
+
if (options.stdinContent?.trim()) {
|
|
787
|
+
const stdinResults = await processStdinContent(options.stdinContent, options);
|
|
788
|
+
allResults.push(...stdinResults);
|
|
789
|
+
}
|
|
790
|
+
if (inputs.length > 0) {
|
|
791
|
+
const identifiersToFetch = [];
|
|
792
|
+
for (const input of inputs) {
|
|
793
|
+
if (existsSync(input)) {
|
|
794
|
+
const fileResults = await processFile(input, options);
|
|
795
|
+
allResults.push(...fileResults);
|
|
796
|
+
} else {
|
|
797
|
+
identifiersToFetch.push(input);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (identifiersToFetch.length > 0) {
|
|
801
|
+
const identifierResults = await processIdentifiers(identifiersToFetch, options);
|
|
802
|
+
allResults.push(...identifierResults);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return { results: allResults };
|
|
806
|
+
}
|
|
807
|
+
async function processStdinContent(content, options) {
|
|
808
|
+
const format = options.format || "auto";
|
|
809
|
+
if (format === "json" || format === "bibtex" || format === "ris") {
|
|
810
|
+
const result = await importFromContent(content, format);
|
|
811
|
+
return result.results.map((r) => ({
|
|
812
|
+
...r,
|
|
813
|
+
source: r.source === "content" ? "stdin" : r.source
|
|
814
|
+
}));
|
|
815
|
+
}
|
|
816
|
+
if (format === "pmid" || format === "doi") {
|
|
817
|
+
const identifiers2 = content.split(/\s+/).filter((s) => s.length > 0);
|
|
818
|
+
return processIdentifiers(identifiers2, options);
|
|
819
|
+
}
|
|
820
|
+
const detectedFormat = detectByContent(content);
|
|
821
|
+
if (detectedFormat === "json" || detectedFormat === "bibtex" || detectedFormat === "ris") {
|
|
822
|
+
const result = await importFromContent(content, detectedFormat);
|
|
823
|
+
return result.results.map((r) => ({
|
|
824
|
+
...r,
|
|
825
|
+
source: r.source === "content" ? "stdin" : r.source
|
|
826
|
+
}));
|
|
827
|
+
}
|
|
828
|
+
const identifiers = content.split(/\s+/).filter((s) => s.length > 0);
|
|
829
|
+
if (identifiers.length === 0) {
|
|
830
|
+
return [];
|
|
831
|
+
}
|
|
832
|
+
return processIdentifiers(identifiers, options);
|
|
833
|
+
}
|
|
834
|
+
async function addReferences(inputs, library, options) {
|
|
835
|
+
const added = [];
|
|
836
|
+
const failed = [];
|
|
837
|
+
const skipped = [];
|
|
838
|
+
const importOptions = buildImportOptions(options);
|
|
839
|
+
const importResult = await importFromInputs(inputs, importOptions);
|
|
840
|
+
const existingItems = await library.getAll();
|
|
841
|
+
const addedIds = /* @__PURE__ */ new Set();
|
|
842
|
+
for (const result of importResult.results) {
|
|
843
|
+
const processed = await processImportResult(
|
|
844
|
+
result,
|
|
845
|
+
existingItems,
|
|
846
|
+
addedIds,
|
|
847
|
+
options.force ?? false,
|
|
848
|
+
library
|
|
849
|
+
);
|
|
850
|
+
if (processed.type === "failed") {
|
|
851
|
+
failed.push(processed.item);
|
|
852
|
+
} else if (processed.type === "skipped") {
|
|
853
|
+
skipped.push(processed.item);
|
|
854
|
+
} else {
|
|
855
|
+
added.push(processed.item);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (added.length > 0) {
|
|
859
|
+
await library.save();
|
|
860
|
+
}
|
|
861
|
+
return { added, failed, skipped };
|
|
862
|
+
}
|
|
863
|
+
function buildImportOptions(options) {
|
|
864
|
+
const importOptions = {};
|
|
865
|
+
if (options.format !== void 0) {
|
|
866
|
+
importOptions.format = options.format;
|
|
867
|
+
}
|
|
868
|
+
if (options.pubmedConfig !== void 0) {
|
|
869
|
+
importOptions.pubmedConfig = options.pubmedConfig;
|
|
870
|
+
}
|
|
871
|
+
if (options.stdinContent !== void 0) {
|
|
872
|
+
importOptions.stdinContent = options.stdinContent;
|
|
873
|
+
}
|
|
874
|
+
return importOptions;
|
|
875
|
+
}
|
|
876
|
+
async function processImportResult(result, existingItems, addedIds, force, library) {
|
|
877
|
+
if (!result.success) {
|
|
878
|
+
return { type: "failed", item: { source: result.source, error: result.error } };
|
|
879
|
+
}
|
|
880
|
+
const item = result.item;
|
|
881
|
+
if (!force) {
|
|
882
|
+
const duplicateResult = detectDuplicate(item, existingItems);
|
|
883
|
+
const existingMatch = duplicateResult.matches[0];
|
|
884
|
+
if (existingMatch) {
|
|
885
|
+
return {
|
|
886
|
+
type: "skipped",
|
|
887
|
+
item: { source: result.source, existingId: existingMatch.existing.id ?? "" }
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const allExistingIds = /* @__PURE__ */ new Set([...existingItems.map((i) => i.id), ...addedIds]);
|
|
892
|
+
const { id, changed } = resolveIdCollision(item.id, allExistingIds);
|
|
893
|
+
const finalItem = { ...item, id };
|
|
894
|
+
await library.add(finalItem);
|
|
895
|
+
addedIds.add(id);
|
|
896
|
+
const addedItem = {
|
|
897
|
+
id,
|
|
898
|
+
title: typeof finalItem.title === "string" ? finalItem.title : ""
|
|
899
|
+
};
|
|
900
|
+
if (changed) {
|
|
901
|
+
addedItem.idChanged = true;
|
|
902
|
+
addedItem.originalId = item.id;
|
|
903
|
+
}
|
|
904
|
+
return { type: "added", item: addedItem };
|
|
905
|
+
}
|
|
906
|
+
function generateSuffix(index) {
|
|
907
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
|
908
|
+
let suffix = "";
|
|
909
|
+
let n = index;
|
|
910
|
+
do {
|
|
911
|
+
suffix = alphabet[n % 26] + suffix;
|
|
912
|
+
n = Math.floor(n / 26) - 1;
|
|
913
|
+
} while (n >= 0);
|
|
914
|
+
return suffix;
|
|
915
|
+
}
|
|
916
|
+
function resolveIdCollision(baseId, existingIds) {
|
|
917
|
+
if (!existingIds.has(baseId)) {
|
|
918
|
+
return { id: baseId, changed: false };
|
|
919
|
+
}
|
|
920
|
+
let index = 0;
|
|
921
|
+
let newId;
|
|
922
|
+
do {
|
|
923
|
+
const suffix = generateSuffix(index);
|
|
924
|
+
newId = `${baseId}${suffix}`;
|
|
925
|
+
index++;
|
|
926
|
+
} while (existingIds.has(newId));
|
|
927
|
+
return { id: newId, changed: true };
|
|
928
|
+
}
|
|
929
|
+
const add = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
930
|
+
__proto__: null,
|
|
931
|
+
addReferences
|
|
932
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
933
|
+
function formatAuthor(author) {
|
|
934
|
+
const family = author.family || "";
|
|
935
|
+
const givenInitial = author.given ? `${author.given.charAt(0)}.` : "";
|
|
936
|
+
return givenInitial ? `${family}, ${givenInitial}` : family;
|
|
937
|
+
}
|
|
938
|
+
function formatAuthors(authors) {
|
|
939
|
+
return authors.map(formatAuthor).join("; ");
|
|
940
|
+
}
|
|
941
|
+
function formatSingleReference(item) {
|
|
942
|
+
const lines = [];
|
|
943
|
+
const header = item.title ? `[${item.id}] ${item.title}` : `[${item.id}]`;
|
|
944
|
+
lines.push(header);
|
|
945
|
+
if (item.author && item.author.length > 0) {
|
|
946
|
+
lines.push(` Authors: ${formatAuthors(item.author)}`);
|
|
947
|
+
}
|
|
948
|
+
const year = item.issued?.["date-parts"]?.[0]?.[0];
|
|
949
|
+
lines.push(` Year: ${year || "(no year)"}`);
|
|
950
|
+
lines.push(` Type: ${item.type}`);
|
|
951
|
+
if (item.DOI) {
|
|
952
|
+
lines.push(` DOI: ${item.DOI}`);
|
|
953
|
+
}
|
|
954
|
+
if (item.PMID) {
|
|
955
|
+
lines.push(` PMID: ${item.PMID}`);
|
|
956
|
+
}
|
|
957
|
+
if (item.PMCID) {
|
|
958
|
+
lines.push(` PMCID: ${item.PMCID}`);
|
|
959
|
+
}
|
|
960
|
+
if (item.URL) {
|
|
961
|
+
lines.push(` URL: ${item.URL}`);
|
|
962
|
+
}
|
|
963
|
+
const uuid = item.custom?.uuid || "(no uuid)";
|
|
964
|
+
lines.push(` UUID: ${uuid}`);
|
|
965
|
+
return lines.join("\n");
|
|
966
|
+
}
|
|
967
|
+
function formatPretty(items) {
|
|
968
|
+
if (items.length === 0) {
|
|
969
|
+
return "";
|
|
970
|
+
}
|
|
971
|
+
return items.map(formatSingleReference).join("\n\n");
|
|
972
|
+
}
|
|
973
|
+
function mapEntryType(cslType) {
|
|
974
|
+
const typeMap = {
|
|
975
|
+
article: "article",
|
|
976
|
+
"article-journal": "article",
|
|
977
|
+
"article-magazine": "article",
|
|
978
|
+
"article-newspaper": "article",
|
|
979
|
+
book: "book",
|
|
980
|
+
chapter: "inbook",
|
|
981
|
+
"paper-conference": "inproceedings",
|
|
982
|
+
thesis: "phdthesis",
|
|
983
|
+
report: "techreport",
|
|
984
|
+
webpage: "misc"
|
|
985
|
+
};
|
|
986
|
+
return typeMap[cslType] || "misc";
|
|
987
|
+
}
|
|
988
|
+
function formatBibtexAuthor(author) {
|
|
989
|
+
if (author.literal) {
|
|
990
|
+
return author.literal;
|
|
991
|
+
}
|
|
992
|
+
const family = author.family || "";
|
|
993
|
+
const given = author.given || "";
|
|
994
|
+
return given ? `${family}, ${given}` : family;
|
|
995
|
+
}
|
|
996
|
+
function formatBibtexAuthors(authors) {
|
|
997
|
+
return authors.map(formatBibtexAuthor).join(" and ");
|
|
998
|
+
}
|
|
999
|
+
function formatField(name, value) {
|
|
1000
|
+
return ` ${name} = {${value}},`;
|
|
1001
|
+
}
|
|
1002
|
+
function addBasicFields(lines, item) {
|
|
1003
|
+
if (item.title) {
|
|
1004
|
+
lines.push(formatField("title", item.title));
|
|
1005
|
+
}
|
|
1006
|
+
if (item.author && item.author.length > 0) {
|
|
1007
|
+
lines.push(formatField("author", formatBibtexAuthors(item.author)));
|
|
1008
|
+
}
|
|
1009
|
+
const year = item.issued?.["date-parts"]?.[0]?.[0];
|
|
1010
|
+
if (year) {
|
|
1011
|
+
lines.push(formatField("year", String(year)));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function addPublicationDetails(lines, item, entryType) {
|
|
1015
|
+
if (item["container-title"]) {
|
|
1016
|
+
if (entryType === "article") {
|
|
1017
|
+
lines.push(formatField("journal", item["container-title"]));
|
|
1018
|
+
} else if (entryType === "inbook" || entryType === "inproceedings") {
|
|
1019
|
+
lines.push(formatField("booktitle", item["container-title"]));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (item.volume) {
|
|
1023
|
+
lines.push(formatField("volume", item.volume));
|
|
1024
|
+
}
|
|
1025
|
+
if (item.issue) {
|
|
1026
|
+
lines.push(formatField("number", item.issue));
|
|
1027
|
+
}
|
|
1028
|
+
if (item.page) {
|
|
1029
|
+
lines.push(formatField("pages", item.page));
|
|
1030
|
+
}
|
|
1031
|
+
if (item.publisher) {
|
|
1032
|
+
lines.push(formatField("publisher", item.publisher));
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function addIdentifierFields(lines, item) {
|
|
1036
|
+
if (item.DOI) {
|
|
1037
|
+
lines.push(formatField("doi", item.DOI));
|
|
1038
|
+
}
|
|
1039
|
+
if (item.URL) {
|
|
1040
|
+
lines.push(formatField("url", item.URL));
|
|
1041
|
+
}
|
|
1042
|
+
if (item.PMID) {
|
|
1043
|
+
lines.push(formatField("note", `PMID: ${item.PMID}`));
|
|
1044
|
+
} else if (item.PMCID) {
|
|
1045
|
+
lines.push(formatField("note", `PMCID: ${item.PMCID}`));
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function formatSingleBibtexEntry(item) {
|
|
1049
|
+
const entryType = mapEntryType(item.type);
|
|
1050
|
+
const lines = [];
|
|
1051
|
+
lines.push(`@${entryType}{${item.id},`);
|
|
1052
|
+
addBasicFields(lines, item);
|
|
1053
|
+
addPublicationDetails(lines, item, entryType);
|
|
1054
|
+
addIdentifierFields(lines, item);
|
|
1055
|
+
lines.push("}");
|
|
1056
|
+
return lines.join("\n");
|
|
1057
|
+
}
|
|
1058
|
+
function formatBibtex(items) {
|
|
1059
|
+
if (items.length === 0) {
|
|
1060
|
+
return "";
|
|
1061
|
+
}
|
|
1062
|
+
return items.map(formatSingleBibtexEntry).join("\n\n");
|
|
1063
|
+
}
|
|
1064
|
+
function formatFirstAuthor(item) {
|
|
1065
|
+
if (!item.author || item.author.length === 0) {
|
|
1066
|
+
return "Unknown";
|
|
1067
|
+
}
|
|
1068
|
+
const firstAuthor = item.author[0];
|
|
1069
|
+
if (!firstAuthor) {
|
|
1070
|
+
return "Unknown";
|
|
1071
|
+
}
|
|
1072
|
+
const family = firstAuthor.family || "Unknown";
|
|
1073
|
+
const givenInitial = firstAuthor.given ? firstAuthor.given[0] : "";
|
|
1074
|
+
if (givenInitial) {
|
|
1075
|
+
return `${family} ${givenInitial}`;
|
|
1076
|
+
}
|
|
1077
|
+
return family;
|
|
1078
|
+
}
|
|
1079
|
+
function hasMultipleAuthors(item) {
|
|
1080
|
+
return (item.author?.length || 0) > 1;
|
|
1081
|
+
}
|
|
1082
|
+
function extractYear(item) {
|
|
1083
|
+
if (item.issued?.["date-parts"]?.[0]?.[0]) {
|
|
1084
|
+
return String(item.issued["date-parts"][0][0]);
|
|
1085
|
+
}
|
|
1086
|
+
return "n.d.";
|
|
1087
|
+
}
|
|
1088
|
+
function getJournalAbbrev(item) {
|
|
1089
|
+
const containerTitleShort = item["container-title-short"];
|
|
1090
|
+
if (typeof containerTitleShort === "string") {
|
|
1091
|
+
return containerTitleShort;
|
|
1092
|
+
}
|
|
1093
|
+
return item["container-title"] || "";
|
|
1094
|
+
}
|
|
1095
|
+
function formatVolumeIssuePage(item) {
|
|
1096
|
+
const volume = item.volume;
|
|
1097
|
+
const issue = item.issue;
|
|
1098
|
+
const page = item.page;
|
|
1099
|
+
if (!volume && !issue && !page) {
|
|
1100
|
+
return "";
|
|
1101
|
+
}
|
|
1102
|
+
let result = "";
|
|
1103
|
+
if (volume) {
|
|
1104
|
+
result += volume;
|
|
1105
|
+
if (issue) {
|
|
1106
|
+
result += `(${issue})`;
|
|
1107
|
+
}
|
|
1108
|
+
if (page) {
|
|
1109
|
+
result += `:${page}`;
|
|
1110
|
+
}
|
|
1111
|
+
} else if (page) {
|
|
1112
|
+
result += page;
|
|
1113
|
+
}
|
|
1114
|
+
return result;
|
|
1115
|
+
}
|
|
1116
|
+
function getIdentifier(item) {
|
|
1117
|
+
if (item.PMID) {
|
|
1118
|
+
return `PMID:${item.PMID}`;
|
|
1119
|
+
}
|
|
1120
|
+
if (item.DOI) {
|
|
1121
|
+
return `DOI:${item.DOI}`;
|
|
1122
|
+
}
|
|
1123
|
+
if (item.URL) {
|
|
1124
|
+
return item.URL;
|
|
1125
|
+
}
|
|
1126
|
+
return "";
|
|
1127
|
+
}
|
|
1128
|
+
function formatBibliographyEntry(item) {
|
|
1129
|
+
const parts = [];
|
|
1130
|
+
const author = formatFirstAuthor(item);
|
|
1131
|
+
const etAl = hasMultipleAuthors(item) ? " et al" : "";
|
|
1132
|
+
parts.push(`${author}${etAl}.`);
|
|
1133
|
+
const journal = getJournalAbbrev(item);
|
|
1134
|
+
if (journal) {
|
|
1135
|
+
parts.push(`${journal}.`);
|
|
1136
|
+
}
|
|
1137
|
+
const year = extractYear(item);
|
|
1138
|
+
const volumeIssuePage = formatVolumeIssuePage(item);
|
|
1139
|
+
if (volumeIssuePage) {
|
|
1140
|
+
parts.push(`${year};${volumeIssuePage}.`);
|
|
1141
|
+
} else {
|
|
1142
|
+
parts.push(`${year}.`);
|
|
1143
|
+
}
|
|
1144
|
+
const identifier = getIdentifier(item);
|
|
1145
|
+
if (identifier) {
|
|
1146
|
+
parts.push(`${identifier}.`);
|
|
1147
|
+
}
|
|
1148
|
+
if (item.title) {
|
|
1149
|
+
parts.push(`${item.title}.`);
|
|
1150
|
+
}
|
|
1151
|
+
return parts.join(" ");
|
|
1152
|
+
}
|
|
1153
|
+
function getFirstAuthorFamilyName(item) {
|
|
1154
|
+
if (!item.author || item.author.length === 0) {
|
|
1155
|
+
return "Unknown";
|
|
1156
|
+
}
|
|
1157
|
+
const firstAuthor = item.author[0];
|
|
1158
|
+
if (!firstAuthor) {
|
|
1159
|
+
return "Unknown";
|
|
1160
|
+
}
|
|
1161
|
+
return firstAuthor.family || "Unknown";
|
|
1162
|
+
}
|
|
1163
|
+
function formatInTextEntry(item) {
|
|
1164
|
+
const author = getFirstAuthorFamilyName(item);
|
|
1165
|
+
const etAl = hasMultipleAuthors(item) ? " et al" : "";
|
|
1166
|
+
const year = extractYear(item);
|
|
1167
|
+
return `${author}${etAl}, ${year}`;
|
|
1168
|
+
}
|
|
1169
|
+
function formatBibliography(items) {
|
|
1170
|
+
if (items.length === 0) {
|
|
1171
|
+
return "";
|
|
1172
|
+
}
|
|
1173
|
+
return items.map(formatBibliographyEntry).join("\n\n");
|
|
1174
|
+
}
|
|
1175
|
+
function formatInText(items) {
|
|
1176
|
+
if (items.length === 0) {
|
|
1177
|
+
return "";
|
|
1178
|
+
}
|
|
1179
|
+
const citations = items.map(formatInTextEntry).join("; ");
|
|
1180
|
+
return `(${citations})`;
|
|
1181
|
+
}
|
|
1182
|
+
function formatBibliographyCSL(items, options) {
|
|
1183
|
+
if (items.length === 0) {
|
|
1184
|
+
return "";
|
|
1185
|
+
}
|
|
1186
|
+
const style = options.style || "apa";
|
|
1187
|
+
const format = options.format || "text";
|
|
1188
|
+
const locale = options.locale || "en-US";
|
|
1189
|
+
try {
|
|
1190
|
+
const cite2 = new Cite(items);
|
|
1191
|
+
const result = cite2.format("bibliography", {
|
|
1192
|
+
format,
|
|
1193
|
+
template: style,
|
|
1194
|
+
lang: locale
|
|
1195
|
+
});
|
|
1196
|
+
return result;
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1199
|
+
process.stderr.write(
|
|
1200
|
+
`Warning: CSL processing failed (style: ${style}), falling back to simplified format: ${errorMessage}
|
|
1201
|
+
`
|
|
1202
|
+
);
|
|
1203
|
+
return formatBibliography(items);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function formatInTextCSL(items, options) {
|
|
1207
|
+
if (items.length === 0) {
|
|
1208
|
+
return "";
|
|
1209
|
+
}
|
|
1210
|
+
const style = options.style || "apa";
|
|
1211
|
+
const format = options.format || "text";
|
|
1212
|
+
const locale = options.locale || "en-US";
|
|
1213
|
+
try {
|
|
1214
|
+
const cite2 = new Cite(items);
|
|
1215
|
+
const result = cite2.format("citation", {
|
|
1216
|
+
format,
|
|
1217
|
+
template: style,
|
|
1218
|
+
lang: locale
|
|
1219
|
+
});
|
|
1220
|
+
return result;
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1223
|
+
process.stderr.write(
|
|
1224
|
+
`Warning: CSL processing failed (style: ${style}), falling back to simplified format: ${errorMessage}
|
|
1225
|
+
`
|
|
1226
|
+
);
|
|
1227
|
+
return formatInText(items);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function shouldUseFallback(options) {
|
|
1231
|
+
if (options.cslFile) {
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
if (options.style && !isBuiltinStyle(options.style)) {
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
function formatCitation(item, inText, options) {
|
|
1240
|
+
const useFallback = shouldUseFallback(options);
|
|
1241
|
+
const style = options.cslFile ?? options.style ?? "apa";
|
|
1242
|
+
const locale = options.locale ?? "en-US";
|
|
1243
|
+
const format = options.format ?? "text";
|
|
1244
|
+
if (useFallback) {
|
|
1245
|
+
return inText ? formatInText([item]) : formatBibliography([item]);
|
|
1246
|
+
}
|
|
1247
|
+
return inText ? formatInTextCSL([item], { style, locale, format }) : formatBibliographyCSL([item], { style, locale, format });
|
|
1248
|
+
}
|
|
1249
|
+
async function generateCitationForIdentifier(library, identifier, idType, inText, options) {
|
|
1250
|
+
const item = await library.find(identifier, { idType });
|
|
1251
|
+
if (!item) {
|
|
1252
|
+
const lookupType = idType === "uuid" ? "UUID" : idType.toUpperCase();
|
|
1253
|
+
return {
|
|
1254
|
+
success: false,
|
|
1255
|
+
identifier,
|
|
1256
|
+
error: `Reference with ${lookupType} '${identifier}' not found`
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
const citation = formatCitation(item, inText, options);
|
|
1260
|
+
return {
|
|
1261
|
+
success: true,
|
|
1262
|
+
identifier,
|
|
1263
|
+
citation
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async function citeReferences(library, options) {
|
|
1267
|
+
const { identifiers, idType = "id", inText = false, style, cslFile, locale, format } = options;
|
|
1268
|
+
const results = [];
|
|
1269
|
+
for (const identifier of identifiers) {
|
|
1270
|
+
const result = await generateCitationForIdentifier(library, identifier, idType, inText, {
|
|
1271
|
+
style,
|
|
1272
|
+
cslFile,
|
|
1273
|
+
locale,
|
|
1274
|
+
format
|
|
1275
|
+
});
|
|
1276
|
+
results.push(result);
|
|
1277
|
+
}
|
|
1278
|
+
return { results };
|
|
1279
|
+
}
|
|
1280
|
+
const cite = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1281
|
+
__proto__: null,
|
|
1282
|
+
citeReferences
|
|
1283
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
1284
|
+
async function listReferences(library, options) {
|
|
1285
|
+
const format = options.format ?? "pretty";
|
|
1286
|
+
const items = await library.getAll();
|
|
1287
|
+
switch (format) {
|
|
1288
|
+
case "json":
|
|
1289
|
+
return { items: items.map((item) => JSON.stringify(item)) };
|
|
1290
|
+
case "bibtex":
|
|
1291
|
+
return { items: items.map((item) => formatBibtex([item])) };
|
|
1292
|
+
case "ids-only":
|
|
1293
|
+
return { items: items.map((item) => item.id) };
|
|
1294
|
+
case "uuid":
|
|
1295
|
+
return {
|
|
1296
|
+
items: items.filter(
|
|
1297
|
+
(item) => Boolean(item.custom?.uuid)
|
|
1298
|
+
).map((item) => item.custom.uuid)
|
|
1299
|
+
};
|
|
1300
|
+
default:
|
|
1301
|
+
return { items: items.map((item) => formatPretty([item])) };
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
const list = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1305
|
+
__proto__: null,
|
|
1306
|
+
listReferences
|
|
1307
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
1308
|
+
async function removeReference(library, options) {
|
|
1309
|
+
const { identifier, idType = "id" } = options;
|
|
1310
|
+
const result = await library.remove(identifier, { idType });
|
|
1311
|
+
if (result.removed) {
|
|
1312
|
+
await library.save();
|
|
1313
|
+
}
|
|
1314
|
+
return result;
|
|
1315
|
+
}
|
|
1316
|
+
async function searchReferences(library, options) {
|
|
1317
|
+
const format = options.format ?? "pretty";
|
|
1318
|
+
const query = options.query;
|
|
1319
|
+
const allItems = await library.getAll();
|
|
1320
|
+
let matchedItems;
|
|
1321
|
+
if (!query.trim()) {
|
|
1322
|
+
matchedItems = allItems;
|
|
1323
|
+
} else {
|
|
1324
|
+
const tokens = tokenize(query).tokens;
|
|
1325
|
+
const results = search$1(allItems, tokens);
|
|
1326
|
+
const sorted = sortResults(results);
|
|
1327
|
+
matchedItems = sorted.map((result) => result.reference);
|
|
1328
|
+
}
|
|
1329
|
+
switch (format) {
|
|
1330
|
+
case "json":
|
|
1331
|
+
return { items: matchedItems.map((item) => JSON.stringify(item)) };
|
|
1332
|
+
case "bibtex":
|
|
1333
|
+
return { items: matchedItems.map((item) => formatBibtex([item])) };
|
|
1334
|
+
case "ids-only":
|
|
1335
|
+
return { items: matchedItems.map((item) => item.id) };
|
|
1336
|
+
case "uuid":
|
|
1337
|
+
return {
|
|
1338
|
+
items: matchedItems.filter(
|
|
1339
|
+
(item) => Boolean(item.custom?.uuid)
|
|
1340
|
+
).map((item) => item.custom.uuid)
|
|
1341
|
+
};
|
|
1342
|
+
default:
|
|
1343
|
+
return { items: matchedItems.map((item) => formatPretty([item])) };
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
const search = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1347
|
+
__proto__: null,
|
|
1348
|
+
searchReferences
|
|
1349
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
1350
|
+
function buildPubmedConfig(config) {
|
|
1351
|
+
const pubmedConfig = {};
|
|
1352
|
+
if (config.pubmed.email !== void 0) {
|
|
1353
|
+
pubmedConfig.email = config.pubmed.email;
|
|
1354
|
+
}
|
|
1355
|
+
if (config.pubmed.apiKey !== void 0) {
|
|
1356
|
+
pubmedConfig.apiKey = config.pubmed.apiKey;
|
|
1357
|
+
}
|
|
1358
|
+
return pubmedConfig;
|
|
1359
|
+
}
|
|
1360
|
+
function createAddRoute(library, config) {
|
|
1361
|
+
const route = new Hono();
|
|
1362
|
+
route.post("/", async (c) => {
|
|
1363
|
+
let body;
|
|
1364
|
+
try {
|
|
1365
|
+
body = await c.req.json();
|
|
1366
|
+
} catch {
|
|
1367
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
1368
|
+
}
|
|
1369
|
+
if (!body || typeof body !== "object") {
|
|
1370
|
+
return c.json({ error: "Request body must be an object" }, 400);
|
|
1371
|
+
}
|
|
1372
|
+
const { inputs, options } = body;
|
|
1373
|
+
if (!inputs || !Array.isArray(inputs) || inputs.length === 0) {
|
|
1374
|
+
return c.json({ error: "inputs must be a non-empty array of strings" }, 400);
|
|
1375
|
+
}
|
|
1376
|
+
if (!inputs.every((input) => typeof input === "string")) {
|
|
1377
|
+
return c.json({ error: "All inputs must be strings" }, 400);
|
|
1378
|
+
}
|
|
1379
|
+
const addOptions = {
|
|
1380
|
+
force: options?.force ?? false,
|
|
1381
|
+
pubmedConfig: buildPubmedConfig(config)
|
|
1382
|
+
};
|
|
1383
|
+
if (options?.format) {
|
|
1384
|
+
addOptions.format = options.format;
|
|
1385
|
+
}
|
|
1386
|
+
const result = await addReferences(inputs, library, addOptions);
|
|
1387
|
+
return c.json(result);
|
|
1388
|
+
});
|
|
1389
|
+
return route;
|
|
1390
|
+
}
|
|
1391
|
+
const CiteRequestSchema = z.object({
|
|
1392
|
+
identifiers: z.array(z.string()).min(1, "identifiers must be a non-empty array"),
|
|
1393
|
+
idType: z.enum(["id", "uuid", "doi", "pmid", "isbn"]).optional(),
|
|
1394
|
+
inText: z.boolean().optional(),
|
|
1395
|
+
style: z.string().optional(),
|
|
1396
|
+
cslFile: z.string().optional(),
|
|
1397
|
+
locale: z.string().optional(),
|
|
1398
|
+
format: z.enum(["text", "html"]).optional()
|
|
1399
|
+
});
|
|
1400
|
+
function buildCiteOptions(body) {
|
|
1401
|
+
return {
|
|
1402
|
+
identifiers: body.identifiers,
|
|
1403
|
+
...body.idType !== void 0 && { idType: body.idType },
|
|
1404
|
+
...body.inText !== void 0 && { inText: body.inText },
|
|
1405
|
+
...body.style !== void 0 && { style: body.style },
|
|
1406
|
+
...body.cslFile !== void 0 && { cslFile: body.cslFile },
|
|
1407
|
+
...body.locale !== void 0 && { locale: body.locale },
|
|
1408
|
+
...body.format !== void 0 && { format: body.format }
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
function createCiteRoute(library) {
|
|
1412
|
+
const route = new Hono();
|
|
1413
|
+
route.post("/", async (c) => {
|
|
1414
|
+
let rawBody;
|
|
1415
|
+
try {
|
|
1416
|
+
rawBody = await c.req.json();
|
|
1417
|
+
} catch {
|
|
1418
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
1419
|
+
}
|
|
1420
|
+
const parseResult = CiteRequestSchema.safeParse(rawBody);
|
|
1421
|
+
if (!parseResult.success) {
|
|
1422
|
+
const errorMessage = parseResult.error.issues[0]?.message ?? "Invalid request body";
|
|
1423
|
+
return c.json({ error: errorMessage }, 400);
|
|
1424
|
+
}
|
|
1425
|
+
const result = await citeReferences(library, buildCiteOptions(parseResult.data));
|
|
1426
|
+
return c.json(result);
|
|
1427
|
+
});
|
|
1428
|
+
return route;
|
|
1429
|
+
}
|
|
1430
|
+
const healthRoute = new Hono();
|
|
1431
|
+
healthRoute.get("/", (c) => {
|
|
1432
|
+
return c.json({ status: "ok" });
|
|
1433
|
+
});
|
|
1434
|
+
const listRequestBodySchema = z.object({
|
|
1435
|
+
format: z.enum(["pretty", "json", "bibtex", "ids-only", "uuid"]).optional()
|
|
1436
|
+
});
|
|
1437
|
+
function createListRoute(library) {
|
|
1438
|
+
const route = new Hono();
|
|
1439
|
+
route.post("/", async (c) => {
|
|
1440
|
+
let body;
|
|
1441
|
+
try {
|
|
1442
|
+
body = await c.req.json();
|
|
1443
|
+
} catch {
|
|
1444
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
1445
|
+
}
|
|
1446
|
+
const parseResult = listRequestBodySchema.safeParse(body);
|
|
1447
|
+
if (!parseResult.success) {
|
|
1448
|
+
return c.json({ error: "Request body must be an object" }, 400);
|
|
1449
|
+
}
|
|
1450
|
+
const requestBody = parseResult.data;
|
|
1451
|
+
const options = {};
|
|
1452
|
+
if (requestBody.format !== void 0) {
|
|
1453
|
+
options.format = requestBody.format;
|
|
1454
|
+
}
|
|
1455
|
+
const result = await listReferences(library, options);
|
|
1456
|
+
return c.json(result);
|
|
1457
|
+
});
|
|
1458
|
+
return route;
|
|
1459
|
+
}
|
|
1460
|
+
function createReferencesRoute(library) {
|
|
1461
|
+
const route = new Hono();
|
|
1462
|
+
route.get("/", async (c) => {
|
|
1463
|
+
const items = await library.getAll();
|
|
1464
|
+
return c.json(items);
|
|
1465
|
+
});
|
|
1466
|
+
route.get("/uuid/:uuid", async (c) => {
|
|
1467
|
+
const uuid = c.req.param("uuid");
|
|
1468
|
+
const item = await library.find(uuid, { idType: "uuid" });
|
|
1469
|
+
if (!item) {
|
|
1470
|
+
return c.json({ error: "Reference not found" }, 404);
|
|
1471
|
+
}
|
|
1472
|
+
return c.json(item);
|
|
1473
|
+
});
|
|
1474
|
+
route.get("/id/:id", async (c) => {
|
|
1475
|
+
const id = c.req.param("id");
|
|
1476
|
+
const item = await library.find(id);
|
|
1477
|
+
if (!item) {
|
|
1478
|
+
return c.json({ error: "Reference not found" }, 404);
|
|
1479
|
+
}
|
|
1480
|
+
return c.json(item);
|
|
1481
|
+
});
|
|
1482
|
+
route.post("/", async (c) => {
|
|
1483
|
+
try {
|
|
1484
|
+
const body = await c.req.json();
|
|
1485
|
+
const addedItem = await library.add(body);
|
|
1486
|
+
return c.json(addedItem, 201);
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
return c.json(
|
|
1489
|
+
{
|
|
1490
|
+
error: "Invalid request body",
|
|
1491
|
+
details: error instanceof Error ? error.message : String(error)
|
|
1492
|
+
},
|
|
1493
|
+
400
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
route.put("/uuid/:uuid", async (c) => {
|
|
1498
|
+
const uuid = c.req.param("uuid");
|
|
1499
|
+
let body;
|
|
1500
|
+
try {
|
|
1501
|
+
body = await c.req.json();
|
|
1502
|
+
} catch {
|
|
1503
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
1504
|
+
}
|
|
1505
|
+
if (!body || typeof body !== "object") {
|
|
1506
|
+
return c.json({ error: "Request body must be an object" }, 400);
|
|
1507
|
+
}
|
|
1508
|
+
const { updates, onIdCollision } = body;
|
|
1509
|
+
if (!updates || typeof updates !== "object") {
|
|
1510
|
+
return c.json({ error: "Request body must contain 'updates' object" }, 400);
|
|
1511
|
+
}
|
|
1512
|
+
const result = await updateReference(library, {
|
|
1513
|
+
identifier: uuid,
|
|
1514
|
+
idType: "uuid",
|
|
1515
|
+
updates,
|
|
1516
|
+
onIdCollision: onIdCollision ?? "suffix"
|
|
1517
|
+
});
|
|
1518
|
+
if (!result.updated) {
|
|
1519
|
+
const status = result.idCollision ? 409 : 404;
|
|
1520
|
+
return c.json(result, status);
|
|
1521
|
+
}
|
|
1522
|
+
return c.json(result);
|
|
1523
|
+
});
|
|
1524
|
+
route.put("/id/:id", async (c) => {
|
|
1525
|
+
const id = c.req.param("id");
|
|
1526
|
+
let body;
|
|
1527
|
+
try {
|
|
1528
|
+
body = await c.req.json();
|
|
1529
|
+
} catch {
|
|
1530
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
1531
|
+
}
|
|
1532
|
+
if (!body || typeof body !== "object") {
|
|
1533
|
+
return c.json({ error: "Request body must be an object" }, 400);
|
|
1534
|
+
}
|
|
1535
|
+
const { updates, onIdCollision } = body;
|
|
1536
|
+
if (!updates || typeof updates !== "object") {
|
|
1537
|
+
return c.json({ error: "Request body must contain 'updates' object" }, 400);
|
|
1538
|
+
}
|
|
1539
|
+
const result = await updateReference(library, {
|
|
1540
|
+
identifier: id,
|
|
1541
|
+
updates,
|
|
1542
|
+
onIdCollision: onIdCollision ?? "suffix"
|
|
1543
|
+
});
|
|
1544
|
+
if (!result.updated) {
|
|
1545
|
+
const status = result.idCollision ? 409 : 404;
|
|
1546
|
+
return c.json(result, status);
|
|
1547
|
+
}
|
|
1548
|
+
return c.json(result);
|
|
1549
|
+
});
|
|
1550
|
+
route.delete("/uuid/:uuid", async (c) => {
|
|
1551
|
+
const uuid = c.req.param("uuid");
|
|
1552
|
+
const result = await removeReference(library, {
|
|
1553
|
+
identifier: uuid,
|
|
1554
|
+
idType: "uuid"
|
|
1555
|
+
});
|
|
1556
|
+
if (!result.removed) {
|
|
1557
|
+
return c.json(result, 404);
|
|
1558
|
+
}
|
|
1559
|
+
return c.json(result);
|
|
1560
|
+
});
|
|
1561
|
+
route.delete("/id/:id", async (c) => {
|
|
1562
|
+
const id = c.req.param("id");
|
|
1563
|
+
const result = await removeReference(library, {
|
|
1564
|
+
identifier: id
|
|
1565
|
+
});
|
|
1566
|
+
if (!result.removed) {
|
|
1567
|
+
return c.json(result, 404);
|
|
1568
|
+
}
|
|
1569
|
+
return c.json(result);
|
|
1570
|
+
});
|
|
1571
|
+
return route;
|
|
1572
|
+
}
|
|
1573
|
+
const searchRequestBodySchema = z.object({
|
|
1574
|
+
query: z.string(),
|
|
1575
|
+
format: z.enum(["pretty", "json", "bibtex", "ids-only", "uuid"]).optional()
|
|
1576
|
+
});
|
|
1577
|
+
function createSearchRoute(library) {
|
|
1578
|
+
const route = new Hono();
|
|
1579
|
+
route.post("/", async (c) => {
|
|
1580
|
+
let body;
|
|
1581
|
+
try {
|
|
1582
|
+
body = await c.req.json();
|
|
1583
|
+
} catch {
|
|
1584
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
1585
|
+
}
|
|
1586
|
+
const parseResult = searchRequestBodySchema.safeParse(body);
|
|
1587
|
+
if (!parseResult.success) {
|
|
1588
|
+
return c.json({ error: "Invalid request body" }, 400);
|
|
1589
|
+
}
|
|
1590
|
+
const requestBody = parseResult.data;
|
|
1591
|
+
const options = {
|
|
1592
|
+
query: requestBody.query
|
|
1593
|
+
};
|
|
1594
|
+
if (requestBody.format !== void 0) {
|
|
1595
|
+
options.format = requestBody.format;
|
|
1596
|
+
}
|
|
1597
|
+
const result = await searchReferences(library, options);
|
|
1598
|
+
return c.json(result);
|
|
1599
|
+
});
|
|
1600
|
+
return route;
|
|
1601
|
+
}
|
|
1602
|
+
function createServer(library, config) {
|
|
1603
|
+
const app = new Hono();
|
|
1604
|
+
app.route("/health", healthRoute);
|
|
1605
|
+
const referencesRoute = createReferencesRoute(library);
|
|
1606
|
+
app.route("/api/references", referencesRoute);
|
|
1607
|
+
const addRoute = createAddRoute(library, config);
|
|
1608
|
+
app.route("/api/add", addRoute);
|
|
1609
|
+
const citeRoute = createCiteRoute(library);
|
|
1610
|
+
app.route("/api/cite", citeRoute);
|
|
1611
|
+
const listRoute = createListRoute(library);
|
|
1612
|
+
app.route("/api/list", listRoute);
|
|
1613
|
+
const searchRoute = createSearchRoute(library);
|
|
1614
|
+
app.route("/api/search", searchRoute);
|
|
1615
|
+
return app;
|
|
1616
|
+
}
|
|
1617
|
+
async function startServerWithFileWatcher(libraryPath, config) {
|
|
1618
|
+
const library = await Library.load(libraryPath);
|
|
1619
|
+
const app = createServer(library, config);
|
|
1620
|
+
const fileWatcher = new FileWatcher(libraryPath, {
|
|
1621
|
+
debounceMs: config.watch.debounceMs,
|
|
1622
|
+
maxRetries: config.watch.maxRetries,
|
|
1623
|
+
retryDelayMs: config.watch.retryIntervalMs,
|
|
1624
|
+
pollIntervalMs: config.watch.pollIntervalMs
|
|
1625
|
+
});
|
|
1626
|
+
fileWatcher.on("change", () => {
|
|
1627
|
+
library.reload().catch((error) => {
|
|
1628
|
+
console.error("Failed to reload library:", error);
|
|
1629
|
+
});
|
|
1630
|
+
});
|
|
1631
|
+
await fileWatcher.start();
|
|
1632
|
+
const dispose = async () => {
|
|
1633
|
+
fileWatcher.close();
|
|
1634
|
+
};
|
|
1635
|
+
return {
|
|
1636
|
+
app,
|
|
1637
|
+
library,
|
|
1638
|
+
fileWatcher,
|
|
1639
|
+
dispose
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
export {
|
|
1643
|
+
BUILTIN_STYLES as B,
|
|
1644
|
+
addReferences as a,
|
|
1645
|
+
startServerWithFileWatcher as b,
|
|
1646
|
+
citeReferences as c,
|
|
1647
|
+
createServer as d,
|
|
1648
|
+
add as e,
|
|
1649
|
+
cite as f,
|
|
1650
|
+
list as g,
|
|
1651
|
+
search as h,
|
|
1652
|
+
listReferences as l,
|
|
1653
|
+
removeReference as r,
|
|
1654
|
+
searchReferences as s,
|
|
1655
|
+
updateReference as u
|
|
1656
|
+
};
|
|
1657
|
+
//# sourceMappingURL=index-Bl_mOQRe.js.map
|