@ncukondo/reference-manager 0.20.0 → 0.22.0

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.
Files changed (57) hide show
  1. package/dist/chunks/{action-menu-Brg5Lmz7.js → action-menu-B1DCdkkp.js} +4 -3
  2. package/dist/chunks/{action-menu-Brg5Lmz7.js.map → action-menu-B1DCdkkp.js.map} +1 -1
  3. package/dist/chunks/{format-CYO99JV-.js → format-CfTZqhUx.js} +2 -2
  4. package/dist/chunks/{format-CYO99JV-.js.map → format-CfTZqhUx.js.map} +1 -1
  5. package/dist/chunks/index-BK3hTiKR.js +5929 -0
  6. package/dist/chunks/index-BK3hTiKR.js.map +1 -0
  7. package/dist/chunks/{index-D--7n1SB.js → index-BvIfOciH.js} +8 -5
  8. package/dist/chunks/index-BvIfOciH.js.map +1 -0
  9. package/dist/chunks/{index-BR5tlrNU.js → index-D7eiOplw.js} +3 -2
  10. package/dist/chunks/index-D7eiOplw.js.map +1 -0
  11. package/dist/chunks/{index-Cno7_aWr.js → index-DOvEusHb.js} +447 -426
  12. package/dist/chunks/index-DOvEusHb.js.map +1 -0
  13. package/dist/chunks/{loader-BtW20O32.js → loader-BHvcats5.js} +113 -27
  14. package/dist/chunks/loader-BHvcats5.js.map +1 -0
  15. package/dist/chunks/{reference-select-DrINWBuP.js → reference-select-DTqhevya.js} +3 -3
  16. package/dist/chunks/{reference-select-DrINWBuP.js.map → reference-select-DTqhevya.js.map} +1 -1
  17. package/dist/chunks/{style-select-DxcSWBSF.js → style-select-CXclvgJO.js} +3 -3
  18. package/dist/chunks/{style-select-DxcSWBSF.js.map → style-select-CXclvgJO.js.map} +1 -1
  19. package/dist/cli/commands/add.d.ts +20 -0
  20. package/dist/cli/commands/add.d.ts.map +1 -1
  21. package/dist/cli/commands/fulltext.d.ts +86 -3
  22. package/dist/cli/commands/fulltext.d.ts.map +1 -1
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli.js +1 -1
  25. package/dist/config/defaults.d.ts.map +1 -1
  26. package/dist/config/env-override.d.ts.map +1 -1
  27. package/dist/config/key-parser.d.ts.map +1 -1
  28. package/dist/config/loader.d.ts.map +1 -1
  29. package/dist/config/schema.d.ts +65 -0
  30. package/dist/config/schema.d.ts.map +1 -1
  31. package/dist/features/format/pretty.d.ts.map +1 -1
  32. package/dist/features/format/resource-indicators.d.ts +8 -0
  33. package/dist/features/format/resource-indicators.d.ts.map +1 -0
  34. package/dist/features/interactive/apps/runSearchFlow.d.ts +5 -0
  35. package/dist/features/interactive/apps/runSearchFlow.d.ts.map +1 -1
  36. package/dist/features/operations/fulltext/convert.d.ts +22 -0
  37. package/dist/features/operations/fulltext/convert.d.ts.map +1 -0
  38. package/dist/features/operations/fulltext/discover.d.ts +35 -0
  39. package/dist/features/operations/fulltext/discover.d.ts.map +1 -0
  40. package/dist/features/operations/fulltext/fetch.d.ts +34 -0
  41. package/dist/features/operations/fulltext/fetch.d.ts.map +1 -0
  42. package/dist/features/operations/fulltext/index.d.ts +3 -0
  43. package/dist/features/operations/fulltext/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -1
  45. package/dist/mcp/tools/fulltext.d.ts +37 -0
  46. package/dist/mcp/tools/fulltext.d.ts.map +1 -1
  47. package/dist/mcp/tools/index.d.ts.map +1 -1
  48. package/dist/server/routes/references.d.ts +3 -1
  49. package/dist/server/routes/references.d.ts.map +1 -1
  50. package/dist/server.js +2 -2
  51. package/package.json +2 -1
  52. package/dist/chunks/index-BR5tlrNU.js.map +0 -1
  53. package/dist/chunks/index-Cno7_aWr.js.map +0 -1
  54. package/dist/chunks/index-D--7n1SB.js.map +0 -1
  55. package/dist/chunks/index-DrZawbND.js +0 -2082
  56. package/dist/chunks/index-DrZawbND.js.map +0 -1
  57. package/dist/chunks/loader-BtW20O32.js.map +0 -1
@@ -1,2082 +0,0 @@
1
- import { Hono } from "hono";
2
- import { g as CslItemSchema, f as detectDuplicate, k as generateId, a as sortOrderSchema, b as sortFieldSchema, p as pickDefined, t as tokenize, s as search$1, e as sortResults, v as searchSortFieldSchema, L as Library, F as FileWatcher } from "./file-watcher-CrsNHUpz.js";
3
- import * as fs from "node:fs";
4
- import { existsSync, readFileSync } from "node:fs";
5
- import { Cite, plugins } from "@citation-js/core";
6
- import "@citation-js/plugin-doi";
7
- import "@citation-js/plugin-isbn";
8
- import "@citation-js/plugin-bibtex";
9
- import "@citation-js/plugin-ris";
10
- import { z } from "zod";
11
- import * as path from "node:path";
12
- import path__default, { join } from "node:path";
13
- import "@citation-js/plugin-csl";
14
- import { unlink, rmdir } from "node:fs/promises";
15
- const RESERVED_ROLES = ["fulltext", "supplement", "notes", "draft"];
16
- function isReservedRole(role) {
17
- return RESERVED_ROLES.includes(role);
18
- }
19
- function getExtension(filename) {
20
- const ext = path__default.extname(filename);
21
- return ext.startsWith(".") ? ext.slice(1).toLowerCase() : ext.toLowerCase();
22
- }
23
- function isValidFulltextFiles(files) {
24
- const fulltextFiles = files.filter((f) => f.role === "fulltext");
25
- if (fulltextFiles.length > 2) {
26
- return false;
27
- }
28
- if (fulltextFiles.length <= 1) {
29
- return true;
30
- }
31
- const extensions = fulltextFiles.map((f) => getExtension(f.filename));
32
- const pdfCount = extensions.filter((ext) => ext === "pdf").length;
33
- const mdCount = extensions.filter((ext) => ext === "md").length;
34
- return pdfCount === 1 && mdCount === 1;
35
- }
36
- const FULLTEXT_ROLE = "fulltext";
37
- function formatToExtension(format) {
38
- return format === "markdown" ? "md" : format;
39
- }
40
- function extensionToFormat(ext) {
41
- const normalized = ext.toLowerCase();
42
- if (normalized === "pdf") return "pdf";
43
- if (normalized === "md" || normalized === "markdown") return "markdown";
44
- return void 0;
45
- }
46
- function findFulltextFile(attachments, format) {
47
- if (!attachments?.files) {
48
- return void 0;
49
- }
50
- const targetExt = formatToExtension(format);
51
- return attachments.files.find((file) => {
52
- if (file.role !== FULLTEXT_ROLE) {
53
- return false;
54
- }
55
- const fileExt = getExtension(file.filename);
56
- return fileExt === targetExt;
57
- });
58
- }
59
- function findFulltextFiles(attachments) {
60
- if (!attachments?.files) {
61
- return [];
62
- }
63
- return attachments.files.filter((file) => file.role === FULLTEXT_ROLE);
64
- }
65
- function formatFirstAuthor(item) {
66
- if (!item.author || item.author.length === 0) {
67
- return "Unknown";
68
- }
69
- const firstAuthor = item.author[0];
70
- if (!firstAuthor) {
71
- return "Unknown";
72
- }
73
- const family = firstAuthor.family || "Unknown";
74
- const givenInitial = firstAuthor.given ? firstAuthor.given[0] : "";
75
- if (givenInitial) {
76
- return `${family} ${givenInitial}`;
77
- }
78
- return family;
79
- }
80
- function hasMultipleAuthors(item) {
81
- return (item.author?.length || 0) > 1;
82
- }
83
- function extractYear(item) {
84
- if (item.issued?.["date-parts"]?.[0]?.[0]) {
85
- return String(item.issued["date-parts"][0][0]);
86
- }
87
- return "n.d.";
88
- }
89
- function getJournalAbbrev(item) {
90
- const containerTitleShort = item["container-title-short"];
91
- if (typeof containerTitleShort === "string") {
92
- return containerTitleShort;
93
- }
94
- return item["container-title"] || "";
95
- }
96
- function formatVolumeIssuePage(item) {
97
- const volume = item.volume;
98
- const issue = item.issue;
99
- const page = item.page;
100
- if (!volume && !issue && !page) {
101
- return "";
102
- }
103
- let result = "";
104
- if (volume) {
105
- result += volume;
106
- if (issue) {
107
- result += `(${issue})`;
108
- }
109
- if (page) {
110
- result += `:${page}`;
111
- }
112
- } else if (page) {
113
- result += page;
114
- }
115
- return result;
116
- }
117
- function getIdentifier(item) {
118
- if (item.PMID) {
119
- return `PMID:${item.PMID}`;
120
- }
121
- if (item.DOI) {
122
- return `DOI:${item.DOI}`;
123
- }
124
- if (item.URL) {
125
- return item.URL;
126
- }
127
- return "";
128
- }
129
- function formatBibliographyEntry(item) {
130
- const parts = [];
131
- const author = formatFirstAuthor(item);
132
- const etAl = hasMultipleAuthors(item) ? " et al" : "";
133
- parts.push(`${author}${etAl}.`);
134
- const journal = getJournalAbbrev(item);
135
- if (journal) {
136
- parts.push(`${journal}.`);
137
- }
138
- const year = extractYear(item);
139
- const volumeIssuePage = formatVolumeIssuePage(item);
140
- if (volumeIssuePage) {
141
- parts.push(`${year};${volumeIssuePage}.`);
142
- } else {
143
- parts.push(`${year}.`);
144
- }
145
- const identifier = getIdentifier(item);
146
- if (identifier) {
147
- parts.push(`${identifier}.`);
148
- }
149
- if (item.title) {
150
- parts.push(`${item.title}.`);
151
- }
152
- return parts.join(" ");
153
- }
154
- function getFirstAuthorFamilyName(item) {
155
- if (!item.author || item.author.length === 0) {
156
- return "Unknown";
157
- }
158
- const firstAuthor = item.author[0];
159
- if (!firstAuthor) {
160
- return "Unknown";
161
- }
162
- return firstAuthor.family || "Unknown";
163
- }
164
- function formatInTextEntry(item) {
165
- const author = getFirstAuthorFamilyName(item);
166
- const etAl = hasMultipleAuthors(item) ? " et al" : "";
167
- const year = extractYear(item);
168
- return `${author}${etAl}, ${year}`;
169
- }
170
- function formatBibliography(items) {
171
- if (items.length === 0) {
172
- return "";
173
- }
174
- return items.map(formatBibliographyEntry).join("\n\n");
175
- }
176
- function formatInText(items) {
177
- if (items.length === 0) {
178
- return "";
179
- }
180
- const citations = items.map(formatInTextEntry).join("; ");
181
- return `(${citations})`;
182
- }
183
- function registerCustomStyle(styleName, styleXml) {
184
- if (!styleXml.includes("<style") || !styleXml.includes("</style>")) {
185
- throw new Error(
186
- "Invalid CSL file: Missing <style> element. The file may be malformed or not a valid CSL style."
187
- );
188
- }
189
- const hasCitation = styleXml.includes("<citation") || styleXml.includes("<citation>");
190
- const hasBibliography = styleXml.includes("<bibliography") || styleXml.includes("<bibliography>");
191
- if (!hasCitation && !hasBibliography) {
192
- throw new Error(
193
- "Invalid CSL file: Missing <citation> or <bibliography> section. A valid CSL style must define at least one of these sections."
194
- );
195
- }
196
- try {
197
- const config = plugins.config.get("@csl");
198
- config.templates.add(styleName, styleXml);
199
- return styleName;
200
- } catch (error) {
201
- const message = error instanceof Error ? error.message : String(error);
202
- throw new Error(`Failed to register CSL style '${styleName}': ${message}`);
203
- }
204
- }
205
- function formatBibliographyCSL(items, options) {
206
- if (items.length === 0) {
207
- return "";
208
- }
209
- let style = options.style || "apa";
210
- const format = options.format || "text";
211
- const locale = options.locale || "en-US";
212
- if (options.styleXml) {
213
- style = registerCustomStyle(style, options.styleXml);
214
- }
215
- try {
216
- const cite2 = new Cite(items);
217
- const result = cite2.format("bibliography", {
218
- format,
219
- template: style,
220
- lang: locale
221
- });
222
- return result;
223
- } catch (error) {
224
- const errorMessage = error instanceof Error ? error.message : String(error);
225
- process.stderr.write(
226
- `Warning: CSL processing failed (style: ${style}), falling back to simplified format: ${errorMessage}
227
- `
228
- );
229
- return formatBibliography(items);
230
- }
231
- }
232
- function formatInTextCSL(items, options) {
233
- if (items.length === 0) {
234
- return "";
235
- }
236
- let style = options.style || "apa";
237
- const format = options.format || "text";
238
- const locale = options.locale || "en-US";
239
- if (options.styleXml) {
240
- style = registerCustomStyle(style, options.styleXml);
241
- }
242
- try {
243
- const cite2 = new Cite(items);
244
- const result = cite2.format("citation", {
245
- format,
246
- template: style,
247
- lang: locale
248
- });
249
- return result;
250
- } catch (error) {
251
- const errorMessage = error instanceof Error ? error.message : String(error);
252
- process.stderr.write(
253
- `Warning: CSL processing failed (style: ${style}), falling back to simplified format: ${errorMessage}
254
- `
255
- );
256
- return formatInText(items);
257
- }
258
- }
259
- function getCreatedAt(item) {
260
- const createdAt = item.custom?.created_at;
261
- if (!createdAt) return 0;
262
- return new Date(createdAt).getTime();
263
- }
264
- function getUpdatedAt(item) {
265
- const timestamp = item.custom?.timestamp;
266
- if (timestamp) return new Date(timestamp).getTime();
267
- return getCreatedAt(item);
268
- }
269
- function getPublishedDate(item) {
270
- const dateParts = item.issued?.["date-parts"]?.[0];
271
- if (!dateParts || dateParts.length === 0) return 0;
272
- const year = dateParts[0] ?? 0;
273
- const month = dateParts[1] ?? 1;
274
- const day = dateParts[2] ?? 1;
275
- return new Date(year, month - 1, day).getTime();
276
- }
277
- function getAuthorName(item) {
278
- const firstAuthor = item.author?.[0];
279
- if (!firstAuthor) return "Anonymous";
280
- return firstAuthor.family ?? firstAuthor.literal ?? "Anonymous";
281
- }
282
- function getTitle(item) {
283
- return item.title ?? "";
284
- }
285
- function getSortValue(item, field) {
286
- switch (field) {
287
- case "created":
288
- return getCreatedAt(item);
289
- case "updated":
290
- return getUpdatedAt(item);
291
- case "published":
292
- return getPublishedDate(item);
293
- case "author":
294
- return getAuthorName(item).toLowerCase();
295
- case "title":
296
- return getTitle(item).toLowerCase();
297
- }
298
- }
299
- function compareValues(a, b, order) {
300
- const multiplier = order === "desc" ? -1 : 1;
301
- if (typeof a === "number" && typeof b === "number") {
302
- return (a - b) * multiplier;
303
- }
304
- const strA = String(a);
305
- const strB = String(b);
306
- return strA.localeCompare(strB) * multiplier;
307
- }
308
- function sortReferences(items, sort, order) {
309
- return [...items].sort((a, b) => {
310
- const aValue = getSortValue(a, sort);
311
- const bValue = getSortValue(b, sort);
312
- const primaryCompare = compareValues(aValue, bValue, order);
313
- if (primaryCompare !== 0) return primaryCompare;
314
- const aCreated = getCreatedAt(a);
315
- const bCreated = getCreatedAt(b);
316
- const createdCompare = bCreated - aCreated;
317
- if (createdCompare !== 0) return createdCompare;
318
- return a.id.localeCompare(b.id);
319
- });
320
- }
321
- function paginate(items, options) {
322
- const offset = options.offset ?? 0;
323
- const limit = options.limit ?? 0;
324
- if (limit < 0) {
325
- throw new Error("limit must be non-negative");
326
- }
327
- if (offset < 0) {
328
- throw new Error("offset must be non-negative");
329
- }
330
- const isUnlimited = limit === 0;
331
- const afterOffset = items.slice(offset);
332
- const paginatedItems = isUnlimited ? afterOffset : afterOffset.slice(0, limit);
333
- let nextOffset = null;
334
- if (!isUnlimited && paginatedItems.length > 0) {
335
- const nextPosition = offset + paginatedItems.length;
336
- if (nextPosition < items.length) {
337
- nextOffset = nextPosition;
338
- }
339
- }
340
- return {
341
- items: paginatedItems,
342
- nextOffset
343
- };
344
- }
345
- const BUILTIN_STYLES = ["apa", "vancouver", "harvard"];
346
- function isBuiltinStyle(styleName) {
347
- return BUILTIN_STYLES.includes(styleName);
348
- }
349
- function expandTilde(filePath) {
350
- if (filePath.startsWith("~/")) {
351
- const home = process.env.HOME || process.env.USERPROFILE || "";
352
- return path.join(home, filePath.slice(2));
353
- }
354
- return filePath;
355
- }
356
- function loadCSLStyleFile(stylePath) {
357
- return fs.readFileSync(stylePath, "utf-8");
358
- }
359
- function resolveStyle(options) {
360
- const { cslFile, style, cslDirectory, defaultStyle } = options;
361
- if (cslFile) {
362
- if (!fs.existsSync(cslFile)) {
363
- throw new Error(`CSL file '${cslFile}' not found`);
364
- }
365
- const styleXml = loadCSLStyleFile(cslFile);
366
- const styleName = path.basename(cslFile, ".csl");
367
- return {
368
- type: "custom",
369
- styleName,
370
- styleXml
371
- };
372
- }
373
- const styleToResolve = style || defaultStyle || "apa";
374
- if (isBuiltinStyle(styleToResolve)) {
375
- return {
376
- type: "builtin",
377
- styleName: styleToResolve
378
- };
379
- }
380
- if (cslDirectory) {
381
- const directories = Array.isArray(cslDirectory) ? cslDirectory : [cslDirectory];
382
- for (const dir of directories) {
383
- const expandedDir = expandTilde(dir);
384
- const stylePath = path.join(expandedDir, `${styleToResolve}.csl`);
385
- if (fs.existsSync(stylePath)) {
386
- const styleXml = loadCSLStyleFile(stylePath);
387
- return {
388
- type: "custom",
389
- styleName: styleToResolve,
390
- styleXml
391
- };
392
- }
393
- }
394
- }
395
- if (defaultStyle && isBuiltinStyle(defaultStyle)) {
396
- return {
397
- type: "builtin",
398
- styleName: defaultStyle
399
- };
400
- }
401
- return {
402
- type: "builtin",
403
- styleName: "apa"
404
- };
405
- }
406
- function getFulltextAttachmentTypes(item) {
407
- const types = [];
408
- const attachments = item.custom?.attachments;
409
- const fulltextFiles = findFulltextFiles(attachments);
410
- for (const file of fulltextFiles) {
411
- const ext = file.filename.split(".").pop()?.toLowerCase();
412
- const format = ext ? extensionToFormat(ext) : void 0;
413
- if (format && !types.includes(format)) {
414
- types.push(format);
415
- }
416
- }
417
- return types;
418
- }
419
- async function deleteFulltextFiles(item, fulltextDirectory) {
420
- const filesToDelete = [];
421
- const attachments = item.custom?.attachments;
422
- if (attachments?.directory) {
423
- const fulltextFiles = findFulltextFiles(attachments);
424
- for (const file of fulltextFiles) {
425
- filesToDelete.push(join(fulltextDirectory, attachments.directory, file.filename));
426
- }
427
- }
428
- for (const filePath of filesToDelete) {
429
- try {
430
- await unlink(filePath);
431
- } catch {
432
- }
433
- }
434
- if (attachments?.directory) {
435
- try {
436
- const dirPath = join(fulltextDirectory, attachments.directory);
437
- await rmdir(dirPath);
438
- } catch {
439
- }
440
- }
441
- }
442
- async function removeReference(library, options) {
443
- const { identifier, idType = "id", fulltextDirectory, deleteFulltext = false } = options;
444
- const result = await library.remove(identifier, { idType });
445
- if (!result.removed || !result.removedItem) {
446
- return { removed: false };
447
- }
448
- let deletedFulltextTypes;
449
- if (deleteFulltext && fulltextDirectory) {
450
- deletedFulltextTypes = getFulltextAttachmentTypes(result.removedItem);
451
- if (deletedFulltextTypes.length > 0) {
452
- await deleteFulltextFiles(result.removedItem, fulltextDirectory);
453
- }
454
- }
455
- await library.save();
456
- return {
457
- removed: true,
458
- removedItem: result.removedItem,
459
- ...deletedFulltextTypes && deletedFulltextTypes.length > 0 && { deletedFulltextTypes }
460
- };
461
- }
462
- const remove = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
463
- __proto__: null,
464
- getFulltextAttachmentTypes,
465
- removeReference
466
- }, Symbol.toStringTag, { value: "Module" }));
467
- const DEFAULT_TTL_MS = 60 * 60 * 1e3;
468
- const pmidCache = /* @__PURE__ */ new Map();
469
- const doiCache = /* @__PURE__ */ new Map();
470
- const isbnCache = /* @__PURE__ */ new Map();
471
- function isEntryValid(entry) {
472
- const now = Date.now();
473
- return now - entry.cachedAt < entry.ttlMs;
474
- }
475
- function getFromCache(cache, key) {
476
- const entry = cache.get(key);
477
- if (!entry) {
478
- return void 0;
479
- }
480
- if (!isEntryValid(entry)) {
481
- cache.delete(key);
482
- return void 0;
483
- }
484
- return entry.item;
485
- }
486
- function storeInCache(cache, key, item, config) {
487
- const ttlMs = DEFAULT_TTL_MS;
488
- cache.set(key, {
489
- item,
490
- cachedAt: Date.now(),
491
- ttlMs
492
- });
493
- }
494
- function getPmidFromCache(pmid) {
495
- return getFromCache(pmidCache, pmid);
496
- }
497
- function cachePmidResult(pmid, item, config) {
498
- storeInCache(pmidCache, pmid, item);
499
- }
500
- function getDoiFromCache(doi) {
501
- return getFromCache(doiCache, doi);
502
- }
503
- function cacheDoiResult(doi, item, config) {
504
- storeInCache(doiCache, doi, item);
505
- }
506
- function getIsbnFromCache(isbn) {
507
- return getFromCache(isbnCache, isbn);
508
- }
509
- function cacheIsbnResult(isbn, item, config) {
510
- storeInCache(isbnCache, isbn, item);
511
- }
512
- const DOI_URL_PREFIXES$1 = [
513
- "https://doi.org/",
514
- "http://doi.org/",
515
- "https://dx.doi.org/",
516
- "http://dx.doi.org/"
517
- ];
518
- function normalizeDoi(doi) {
519
- const trimmed = doi.trim();
520
- if (!trimmed) {
521
- return "";
522
- }
523
- const lowerInput = trimmed.toLowerCase();
524
- for (const prefix of DOI_URL_PREFIXES$1) {
525
- if (lowerInput.startsWith(prefix.toLowerCase())) {
526
- return trimmed.slice(prefix.length);
527
- }
528
- }
529
- return trimmed;
530
- }
531
- function normalizePmid(pmid) {
532
- const trimmed = pmid.trim();
533
- if (!trimmed) {
534
- return "";
535
- }
536
- const normalized = trimmed.replace(/^pmid:\s*/i, "");
537
- return normalized.trim();
538
- }
539
- function normalizeIsbn(isbn) {
540
- const trimmed = isbn.trim();
541
- if (!trimmed) {
542
- return "";
543
- }
544
- if (!/^isbn:/i.test(trimmed)) {
545
- return "";
546
- }
547
- let normalized = trimmed.replace(/^isbn:\s*/i, "");
548
- normalized = normalized.replace(/[-\s]/g, "");
549
- normalized = normalized.toUpperCase();
550
- return normalized;
551
- }
552
- const EXTENSION_MAP = {
553
- ".json": "json",
554
- ".bib": "bibtex",
555
- ".ris": "ris",
556
- ".nbib": "nbib"
557
- };
558
- const DOI_URL_PREFIXES = [
559
- "https://doi.org/",
560
- "http://doi.org/",
561
- "https://dx.doi.org/",
562
- "http://dx.doi.org/"
563
- ];
564
- function detectByExtension(input) {
565
- if (!input) return "unknown";
566
- const dotIndex = input.lastIndexOf(".");
567
- if (dotIndex === -1 || dotIndex === input.length - 1) {
568
- return "unknown";
569
- }
570
- const ext = input.slice(dotIndex).toLowerCase();
571
- return EXTENSION_MAP[ext] ?? "unknown";
572
- }
573
- function detectByContent(content) {
574
- const trimmed = content.trim();
575
- if (!trimmed) return "unknown";
576
- if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
577
- return "json";
578
- }
579
- if (trimmed.startsWith("@")) {
580
- return "bibtex";
581
- }
582
- if (trimmed.startsWith("TY -")) {
583
- return "ris";
584
- }
585
- if (trimmed.startsWith("PMID-")) {
586
- return "nbib";
587
- }
588
- return detectIdentifier(trimmed);
589
- }
590
- function detectIdentifier(input) {
591
- const parts = input.split(/\s+/).filter((p) => p.length > 0);
592
- if (parts.length === 0) {
593
- return "unknown";
594
- }
595
- const formats = [];
596
- for (const part of parts) {
597
- const format = detectSingleIdentifier(part);
598
- if (format === "unknown") {
599
- return "unknown";
600
- }
601
- formats.push(format);
602
- }
603
- if (formats.length === 1) {
604
- return formats[0];
605
- }
606
- return "identifiers";
607
- }
608
- function detectSingleIdentifier(input) {
609
- if (isDoi(input)) {
610
- return "doi";
611
- }
612
- if (isIsbn(input)) {
613
- return "isbn";
614
- }
615
- if (isPmid(input)) {
616
- return "pmid";
617
- }
618
- return "unknown";
619
- }
620
- function isDoi(input) {
621
- for (const prefix of DOI_URL_PREFIXES) {
622
- if (input.toLowerCase().startsWith(prefix.toLowerCase())) {
623
- const remainder = input.slice(prefix.length);
624
- return isDoiFormat(remainder);
625
- }
626
- }
627
- return isDoiFormat(input);
628
- }
629
- function isDoiFormat(input) {
630
- if (!input.startsWith("10.")) {
631
- return false;
632
- }
633
- if (input.length <= 3) {
634
- return false;
635
- }
636
- const slashIndex = input.indexOf("/");
637
- if (slashIndex === -1 || slashIndex <= 3) {
638
- return false;
639
- }
640
- return true;
641
- }
642
- function isPmid(input) {
643
- if (!input || input.length === 0) {
644
- return false;
645
- }
646
- const normalized = normalizePmid(input);
647
- if (!normalized) {
648
- return false;
649
- }
650
- return /^\d+$/.test(normalized);
651
- }
652
- function isIsbn(input) {
653
- if (!input || input.length === 0) {
654
- return false;
655
- }
656
- const normalized = normalizeIsbn(input);
657
- if (!normalized) {
658
- return false;
659
- }
660
- if (normalized.length !== 10 && normalized.length !== 13) {
661
- return false;
662
- }
663
- if (normalized.length === 10) {
664
- if (!/^\d{9}[\dX]$/.test(normalized)) {
665
- return false;
666
- }
667
- return true;
668
- }
669
- if (!/^\d{13}$/.test(normalized)) {
670
- return false;
671
- }
672
- return true;
673
- }
674
- const RATE_LIMITS = {
675
- pubmed: {
676
- withoutApiKey: 3,
677
- // 3 req/sec
678
- withApiKey: 10
679
- // 10 req/sec
680
- },
681
- crossref: 50,
682
- // 50 req/sec
683
- isbn: 10
684
- // 10 req/sec (conservative for Google Books API daily limit)
685
- };
686
- class RateLimiterImpl {
687
- requestsPerSecond;
688
- intervalMs;
689
- _lastRequestTime = 0;
690
- _pending = Promise.resolve();
691
- constructor(requestsPerSecond) {
692
- this.requestsPerSecond = requestsPerSecond;
693
- this.intervalMs = 1e3 / requestsPerSecond;
694
- }
695
- get lastRequestTime() {
696
- return this._lastRequestTime;
697
- }
698
- async acquire() {
699
- this._pending = this._pending.then(() => this._acquireInternal());
700
- return this._pending;
701
- }
702
- async _acquireInternal() {
703
- const now = Date.now();
704
- const elapsed = now - this._lastRequestTime;
705
- const waitTime = Math.max(0, this.intervalMs - elapsed);
706
- if (waitTime > 0 && this._lastRequestTime > 0) {
707
- await this._delay(waitTime);
708
- }
709
- this._lastRequestTime = Date.now();
710
- }
711
- _delay(ms) {
712
- return new Promise((resolve) => setTimeout(resolve, ms));
713
- }
714
- }
715
- const limiters = /* @__PURE__ */ new Map();
716
- function createRateLimiter(options) {
717
- return new RateLimiterImpl(options.requestsPerSecond);
718
- }
719
- function getRateLimiter(api, config) {
720
- const existing = limiters.get(api);
721
- if (existing) {
722
- return existing;
723
- }
724
- const requestsPerSecond = getRequestsPerSecond(api, config);
725
- const limiter = createRateLimiter({ requestsPerSecond });
726
- limiters.set(api, limiter);
727
- return limiter;
728
- }
729
- function getRequestsPerSecond(api, config) {
730
- switch (api) {
731
- case "pubmed":
732
- return config.pubmedApiKey ? RATE_LIMITS.pubmed.withApiKey : RATE_LIMITS.pubmed.withoutApiKey;
733
- case "crossref":
734
- return RATE_LIMITS.crossref;
735
- case "isbn":
736
- return RATE_LIMITS.isbn;
737
- }
738
- }
739
- const PMC_API_BASE = "https://pmc.ncbi.nlm.nih.gov/api/ctxp/v1/pubmed/";
740
- const DEFAULT_TIMEOUT_MS = 1e4;
741
- const DOI_PATTERN = /^10\.\d{4,}(?:\.\d+)*\/\S+$/;
742
- function buildPmcUrl(pmids, config) {
743
- const url = new URL(PMC_API_BASE);
744
- url.searchParams.set("format", "csl");
745
- for (const pmid of pmids) {
746
- url.searchParams.append("id", pmid);
747
- }
748
- if (config.email) {
749
- url.searchParams.set("email", config.email);
750
- }
751
- if (config.apiKey) {
752
- url.searchParams.set("api_key", config.apiKey);
753
- }
754
- return url.toString();
755
- }
756
- function extractPmidFromId(id) {
757
- if (!id) return void 0;
758
- const match = id.match(/^pmid:(\d+)$/);
759
- return match?.[1];
760
- }
761
- function parseRawItems(rawItems) {
762
- const foundItems = /* @__PURE__ */ new Map();
763
- const validationErrors = /* @__PURE__ */ new Map();
764
- for (const rawItem of rawItems) {
765
- const parseResult = CslItemSchema.safeParse(rawItem);
766
- if (parseResult.success) {
767
- const pmid = extractPmidFromId(parseResult.data.id);
768
- if (pmid) {
769
- foundItems.set(pmid, parseResult.data);
770
- }
771
- } else {
772
- const maybeId = rawItem?.id;
773
- const pmid = extractPmidFromId(maybeId);
774
- if (pmid) {
775
- validationErrors.set(pmid, parseResult.error.message);
776
- }
777
- }
778
- }
779
- return { foundItems, validationErrors };
780
- }
781
- function buildPmidResult(pmid, foundItems, validationErrors) {
782
- const item = foundItems.get(pmid);
783
- if (item) {
784
- return { pmid, success: true, item };
785
- }
786
- const validationError = validationErrors.get(pmid);
787
- if (validationError) {
788
- return {
789
- pmid,
790
- success: false,
791
- error: `Invalid CSL-JSON data: ${validationError}`,
792
- reason: "validation_error"
793
- };
794
- }
795
- return {
796
- pmid,
797
- success: false,
798
- error: `PMID ${pmid} not found`,
799
- reason: "not_found"
800
- };
801
- }
802
- async function fetchPmids(pmids, config) {
803
- if (pmids.length === 0) {
804
- return [];
805
- }
806
- const rateLimiterConfig = config.apiKey ? { pubmedApiKey: config.apiKey } : {};
807
- const rateLimiter = getRateLimiter("pubmed", rateLimiterConfig);
808
- await rateLimiter.acquire();
809
- const url = buildPmcUrl(pmids, config);
810
- try {
811
- const response = await fetch(url, {
812
- method: "GET",
813
- signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
814
- });
815
- if (!response.ok) {
816
- const errorMsg = `HTTP ${response.status}: ${response.statusText}`;
817
- return pmids.map((pmid) => ({
818
- pmid,
819
- success: false,
820
- error: errorMsg,
821
- reason: "fetch_error"
822
- }));
823
- }
824
- const data = await response.json();
825
- const rawItems = Array.isArray(data) ? data : [data];
826
- const { foundItems, validationErrors } = parseRawItems(rawItems);
827
- return pmids.map((pmid) => buildPmidResult(pmid, foundItems, validationErrors));
828
- } catch (error) {
829
- const errorMsg = error instanceof Error ? error.message : String(error);
830
- return pmids.map((pmid) => ({
831
- pmid,
832
- success: false,
833
- error: errorMsg,
834
- reason: "fetch_error"
835
- }));
836
- }
837
- }
838
- async function fetchDoi(doi) {
839
- if (!DOI_PATTERN.test(doi)) {
840
- return {
841
- success: false,
842
- error: `Invalid DOI format: ${doi}`,
843
- reason: "validation_error"
844
- };
845
- }
846
- const rateLimiter = getRateLimiter("crossref", {});
847
- await rateLimiter.acquire();
848
- try {
849
- const cite2 = await Cite.async(doi);
850
- const rawItems = cite2.get({ format: "real", type: "json" });
851
- if (!rawItems || !Array.isArray(rawItems) || rawItems.length === 0) {
852
- return {
853
- success: false,
854
- error: `No data returned for DOI ${doi}`,
855
- reason: "not_found"
856
- };
857
- }
858
- const parseResult = CslItemSchema.safeParse(rawItems[0]);
859
- if (!parseResult.success) {
860
- return {
861
- success: false,
862
- error: `Invalid CSL-JSON data for DOI ${doi}: ${parseResult.error.message}`,
863
- reason: "validation_error"
864
- };
865
- }
866
- return { success: true, item: parseResult.data };
867
- } catch (error) {
868
- const errorMsg = error instanceof Error ? error.message : String(error);
869
- return {
870
- success: false,
871
- error: errorMsg,
872
- reason: "fetch_error"
873
- };
874
- }
875
- }
876
- const ISBN10_PATTERN = /^\d{9}[\dX]$/;
877
- const ISBN13_PATTERN = /^\d{13}$/;
878
- async function fetchIsbn(isbn) {
879
- if (!ISBN10_PATTERN.test(isbn) && !ISBN13_PATTERN.test(isbn)) {
880
- return {
881
- success: false,
882
- error: `Invalid ISBN format: ${isbn}`,
883
- reason: "validation_error"
884
- };
885
- }
886
- const rateLimiter = getRateLimiter("isbn", {});
887
- await rateLimiter.acquire();
888
- try {
889
- const cite2 = await Cite.async(isbn);
890
- const rawItems = cite2.get({ format: "real", type: "json" });
891
- if (!rawItems || !Array.isArray(rawItems) || rawItems.length === 0) {
892
- return {
893
- success: false,
894
- error: `No data returned for ISBN ${isbn}`,
895
- reason: "not_found"
896
- };
897
- }
898
- const parseResult = CslItemSchema.safeParse(rawItems[0]);
899
- if (!parseResult.success) {
900
- return {
901
- success: false,
902
- error: `Invalid CSL-JSON data for ISBN ${isbn}: ${parseResult.error.message}`,
903
- reason: "validation_error"
904
- };
905
- }
906
- return { success: true, item: parseResult.data };
907
- } catch (error) {
908
- const errorMsg = error instanceof Error ? error.message : String(error);
909
- return {
910
- success: false,
911
- error: errorMsg,
912
- reason: "fetch_error"
913
- };
914
- }
915
- }
916
- function parseBibtex(content) {
917
- return parseWithCitationJs(content, "bibtex");
918
- }
919
- function parseRis(content) {
920
- return parseWithCitationJs(content, "ris");
921
- }
922
- const NBIB_TO_RIS_TAG_MAP = {
923
- PMID: "AN",
924
- // PubMed ID -> Accession Number
925
- TI: "TI",
926
- // Title -> Title
927
- FAU: "AU",
928
- // Full Author -> Author
929
- AU: "AU",
930
- // Author -> Author (short form, use FAU when available)
931
- JT: "JO",
932
- // Journal Title -> Journal Name
933
- TA: "JA",
934
- // Title Abbreviation -> Journal Abbreviation
935
- AB: "AB",
936
- // Abstract -> Abstract
937
- VI: "VL",
938
- // Volume -> Volume
939
- IP: "IS",
940
- // Issue/Part -> Issue Number
941
- PG: "SP",
942
- // Pagination -> Start Page (includes range)
943
- DP: "PY",
944
- // Date of Publication -> Publication Year
945
- LA: "LA",
946
- // Language -> Language
947
- MH: "KW",
948
- // MeSH Headings -> Keywords
949
- OT: "KW",
950
- // Other Terms -> Keywords
951
- AD: "AD",
952
- // Affiliation/Address -> Author Address
953
- IS: "SN",
954
- // ISSN -> Serial Number
955
- PT: "TY"
956
- // Publication Type -> Type of Reference
957
- };
958
- const NBIB_PUBLICATION_TYPE_MAP = {
959
- "Journal Article": "JOUR",
960
- Review: "JOUR",
961
- Book: "BOOK",
962
- "Book Chapter": "CHAP",
963
- "Conference Paper": "CPAPER",
964
- Thesis: "THES",
965
- Report: "RPRT"
966
- };
967
- function parseNbibEntry(entry) {
968
- const result = [];
969
- const lines = entry.split("\n");
970
- let currentTag = "";
971
- let currentValue = "";
972
- for (const line of lines) {
973
- const tagMatch = line.match(/^([A-Z]{2,4})\s*-\s*(.*)$/);
974
- if (tagMatch) {
975
- if (currentTag) {
976
- result.push({ tag: currentTag, value: currentValue.trim() });
977
- }
978
- currentTag = tagMatch[1] ?? "";
979
- currentValue = tagMatch[2] ?? "";
980
- } else if (currentTag && line.match(/^\s+/)) {
981
- currentValue += ` ${line.trim()}`;
982
- }
983
- }
984
- if (currentTag) {
985
- result.push({ tag: currentTag, value: currentValue.trim() });
986
- }
987
- return result;
988
- }
989
- function convertNbibTagToRisLine(tag, value) {
990
- if (tag === "AID" && value.includes("[doi]")) {
991
- const doi = value.replace(/\s*\[doi\].*$/, "").trim();
992
- return { line: `DO - ${doi}` };
993
- }
994
- if (tag === "AID" && value.includes("[pii]")) {
995
- const pii = value.replace(/\s*\[pii\].*$/, "").trim();
996
- return { line: `C1 - ${pii}` };
997
- }
998
- const risTag = NBIB_TO_RIS_TAG_MAP[tag];
999
- if (!risTag) {
1000
- return null;
1001
- }
1002
- if (risTag === "PY") {
1003
- const yearMatch = value.match(/^(\d{4})/);
1004
- return yearMatch ? { line: `PY - ${yearMatch[1]}` } : null;
1005
- }
1006
- if (risTag === "TY") {
1007
- const risType = NBIB_PUBLICATION_TYPE_MAP[value] || "JOUR";
1008
- return { line: `TY - ${risType}`, isType: true };
1009
- }
1010
- return { line: `${risTag} - ${value}` };
1011
- }
1012
- function convertSingleNbibEntryToRis(entry) {
1013
- const parsed = parseNbibEntry(entry);
1014
- if (parsed.length === 0) {
1015
- return "";
1016
- }
1017
- const risLines = [];
1018
- let hasType = false;
1019
- for (const { tag, value } of parsed) {
1020
- const converted = convertNbibTagToRisLine(tag, value);
1021
- if (!converted) {
1022
- continue;
1023
- }
1024
- if (converted.isType) {
1025
- risLines.unshift(converted.line);
1026
- hasType = true;
1027
- } else {
1028
- risLines.push(converted.line);
1029
- }
1030
- }
1031
- if (!hasType) {
1032
- risLines.unshift("TY - JOUR");
1033
- }
1034
- risLines.push("ER -");
1035
- return risLines.join("\n");
1036
- }
1037
- function convertNbibToRis(content) {
1038
- const trimmed = content.trim();
1039
- if (!trimmed) {
1040
- return "";
1041
- }
1042
- const entries = trimmed.split(/\n\s*\n/).filter((e) => e.trim());
1043
- const risEntries = entries.map((entry) => convertSingleNbibEntryToRis(entry)).filter(Boolean);
1044
- return risEntries.join("\n\n");
1045
- }
1046
- function parseNbib(content) {
1047
- const trimmed = content.trim();
1048
- if (!trimmed) {
1049
- return { success: true, items: [] };
1050
- }
1051
- const risContent = convertNbibToRis(trimmed);
1052
- if (!risContent) {
1053
- return {
1054
- success: false,
1055
- items: [],
1056
- error: "Failed to convert NBIB to RIS: No valid entries found"
1057
- };
1058
- }
1059
- return parseRis(risContent);
1060
- }
1061
- function parseWithCitationJs(content, format) {
1062
- const trimmed = content.trim();
1063
- if (!trimmed) {
1064
- return { success: true, items: [] };
1065
- }
1066
- try {
1067
- const cite2 = new Cite(trimmed);
1068
- const items = cite2.get({ format: "real", type: "json" });
1069
- if (!items || items.length === 0) {
1070
- if (isEmptyFormat(trimmed, format)) {
1071
- return { success: true, items: [] };
1072
- }
1073
- return {
1074
- success: false,
1075
- items: [],
1076
- error: `No valid ${format.toUpperCase()} entries found`
1077
- };
1078
- }
1079
- return { success: true, items };
1080
- } catch (error) {
1081
- const errorMessage = error instanceof Error ? error.message : String(error);
1082
- if (isEmptyFormat(trimmed, format)) {
1083
- return { success: true, items: [] };
1084
- }
1085
- return {
1086
- success: false,
1087
- items: [],
1088
- error: `Failed to parse ${format.toUpperCase()}: ${errorMessage}`
1089
- };
1090
- }
1091
- }
1092
- function isEmptyFormat(content, format) {
1093
- if (format === "bibtex") {
1094
- const lines = content.split("\n");
1095
- return lines.every((line) => {
1096
- const trimmed = line.trim();
1097
- return trimmed === "" || trimmed.startsWith("%");
1098
- });
1099
- }
1100
- return false;
1101
- }
1102
- function classifyIdentifiers(identifiers) {
1103
- const pmids = [];
1104
- const dois = [];
1105
- const isbns = [];
1106
- const unknowns = [];
1107
- for (const id of identifiers) {
1108
- if (isPmid(id)) {
1109
- pmids.push(normalizePmid(id));
1110
- } else if (isDoi(id)) {
1111
- dois.push(normalizeDoi(id));
1112
- } else if (isIsbn(id)) {
1113
- isbns.push(normalizeIsbn(id));
1114
- } else {
1115
- unknowns.push(id);
1116
- }
1117
- }
1118
- return { pmids, dois, isbns, unknowns };
1119
- }
1120
- function buildUnknownResults(unknowns) {
1121
- return unknowns.map((unknown) => ({
1122
- success: false,
1123
- error: `Cannot interpret '${unknown}' as identifier (not a valid PMID or DOI)`,
1124
- source: unknown,
1125
- reason: "validation_error"
1126
- }));
1127
- }
1128
- function clearItemId(item) {
1129
- const { id: _ignored, ...rest } = item;
1130
- return { ...rest, id: "" };
1131
- }
1132
- async function fetchPmidsWithCache(pmids, pubmedConfig) {
1133
- const results = [];
1134
- const pmidsToFetch = [];
1135
- for (const pmid of pmids) {
1136
- const cached = getPmidFromCache(pmid);
1137
- if (cached) {
1138
- results.push({ success: true, item: clearItemId(cached), source: pmid });
1139
- } else {
1140
- pmidsToFetch.push(pmid);
1141
- }
1142
- }
1143
- if (pmidsToFetch.length > 0) {
1144
- const fetchResults = await fetchPmids(pmidsToFetch, pubmedConfig);
1145
- for (const fetchResult of fetchResults) {
1146
- if (fetchResult.success) {
1147
- cachePmidResult(fetchResult.pmid, fetchResult.item);
1148
- results.push({
1149
- success: true,
1150
- item: clearItemId(fetchResult.item),
1151
- source: fetchResult.pmid
1152
- });
1153
- } else {
1154
- results.push({
1155
- success: false,
1156
- error: fetchResult.error,
1157
- source: fetchResult.pmid,
1158
- reason: fetchResult.reason
1159
- });
1160
- }
1161
- }
1162
- }
1163
- return results;
1164
- }
1165
- async function fetchDoisWithCache(dois) {
1166
- const results = [];
1167
- for (const doi of dois) {
1168
- const cached = getDoiFromCache(doi);
1169
- if (cached) {
1170
- results.push({ success: true, item: clearItemId(cached), source: doi });
1171
- continue;
1172
- }
1173
- const fetchResult = await fetchDoi(doi);
1174
- if (fetchResult.success) {
1175
- cacheDoiResult(doi, fetchResult.item);
1176
- results.push({ success: true, item: clearItemId(fetchResult.item), source: doi });
1177
- } else {
1178
- results.push({
1179
- success: false,
1180
- error: fetchResult.error,
1181
- source: doi,
1182
- reason: fetchResult.reason
1183
- });
1184
- }
1185
- }
1186
- return results;
1187
- }
1188
- async function fetchIsbnsWithCache(isbns) {
1189
- const results = [];
1190
- for (const isbn of isbns) {
1191
- const cached = getIsbnFromCache(isbn);
1192
- if (cached) {
1193
- results.push({ success: true, item: clearItemId(cached), source: isbn });
1194
- continue;
1195
- }
1196
- const fetchResult = await fetchIsbn(isbn);
1197
- if (fetchResult.success) {
1198
- cacheIsbnResult(isbn, fetchResult.item);
1199
- results.push({ success: true, item: clearItemId(fetchResult.item), source: isbn });
1200
- } else {
1201
- results.push({
1202
- success: false,
1203
- error: fetchResult.error,
1204
- source: isbn,
1205
- reason: fetchResult.reason
1206
- });
1207
- }
1208
- }
1209
- return results;
1210
- }
1211
- function parseJsonContent(content) {
1212
- try {
1213
- const parsed = JSON.parse(content);
1214
- const items = Array.isArray(parsed) ? parsed : [parsed];
1215
- if (items.length === 0) {
1216
- return { results: [] };
1217
- }
1218
- const results = [];
1219
- for (const item of items) {
1220
- const parseResult = CslItemSchema.safeParse(item);
1221
- if (parseResult.success) {
1222
- results.push({ success: true, item: parseResult.data, source: "json" });
1223
- } else {
1224
- results.push({
1225
- success: false,
1226
- error: `Invalid CSL-JSON: ${parseResult.error.message}`,
1227
- source: "json",
1228
- reason: "validation_error"
1229
- });
1230
- }
1231
- }
1232
- return { results };
1233
- } catch (error) {
1234
- const message = error instanceof Error ? error.message : String(error);
1235
- return {
1236
- results: [
1237
- {
1238
- success: false,
1239
- error: `Failed to parse JSON: ${message}`,
1240
- source: "json",
1241
- reason: "parse_error"
1242
- }
1243
- ]
1244
- };
1245
- }
1246
- }
1247
- function parseBibtexContent(content) {
1248
- const parseResult = parseBibtex(content);
1249
- if (!parseResult.success) {
1250
- return {
1251
- results: [
1252
- {
1253
- success: false,
1254
- error: parseResult.error ?? "Failed to parse BibTeX",
1255
- source: "bibtex",
1256
- reason: "parse_error"
1257
- }
1258
- ]
1259
- };
1260
- }
1261
- if (parseResult.items.length === 0) {
1262
- return { results: [] };
1263
- }
1264
- return {
1265
- results: parseResult.items.map((item) => ({
1266
- success: true,
1267
- item,
1268
- source: "bibtex"
1269
- }))
1270
- };
1271
- }
1272
- function parseRisContent(content) {
1273
- const parseResult = parseRis(content);
1274
- if (!parseResult.success) {
1275
- return {
1276
- results: [
1277
- {
1278
- success: false,
1279
- error: parseResult.error ?? "Failed to parse RIS",
1280
- source: "ris",
1281
- reason: "parse_error"
1282
- }
1283
- ]
1284
- };
1285
- }
1286
- if (parseResult.items.length === 0) {
1287
- return { results: [] };
1288
- }
1289
- return {
1290
- results: parseResult.items.map((item) => ({
1291
- success: true,
1292
- item,
1293
- source: "ris"
1294
- }))
1295
- };
1296
- }
1297
- function parseNbibContent(content) {
1298
- const parseResult = parseNbib(content);
1299
- if (!parseResult.success) {
1300
- return {
1301
- results: [
1302
- {
1303
- success: false,
1304
- error: parseResult.error ?? "Failed to parse NBIB",
1305
- source: "nbib",
1306
- reason: "parse_error"
1307
- }
1308
- ]
1309
- };
1310
- }
1311
- if (parseResult.items.length === 0) {
1312
- return { results: [] };
1313
- }
1314
- return {
1315
- results: parseResult.items.map((item) => ({
1316
- success: true,
1317
- item,
1318
- source: "nbib"
1319
- }))
1320
- };
1321
- }
1322
- async function importFromContent(content, format, _options) {
1323
- let actualFormat;
1324
- if (format === "auto") {
1325
- actualFormat = detectByContent(content);
1326
- if (actualFormat === "unknown") {
1327
- return {
1328
- results: [
1329
- {
1330
- success: false,
1331
- error: "Cannot detect input format. Use --format to specify explicitly.",
1332
- source: "content",
1333
- reason: "validation_error"
1334
- }
1335
- ]
1336
- };
1337
- }
1338
- } else {
1339
- actualFormat = format;
1340
- }
1341
- switch (actualFormat) {
1342
- case "json":
1343
- return parseJsonContent(content);
1344
- case "bibtex":
1345
- return parseBibtexContent(content);
1346
- case "ris":
1347
- return parseRisContent(content);
1348
- case "nbib":
1349
- return parseNbibContent(content);
1350
- default:
1351
- return {
1352
- results: [
1353
- {
1354
- success: false,
1355
- error: `Unsupported format for content parsing: ${actualFormat}`,
1356
- source: "content",
1357
- reason: "validation_error"
1358
- }
1359
- ]
1360
- };
1361
- }
1362
- }
1363
- async function importFromIdentifiers(identifiers, options) {
1364
- if (identifiers.length === 0) {
1365
- return { results: [] };
1366
- }
1367
- const { pmids, dois, isbns, unknowns } = classifyIdentifiers(identifiers);
1368
- const results = [];
1369
- results.push(...buildUnknownResults(unknowns));
1370
- const pmidResults = await fetchPmidsWithCache(pmids, options.pubmedConfig ?? {});
1371
- results.push(...pmidResults);
1372
- const doiResults = await fetchDoisWithCache(dois);
1373
- results.push(...doiResults);
1374
- const isbnResults = await fetchIsbnsWithCache(isbns);
1375
- results.push(...isbnResults);
1376
- return { results };
1377
- }
1378
- function looksLikeFilePath(input) {
1379
- const fileExtensions = [".json", ".bib", ".ris", ".txt", ".xml", ".yaml", ".yml"];
1380
- const lowerInput = input.toLowerCase();
1381
- return fileExtensions.some((ext) => lowerInput.endsWith(ext));
1382
- }
1383
- async function processFile(filePath, options) {
1384
- try {
1385
- const content = readFileSync(filePath, "utf-8");
1386
- let format;
1387
- if (options.format && options.format !== "auto") {
1388
- format = options.format;
1389
- } else {
1390
- const extFormat = detectByExtension(filePath);
1391
- format = extFormat !== "unknown" ? extFormat : "auto";
1392
- }
1393
- const result = await importFromContent(content, format, options);
1394
- return result.results.map((r) => ({
1395
- ...r,
1396
- source: filePath
1397
- }));
1398
- } catch (error) {
1399
- const message = error instanceof Error ? error.message : String(error);
1400
- return [
1401
- {
1402
- success: false,
1403
- error: `Failed to read file: ${message}`,
1404
- source: filePath,
1405
- reason: "fetch_error"
1406
- }
1407
- ];
1408
- }
1409
- }
1410
- async function processIdentifiers(inputs, options) {
1411
- const results = [];
1412
- const validIdentifiers = [];
1413
- for (const input of inputs) {
1414
- const isValidPmid = isPmid(input);
1415
- const isValidDoi = isDoi(input);
1416
- const isValidIsbn = isIsbn(input);
1417
- if (isValidPmid || isValidDoi || isValidIsbn) {
1418
- validIdentifiers.push(input);
1419
- } else {
1420
- const hint = looksLikeFilePath(input) ? " Hint: If this is a file path, check that the file exists." : "";
1421
- results.push({
1422
- success: false,
1423
- error: `Cannot interpret '${input}' as identifier (not a valid PMID, DOI, or ISBN).${hint}`,
1424
- source: input,
1425
- reason: "validation_error"
1426
- });
1427
- }
1428
- }
1429
- if (validIdentifiers.length > 0) {
1430
- const fetchResult = await importFromIdentifiers(validIdentifiers, options);
1431
- results.push(...fetchResult.results);
1432
- }
1433
- return results;
1434
- }
1435
- async function importFromInputs(inputs, options) {
1436
- const allResults = [];
1437
- if (options.stdinContent?.trim()) {
1438
- const stdinResults = await processStdinContent(options.stdinContent, options);
1439
- allResults.push(...stdinResults);
1440
- }
1441
- if (inputs.length > 0) {
1442
- const identifiersToFetch = [];
1443
- for (const input of inputs) {
1444
- if (existsSync(input)) {
1445
- const fileResults = await processFile(input, options);
1446
- allResults.push(...fileResults);
1447
- } else {
1448
- identifiersToFetch.push(input);
1449
- }
1450
- }
1451
- if (identifiersToFetch.length > 0) {
1452
- const identifierResults = await processIdentifiers(identifiersToFetch, options);
1453
- allResults.push(...identifierResults);
1454
- }
1455
- }
1456
- return { results: allResults };
1457
- }
1458
- async function processStdinContent(content, options) {
1459
- const format = options.format || "auto";
1460
- if (format === "json" || format === "bibtex" || format === "ris" || format === "nbib") {
1461
- const result = await importFromContent(content, format);
1462
- return result.results.map((r) => ({
1463
- ...r,
1464
- source: r.source === "content" ? "stdin" : r.source
1465
- }));
1466
- }
1467
- if (format === "pmid" || format === "doi" || format === "isbn") {
1468
- const identifiers2 = content.split(/\s+/).filter((s) => s.length > 0);
1469
- return processIdentifiers(identifiers2, options);
1470
- }
1471
- const detectedFormat = detectByContent(content);
1472
- if (detectedFormat === "json" || detectedFormat === "bibtex" || detectedFormat === "ris" || detectedFormat === "nbib") {
1473
- const result = await importFromContent(content, detectedFormat);
1474
- return result.results.map((r) => ({
1475
- ...r,
1476
- source: r.source === "content" ? "stdin" : r.source
1477
- }));
1478
- }
1479
- const identifiers = content.split(/\s+/).filter((s) => s.length > 0);
1480
- if (identifiers.length === 0) {
1481
- return [];
1482
- }
1483
- return processIdentifiers(identifiers, options);
1484
- }
1485
- async function addReferences(inputs, library, options) {
1486
- const added = [];
1487
- const failed = [];
1488
- const skipped = [];
1489
- const importOptions = buildImportOptions(options);
1490
- const importResult = await importFromInputs(inputs, importOptions);
1491
- const existingItems = await library.getAll();
1492
- const addedIds = /* @__PURE__ */ new Set();
1493
- for (const result of importResult.results) {
1494
- const processed = await processImportResult(
1495
- result,
1496
- existingItems,
1497
- addedIds,
1498
- options.force ?? false,
1499
- library
1500
- );
1501
- if (processed.type === "failed") {
1502
- failed.push(processed.item);
1503
- } else if (processed.type === "skipped") {
1504
- skipped.push(processed.item);
1505
- } else {
1506
- added.push(processed.item);
1507
- }
1508
- }
1509
- if (added.length > 0) {
1510
- await library.save();
1511
- }
1512
- return { added, failed, skipped };
1513
- }
1514
- function buildImportOptions(options) {
1515
- const importOptions = {};
1516
- if (options.format !== void 0) {
1517
- importOptions.format = options.format;
1518
- }
1519
- if (options.pubmedConfig !== void 0) {
1520
- importOptions.pubmedConfig = options.pubmedConfig;
1521
- }
1522
- if (options.stdinContent !== void 0) {
1523
- importOptions.stdinContent = options.stdinContent;
1524
- }
1525
- return importOptions;
1526
- }
1527
- async function processImportResult(result, existingItems, addedIds, force, library) {
1528
- if (!result.success) {
1529
- return {
1530
- type: "failed",
1531
- item: { source: result.source, error: result.error, reason: result.reason }
1532
- };
1533
- }
1534
- const item = result.item;
1535
- if (!force) {
1536
- const duplicateResult = detectDuplicate(item, existingItems);
1537
- const existingMatch = duplicateResult.matches[0];
1538
- if (existingMatch) {
1539
- return {
1540
- type: "skipped",
1541
- item: {
1542
- source: result.source,
1543
- existingId: existingMatch.existing.id ?? "",
1544
- duplicateType: existingMatch.type
1545
- }
1546
- };
1547
- }
1548
- }
1549
- const allExistingIds = /* @__PURE__ */ new Set([...existingItems.map((i) => i.id), ...addedIds]);
1550
- const originalId = item.id?.trim() || "";
1551
- const hasOriginalId = originalId.length > 0;
1552
- const baseId = hasOriginalId ? originalId : generateId(item);
1553
- const { id, changed } = resolveIdCollision(baseId, allExistingIds);
1554
- const finalItem = { ...item, id };
1555
- const addedToLibrary = await library.add(finalItem);
1556
- addedIds.add(id);
1557
- const uuid = addedToLibrary.custom?.uuid ?? "";
1558
- const addedItem = {
1559
- source: result.source,
1560
- id,
1561
- uuid,
1562
- title: typeof finalItem.title === "string" ? finalItem.title : ""
1563
- };
1564
- if (hasOriginalId && changed) {
1565
- addedItem.idChanged = true;
1566
- addedItem.originalId = originalId;
1567
- }
1568
- return { type: "added", item: addedItem };
1569
- }
1570
- function generateSuffix(index) {
1571
- const alphabet = "abcdefghijklmnopqrstuvwxyz";
1572
- let suffix = "";
1573
- let n = index;
1574
- do {
1575
- suffix = alphabet[n % 26] + suffix;
1576
- n = Math.floor(n / 26) - 1;
1577
- } while (n >= 0);
1578
- return suffix;
1579
- }
1580
- function resolveIdCollision(baseId, existingIds) {
1581
- if (!existingIds.has(baseId)) {
1582
- return { id: baseId, changed: false };
1583
- }
1584
- let index = 0;
1585
- let newId;
1586
- do {
1587
- const suffix = generateSuffix(index);
1588
- newId = `${baseId}${suffix}`;
1589
- index++;
1590
- } while (existingIds.has(newId));
1591
- return { id: newId, changed: true };
1592
- }
1593
- const add = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1594
- __proto__: null,
1595
- addReferences
1596
- }, Symbol.toStringTag, { value: "Module" }));
1597
- function buildPubmedConfig(config) {
1598
- const pubmedConfig = {};
1599
- if (config.pubmed.email !== void 0) {
1600
- pubmedConfig.email = config.pubmed.email;
1601
- }
1602
- if (config.pubmed.apiKey !== void 0) {
1603
- pubmedConfig.apiKey = config.pubmed.apiKey;
1604
- }
1605
- return pubmedConfig;
1606
- }
1607
- function createAddRoute(library, config) {
1608
- const route = new Hono();
1609
- route.post("/", async (c) => {
1610
- let body;
1611
- try {
1612
- body = await c.req.json();
1613
- } catch {
1614
- return c.json({ error: "Invalid JSON" }, 400);
1615
- }
1616
- if (!body || typeof body !== "object") {
1617
- return c.json({ error: "Request body must be an object" }, 400);
1618
- }
1619
- const { inputs, options } = body;
1620
- if (!inputs || !Array.isArray(inputs) || inputs.length === 0) {
1621
- return c.json({ error: "inputs must be a non-empty array of strings" }, 400);
1622
- }
1623
- if (!inputs.every((input) => typeof input === "string")) {
1624
- return c.json({ error: "All inputs must be strings" }, 400);
1625
- }
1626
- const addOptions = {
1627
- force: options?.force ?? false,
1628
- pubmedConfig: buildPubmedConfig(config)
1629
- };
1630
- if (options?.format) {
1631
- addOptions.format = options.format;
1632
- }
1633
- const result = await addReferences(inputs, library, addOptions);
1634
- return c.json(result);
1635
- });
1636
- return route;
1637
- }
1638
- function shouldUseFallback(style, hasCustomXml) {
1639
- if (hasCustomXml) {
1640
- return false;
1641
- }
1642
- if (style && !isBuiltinStyle(style)) {
1643
- return true;
1644
- }
1645
- return false;
1646
- }
1647
- function formatCitation(item, inText, options) {
1648
- const useFallback = shouldUseFallback(options.style, !!options.styleXml);
1649
- const style = options.style ?? "apa";
1650
- const locale = options.locale ?? "en-US";
1651
- const format = options.format ?? "text";
1652
- const styleXml = options.styleXml;
1653
- if (useFallback) {
1654
- return inText ? formatInText([item]) : formatBibliography([item]);
1655
- }
1656
- const formatOptions = { style, locale, format, ...styleXml && { styleXml } };
1657
- return inText ? formatInTextCSL([item], formatOptions) : formatBibliographyCSL([item], formatOptions);
1658
- }
1659
- async function generateCitationForIdentifier(library, identifier, idType, inText, options) {
1660
- const item = await library.find(identifier, { idType });
1661
- if (!item) {
1662
- const lookupType = idType === "uuid" ? "UUID" : idType.toUpperCase();
1663
- return {
1664
- success: false,
1665
- identifier,
1666
- error: `Reference with ${lookupType} '${identifier}' not found`
1667
- };
1668
- }
1669
- const citation = formatCitation(item, inText, options);
1670
- return {
1671
- success: true,
1672
- identifier,
1673
- citation
1674
- };
1675
- }
1676
- async function citeReferences(library, options) {
1677
- const {
1678
- identifiers,
1679
- idType = "id",
1680
- inText = false,
1681
- style,
1682
- cslFile,
1683
- defaultStyle,
1684
- cslDirectory,
1685
- locale,
1686
- format
1687
- } = options;
1688
- const results = [];
1689
- const resolution = resolveStyle({
1690
- ...cslFile !== void 0 && { cslFile },
1691
- ...style !== void 0 && { style },
1692
- ...defaultStyle !== void 0 && { defaultStyle },
1693
- ...cslDirectory !== void 0 && { cslDirectory }
1694
- });
1695
- for (const identifier of identifiers) {
1696
- const result = await generateCitationForIdentifier(library, identifier, idType, inText, {
1697
- style: resolution.styleName,
1698
- styleXml: resolution.styleXml,
1699
- locale,
1700
- format
1701
- });
1702
- results.push(result);
1703
- }
1704
- return { results };
1705
- }
1706
- const cite = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1707
- __proto__: null,
1708
- citeReferences
1709
- }, Symbol.toStringTag, { value: "Module" }));
1710
- const CiteRequestSchema = z.object({
1711
- identifiers: z.array(z.string()).min(1, "identifiers must be a non-empty array"),
1712
- idType: z.enum(["id", "uuid", "doi", "pmid", "isbn"]).optional(),
1713
- inText: z.boolean().optional(),
1714
- style: z.string().optional(),
1715
- cslFile: z.string().optional(),
1716
- locale: z.string().optional(),
1717
- format: z.enum(["text", "html"]).optional()
1718
- });
1719
- function buildCiteOptions(body) {
1720
- return {
1721
- identifiers: body.identifiers,
1722
- ...body.idType !== void 0 && { idType: body.idType },
1723
- ...body.inText !== void 0 && { inText: body.inText },
1724
- ...body.style !== void 0 && { style: body.style },
1725
- ...body.cslFile !== void 0 && { cslFile: body.cslFile },
1726
- ...body.locale !== void 0 && { locale: body.locale },
1727
- ...body.format !== void 0 && { format: body.format }
1728
- };
1729
- }
1730
- function createCiteRoute(library) {
1731
- const route = new Hono();
1732
- route.post("/", async (c) => {
1733
- let rawBody;
1734
- try {
1735
- rawBody = await c.req.json();
1736
- } catch {
1737
- return c.json({ error: "Invalid JSON" }, 400);
1738
- }
1739
- const parseResult = CiteRequestSchema.safeParse(rawBody);
1740
- if (!parseResult.success) {
1741
- const errorMessage = parseResult.error.issues[0]?.message ?? "Invalid request body";
1742
- return c.json({ error: errorMessage }, 400);
1743
- }
1744
- const result = await citeReferences(library, buildCiteOptions(parseResult.data));
1745
- return c.json(result);
1746
- });
1747
- return route;
1748
- }
1749
- const healthRoute = new Hono();
1750
- healthRoute.get("/", (c) => {
1751
- return c.json({ status: "ok" });
1752
- });
1753
- async function listReferences(library, options) {
1754
- const sort = options.sort ?? "updated";
1755
- const order = options.order ?? "desc";
1756
- const limit = options.limit ?? 0;
1757
- const offset = options.offset ?? 0;
1758
- const allItems = await library.getAll();
1759
- const total = allItems.length;
1760
- const sorted = sortReferences(allItems, sort, order);
1761
- const { items: paginatedItems, nextOffset } = paginate(sorted, { limit, offset });
1762
- return {
1763
- items: paginatedItems,
1764
- total,
1765
- limit,
1766
- offset,
1767
- nextOffset
1768
- };
1769
- }
1770
- const list = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1771
- __proto__: null,
1772
- listReferences
1773
- }, Symbol.toStringTag, { value: "Module" }));
1774
- const listRequestBodySchema = z.object({
1775
- sort: sortFieldSchema.optional(),
1776
- order: sortOrderSchema.optional(),
1777
- limit: z.number().int().min(0).optional(),
1778
- offset: z.number().int().min(0).optional()
1779
- });
1780
- function createListRoute(library) {
1781
- const route = new Hono();
1782
- route.post("/", async (c) => {
1783
- let body;
1784
- try {
1785
- body = await c.req.json();
1786
- } catch {
1787
- return c.json({ error: "Invalid JSON" }, 400);
1788
- }
1789
- const parseResult = listRequestBodySchema.safeParse(body);
1790
- if (!parseResult.success) {
1791
- const errorMessage = parseResult.error.issues[0]?.message ?? "Invalid request body";
1792
- return c.json({ error: errorMessage }, 400);
1793
- }
1794
- const requestBody = parseResult.data;
1795
- const options = pickDefined(requestBody, [
1796
- "sort",
1797
- "order",
1798
- "limit",
1799
- "offset"
1800
- ]);
1801
- const result = await listReferences(library, options);
1802
- return c.json(result);
1803
- });
1804
- return route;
1805
- }
1806
- async function updateReference(library, options) {
1807
- const { identifier, idType = "id", updates, onIdCollision = "fail" } = options;
1808
- const updateResult = await library.update(identifier, updates, { idType, onIdCollision });
1809
- if (!updateResult.updated) {
1810
- const result2 = { updated: false };
1811
- if (updateResult.errorType) {
1812
- result2.errorType = updateResult.errorType;
1813
- }
1814
- if (updateResult.item) {
1815
- result2.item = updateResult.item;
1816
- }
1817
- if (updateResult.idChanged && updateResult.newId) {
1818
- result2.idChanged = true;
1819
- result2.newId = updateResult.newId;
1820
- }
1821
- return result2;
1822
- }
1823
- await library.save();
1824
- const result = { updated: true };
1825
- if (updateResult.item) {
1826
- result.item = updateResult.item;
1827
- }
1828
- if (updateResult.oldItem) {
1829
- result.oldItem = updateResult.oldItem;
1830
- }
1831
- if (updateResult.idChanged && updateResult.newId) {
1832
- result.idChanged = true;
1833
- result.newId = updateResult.newId;
1834
- }
1835
- return result;
1836
- }
1837
- function createReferencesRoute(library) {
1838
- const route = new Hono();
1839
- route.get("/", async (c) => {
1840
- const items = await library.getAll();
1841
- return c.json(items);
1842
- });
1843
- route.get("/uuid/:uuid", async (c) => {
1844
- const uuid = c.req.param("uuid");
1845
- const item = await library.find(uuid, { idType: "uuid" });
1846
- if (!item) {
1847
- return c.json({ error: "Reference not found" }, 404);
1848
- }
1849
- return c.json(item);
1850
- });
1851
- route.get("/id/:id", async (c) => {
1852
- const id = c.req.param("id");
1853
- const item = await library.find(id);
1854
- if (!item) {
1855
- return c.json({ error: "Reference not found" }, 404);
1856
- }
1857
- return c.json(item);
1858
- });
1859
- route.post("/", async (c) => {
1860
- try {
1861
- const body = await c.req.json();
1862
- const addedItem = await library.add(body);
1863
- return c.json(addedItem, 201);
1864
- } catch (error) {
1865
- return c.json(
1866
- {
1867
- error: "Invalid request body",
1868
- details: error instanceof Error ? error.message : String(error)
1869
- },
1870
- 400
1871
- );
1872
- }
1873
- });
1874
- route.put("/uuid/:uuid", async (c) => {
1875
- const uuid = c.req.param("uuid");
1876
- let body;
1877
- try {
1878
- body = await c.req.json();
1879
- } catch {
1880
- return c.json({ error: "Invalid JSON" }, 400);
1881
- }
1882
- if (!body || typeof body !== "object") {
1883
- return c.json({ error: "Request body must be an object" }, 400);
1884
- }
1885
- const { updates, onIdCollision } = body;
1886
- if (!updates || typeof updates !== "object") {
1887
- return c.json({ error: "Request body must contain 'updates' object" }, 400);
1888
- }
1889
- const result = await updateReference(library, {
1890
- identifier: uuid,
1891
- idType: "uuid",
1892
- updates,
1893
- onIdCollision: onIdCollision ?? "suffix"
1894
- });
1895
- if (!result.updated) {
1896
- const status = result.errorType === "id_collision" ? 409 : 404;
1897
- return c.json(result, status);
1898
- }
1899
- return c.json(result);
1900
- });
1901
- route.put("/id/:id", async (c) => {
1902
- const id = c.req.param("id");
1903
- let body;
1904
- try {
1905
- body = await c.req.json();
1906
- } catch {
1907
- return c.json({ error: "Invalid JSON" }, 400);
1908
- }
1909
- if (!body || typeof body !== "object") {
1910
- return c.json({ error: "Request body must be an object" }, 400);
1911
- }
1912
- const { updates, onIdCollision } = body;
1913
- if (!updates || typeof updates !== "object") {
1914
- return c.json({ error: "Request body must contain 'updates' object" }, 400);
1915
- }
1916
- const result = await updateReference(library, {
1917
- identifier: id,
1918
- updates,
1919
- onIdCollision: onIdCollision ?? "suffix"
1920
- });
1921
- if (!result.updated) {
1922
- const status = result.errorType === "id_collision" ? 409 : 404;
1923
- return c.json(result, status);
1924
- }
1925
- return c.json(result);
1926
- });
1927
- route.delete("/uuid/:uuid", async (c) => {
1928
- const uuid = c.req.param("uuid");
1929
- const result = await removeReference(library, {
1930
- identifier: uuid,
1931
- idType: "uuid"
1932
- });
1933
- if (!result.removed) {
1934
- return c.json(result, 404);
1935
- }
1936
- return c.json(result);
1937
- });
1938
- route.delete("/id/:id", async (c) => {
1939
- const id = c.req.param("id");
1940
- const result = await removeReference(library, {
1941
- identifier: id
1942
- });
1943
- if (!result.removed) {
1944
- return c.json(result, 404);
1945
- }
1946
- return c.json(result);
1947
- });
1948
- return route;
1949
- }
1950
- async function searchReferences(library, options) {
1951
- const query = options.query;
1952
- const sort = options.sort ?? "updated";
1953
- const order = options.order ?? "desc";
1954
- const limit = options.limit ?? 0;
1955
- const offset = options.offset ?? 0;
1956
- const allItems = await library.getAll();
1957
- let matchedItems;
1958
- if (!query.trim()) {
1959
- matchedItems = allItems;
1960
- } else {
1961
- const tokens = tokenize(query).tokens;
1962
- const results = search$1(allItems, tokens);
1963
- if (sort === "relevance") {
1964
- const sorted2 = sortResults(results);
1965
- matchedItems = order === "desc" ? sorted2.map((r) => r.reference) : sorted2.map((r) => r.reference).reverse();
1966
- } else {
1967
- matchedItems = results.map((r) => r.reference);
1968
- }
1969
- }
1970
- const total = matchedItems.length;
1971
- let sorted;
1972
- if (sort === "relevance") {
1973
- sorted = matchedItems;
1974
- } else {
1975
- sorted = sortReferences(matchedItems, sort, order);
1976
- }
1977
- const { items: paginatedItems, nextOffset } = paginate(sorted, { limit, offset });
1978
- return {
1979
- items: paginatedItems,
1980
- total,
1981
- limit,
1982
- offset,
1983
- nextOffset
1984
- };
1985
- }
1986
- const search = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1987
- __proto__: null,
1988
- searchReferences
1989
- }, Symbol.toStringTag, { value: "Module" }));
1990
- const searchRequestBodySchema = z.object({
1991
- query: z.string(),
1992
- sort: searchSortFieldSchema.optional(),
1993
- order: sortOrderSchema.optional(),
1994
- limit: z.number().int().min(0).optional(),
1995
- offset: z.number().int().min(0).optional()
1996
- });
1997
- function createSearchRoute(library) {
1998
- const route = new Hono();
1999
- route.post("/", async (c) => {
2000
- let body;
2001
- try {
2002
- body = await c.req.json();
2003
- } catch {
2004
- return c.json({ error: "Invalid JSON" }, 400);
2005
- }
2006
- const parseResult = searchRequestBodySchema.safeParse(body);
2007
- if (!parseResult.success) {
2008
- const errorMessage = parseResult.error.issues[0]?.message ?? "Invalid request body";
2009
- return c.json({ error: errorMessage }, 400);
2010
- }
2011
- const requestBody = parseResult.data;
2012
- const options = {
2013
- query: requestBody.query,
2014
- ...pickDefined(requestBody, ["sort", "order", "limit", "offset"])
2015
- };
2016
- const result = await searchReferences(library, options);
2017
- return c.json(result);
2018
- });
2019
- return route;
2020
- }
2021
- function createServer(library, config) {
2022
- const app = new Hono();
2023
- app.route("/health", healthRoute);
2024
- const referencesRoute = createReferencesRoute(library);
2025
- app.route("/api/references", referencesRoute);
2026
- const addRoute = createAddRoute(library, config);
2027
- app.route("/api/add", addRoute);
2028
- const citeRoute = createCiteRoute(library);
2029
- app.route("/api/cite", citeRoute);
2030
- const listRoute = createListRoute(library);
2031
- app.route("/api/list", listRoute);
2032
- const searchRoute = createSearchRoute(library);
2033
- app.route("/api/search", searchRoute);
2034
- return app;
2035
- }
2036
- async function startServerWithFileWatcher(libraryPath, config) {
2037
- const library = await Library.load(libraryPath);
2038
- const app = createServer(library, config);
2039
- const fileWatcher = new FileWatcher(libraryPath, {
2040
- debounceMs: config.watch.debounceMs,
2041
- maxRetries: config.watch.maxRetries,
2042
- retryDelayMs: config.watch.retryIntervalMs,
2043
- pollIntervalMs: config.watch.pollIntervalMs
2044
- });
2045
- fileWatcher.on("change", () => {
2046
- library.reload().catch((error) => {
2047
- console.error("Failed to reload library:", error);
2048
- });
2049
- });
2050
- await fileWatcher.start();
2051
- const dispose = async () => {
2052
- fileWatcher.close();
2053
- };
2054
- return {
2055
- app,
2056
- library,
2057
- fileWatcher,
2058
- dispose
2059
- };
2060
- }
2061
- export {
2062
- BUILTIN_STYLES as B,
2063
- FULLTEXT_ROLE as F,
2064
- RESERVED_ROLES as R,
2065
- isReservedRole as a,
2066
- formatToExtension as b,
2067
- findFulltextFiles as c,
2068
- findFulltextFile as d,
2069
- extensionToFormat as e,
2070
- formatBibliographyCSL as f,
2071
- getExtension as g,
2072
- getFulltextAttachmentTypes as h,
2073
- isValidFulltextFiles as i,
2074
- createServer as j,
2075
- add as k,
2076
- cite as l,
2077
- list as m,
2078
- search as n,
2079
- remove as r,
2080
- startServerWithFileWatcher as s
2081
- };
2082
- //# sourceMappingURL=index-DrZawbND.js.map