@ncukondo/reference-manager 0.1.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 (117) hide show
  1. package/README.md +167 -0
  2. package/bin/reference-manager.js +5 -0
  3. package/dist/chunks/detector-BF8Mcc72.js +1415 -0
  4. package/dist/chunks/detector-BF8Mcc72.js.map +1 -0
  5. package/dist/cli/commands/add.d.ts +22 -0
  6. package/dist/cli/commands/add.d.ts.map +1 -0
  7. package/dist/cli/commands/index.d.ts +16 -0
  8. package/dist/cli/commands/index.d.ts.map +1 -0
  9. package/dist/cli/commands/list.d.ts +15 -0
  10. package/dist/cli/commands/list.d.ts.map +1 -0
  11. package/dist/cli/commands/remove.d.ts +19 -0
  12. package/dist/cli/commands/remove.d.ts.map +1 -0
  13. package/dist/cli/commands/search.d.ts +16 -0
  14. package/dist/cli/commands/search.d.ts.map +1 -0
  15. package/dist/cli/commands/server.d.ts +32 -0
  16. package/dist/cli/commands/server.d.ts.map +1 -0
  17. package/dist/cli/commands/update.d.ts +20 -0
  18. package/dist/cli/commands/update.d.ts.map +1 -0
  19. package/dist/cli/helpers.d.ts +61 -0
  20. package/dist/cli/helpers.d.ts.map +1 -0
  21. package/dist/cli/index.d.ts +13 -0
  22. package/dist/cli/index.d.ts.map +1 -0
  23. package/dist/cli/output/bibtex.d.ts +6 -0
  24. package/dist/cli/output/bibtex.d.ts.map +1 -0
  25. package/dist/cli/output/index.d.ts +7 -0
  26. package/dist/cli/output/index.d.ts.map +1 -0
  27. package/dist/cli/output/json.d.ts +6 -0
  28. package/dist/cli/output/json.d.ts.map +1 -0
  29. package/dist/cli/output/pretty.d.ts +6 -0
  30. package/dist/cli/output/pretty.d.ts.map +1 -0
  31. package/dist/cli/server-client.d.ts +38 -0
  32. package/dist/cli/server-client.d.ts.map +1 -0
  33. package/dist/cli/server-detection.d.ts +27 -0
  34. package/dist/cli/server-detection.d.ts.map +1 -0
  35. package/dist/cli.js +981 -0
  36. package/dist/cli.js.map +1 -0
  37. package/dist/config/defaults.d.ts +29 -0
  38. package/dist/config/defaults.d.ts.map +1 -0
  39. package/dist/config/index.d.ts +10 -0
  40. package/dist/config/index.d.ts.map +1 -0
  41. package/dist/config/loader.d.ts +27 -0
  42. package/dist/config/loader.d.ts.map +1 -0
  43. package/dist/config/schema.d.ts +129 -0
  44. package/dist/config/schema.d.ts.map +1 -0
  45. package/dist/core/csl-json/parser.d.ts +9 -0
  46. package/dist/core/csl-json/parser.d.ts.map +1 -0
  47. package/dist/core/csl-json/serializer.d.ts +15 -0
  48. package/dist/core/csl-json/serializer.d.ts.map +1 -0
  49. package/dist/core/csl-json/types.d.ts +124 -0
  50. package/dist/core/csl-json/types.d.ts.map +1 -0
  51. package/dist/core/csl-json/validator.d.ts +19 -0
  52. package/dist/core/csl-json/validator.d.ts.map +1 -0
  53. package/dist/core/identifier/generator.d.ts +17 -0
  54. package/dist/core/identifier/generator.d.ts.map +1 -0
  55. package/dist/core/identifier/normalize.d.ts +20 -0
  56. package/dist/core/identifier/normalize.d.ts.map +1 -0
  57. package/dist/core/identifier/uuid.d.ts +24 -0
  58. package/dist/core/identifier/uuid.d.ts.map +1 -0
  59. package/dist/core/index.d.ts +15 -0
  60. package/dist/core/index.d.ts.map +1 -0
  61. package/dist/core/library.d.ts +73 -0
  62. package/dist/core/library.d.ts.map +1 -0
  63. package/dist/core/reference.d.ts +86 -0
  64. package/dist/core/reference.d.ts.map +1 -0
  65. package/dist/features/duplicate/detector.d.ts +19 -0
  66. package/dist/features/duplicate/detector.d.ts.map +1 -0
  67. package/dist/features/duplicate/index.d.ts +6 -0
  68. package/dist/features/duplicate/index.d.ts.map +1 -0
  69. package/dist/features/duplicate/types.d.ts +45 -0
  70. package/dist/features/duplicate/types.d.ts.map +1 -0
  71. package/dist/features/file-watcher/file-watcher.d.ts +83 -0
  72. package/dist/features/file-watcher/file-watcher.d.ts.map +1 -0
  73. package/dist/features/file-watcher/index.d.ts +2 -0
  74. package/dist/features/file-watcher/index.d.ts.map +1 -0
  75. package/dist/features/merge/index.d.ts +8 -0
  76. package/dist/features/merge/index.d.ts.map +1 -0
  77. package/dist/features/merge/three-way.d.ts +16 -0
  78. package/dist/features/merge/three-way.d.ts.map +1 -0
  79. package/dist/features/merge/types.d.ts +74 -0
  80. package/dist/features/merge/types.d.ts.map +1 -0
  81. package/dist/features/search/index.d.ts +9 -0
  82. package/dist/features/search/index.d.ts.map +1 -0
  83. package/dist/features/search/matcher.d.ts +18 -0
  84. package/dist/features/search/matcher.d.ts.map +1 -0
  85. package/dist/features/search/normalizer.d.ts +12 -0
  86. package/dist/features/search/normalizer.d.ts.map +1 -0
  87. package/dist/features/search/sorter.d.ts +11 -0
  88. package/dist/features/search/sorter.d.ts.map +1 -0
  89. package/dist/features/search/tokenizer.d.ts +6 -0
  90. package/dist/features/search/tokenizer.d.ts.map +1 -0
  91. package/dist/features/search/types.d.ts +77 -0
  92. package/dist/features/search/types.d.ts.map +1 -0
  93. package/dist/index.d.ts +13 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +559 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/server/index.d.ts +9 -0
  98. package/dist/server/index.d.ts.map +1 -0
  99. package/dist/server/portfile.d.ts +43 -0
  100. package/dist/server/portfile.d.ts.map +1 -0
  101. package/dist/server/routes/health.d.ts +7 -0
  102. package/dist/server/routes/health.d.ts.map +1 -0
  103. package/dist/server/routes/references.d.ts +9 -0
  104. package/dist/server/routes/references.d.ts.map +1 -0
  105. package/dist/server.js +91 -0
  106. package/dist/server.js.map +1 -0
  107. package/dist/utils/backup.d.ts +21 -0
  108. package/dist/utils/backup.d.ts.map +1 -0
  109. package/dist/utils/file.d.ts +9 -0
  110. package/dist/utils/file.d.ts.map +1 -0
  111. package/dist/utils/hash.d.ts +9 -0
  112. package/dist/utils/hash.d.ts.map +1 -0
  113. package/dist/utils/index.d.ts +5 -0
  114. package/dist/utils/index.d.ts.map +1 -0
  115. package/dist/utils/logger.d.ts +8 -0
  116. package/dist/utils/logger.d.ts.map +1 -0
  117. package/package.json +72 -0
@@ -0,0 +1,1415 @@
1
+ import { randomUUID, createHash } from "node:crypto";
2
+ import { createReadStream, existsSync, readFileSync } from "node:fs";
3
+ import { readFile, mkdir, writeFile } from "node:fs/promises";
4
+ import { z } from "zod";
5
+ import { dirname, join } from "node:path";
6
+ import { parse } from "@iarna/toml";
7
+ import { tmpdir, homedir } from "node:os";
8
+ function normalizeText(text) {
9
+ return text.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_|_$/g, "");
10
+ }
11
+ function normalizeAuthorName(name) {
12
+ const normalized = normalizeText(name);
13
+ return normalized.slice(0, 32);
14
+ }
15
+ function normalizeTitleSlug(title) {
16
+ const normalized = normalizeText(title);
17
+ return normalized.slice(0, 32);
18
+ }
19
+ function extractAuthorName(item) {
20
+ if (!item.author || item.author.length === 0) {
21
+ return "";
22
+ }
23
+ const firstAuthor = item.author[0];
24
+ if (!firstAuthor) {
25
+ return "";
26
+ }
27
+ if (firstAuthor.family) {
28
+ return normalizeAuthorName(firstAuthor.family);
29
+ }
30
+ if (firstAuthor.literal) {
31
+ return normalizeAuthorName(firstAuthor.literal);
32
+ }
33
+ return "";
34
+ }
35
+ function extractYear$3(item) {
36
+ if (!item.issued || !item.issued["date-parts"] || item.issued["date-parts"].length === 0) {
37
+ return "";
38
+ }
39
+ const dateParts = item.issued["date-parts"][0];
40
+ if (!dateParts || dateParts.length === 0) {
41
+ return "";
42
+ }
43
+ const year = dateParts[0];
44
+ return year ? year.toString() : "";
45
+ }
46
+ function determineTitlePart(hasAuthor, hasYear, title) {
47
+ if (hasAuthor && hasYear) {
48
+ return "";
49
+ }
50
+ if (title) {
51
+ return `-${title}`;
52
+ }
53
+ if (!hasAuthor && !hasYear) {
54
+ return "-untitled";
55
+ }
56
+ return "";
57
+ }
58
+ function generateId(item) {
59
+ const author = extractAuthorName(item);
60
+ const year = extractYear$3(item);
61
+ const title = item.title ? normalizeTitleSlug(item.title) : "";
62
+ const authorPart = author || "anon";
63
+ const yearPart = year || "nd";
64
+ const titlePart = determineTitlePart(Boolean(author), Boolean(year), title);
65
+ return `${authorPart}-${yearPart}${titlePart}`;
66
+ }
67
+ function generateSuffix(index) {
68
+ if (index === 0) {
69
+ return "";
70
+ }
71
+ let suffix = "";
72
+ let num = index;
73
+ while (num > 0) {
74
+ num--;
75
+ suffix = String.fromCharCode(97 + num % 26) + suffix;
76
+ num = Math.floor(num / 26);
77
+ }
78
+ return suffix;
79
+ }
80
+ function generateIdWithCollisionCheck(item, existingIds) {
81
+ const baseId = generateId(item);
82
+ const normalizedExistingIds = existingIds.map((id) => id.toLowerCase());
83
+ let candidate = baseId;
84
+ let suffixIndex = 0;
85
+ while (normalizedExistingIds.includes(candidate.toLowerCase())) {
86
+ suffixIndex++;
87
+ const suffix = generateSuffix(suffixIndex);
88
+ candidate = `${baseId}${suffix}`;
89
+ }
90
+ return candidate;
91
+ }
92
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
93
+ function isValidUuid(uuid) {
94
+ return UUID_REGEX.test(uuid);
95
+ }
96
+ function generateUuid() {
97
+ return randomUUID();
98
+ }
99
+ function generateTimestamp() {
100
+ return (/* @__PURE__ */ new Date()).toISOString();
101
+ }
102
+ function extractUuidFromCustom(custom) {
103
+ if (!custom || !custom.uuid) {
104
+ return null;
105
+ }
106
+ return isValidUuid(custom.uuid) ? custom.uuid : null;
107
+ }
108
+ function ensureUuid(custom) {
109
+ const existingUuid = extractUuidFromCustom(custom);
110
+ if (existingUuid && custom) {
111
+ return custom;
112
+ }
113
+ const newUuid = generateUuid();
114
+ return {
115
+ ...custom,
116
+ uuid: newUuid
117
+ };
118
+ }
119
+ function ensureCreatedAt(custom) {
120
+ if (custom.created_at) {
121
+ return custom;
122
+ }
123
+ if (custom.timestamp) {
124
+ return {
125
+ ...custom,
126
+ created_at: custom.timestamp
127
+ };
128
+ }
129
+ const newTimestamp = generateTimestamp();
130
+ return {
131
+ ...custom,
132
+ created_at: newTimestamp
133
+ };
134
+ }
135
+ function ensureTimestamp(custom) {
136
+ if (custom.timestamp) {
137
+ return custom;
138
+ }
139
+ return {
140
+ ...custom,
141
+ timestamp: custom.created_at
142
+ };
143
+ }
144
+ function ensureCustomMetadata(custom) {
145
+ const withUuid = ensureUuid(custom);
146
+ const withCreatedAt = ensureCreatedAt(withUuid);
147
+ const withTimestamp = ensureTimestamp(withCreatedAt);
148
+ return withTimestamp;
149
+ }
150
+ class Reference {
151
+ item;
152
+ uuid;
153
+ constructor(item) {
154
+ const customMetadata = ensureCustomMetadata(item.custom);
155
+ this.item = { ...item, custom: customMetadata };
156
+ const extractedUuid = extractUuidFromCustom(customMetadata);
157
+ if (!extractedUuid) {
158
+ throw new Error("Failed to extract UUID after ensureCustomMetadata");
159
+ }
160
+ this.uuid = extractedUuid;
161
+ }
162
+ /**
163
+ * Factory method to create a Reference with UUID and ID generation
164
+ */
165
+ static create(item, options) {
166
+ const existingIds = options?.existingIds || /* @__PURE__ */ new Set();
167
+ let updatedItem = item;
168
+ if (!item.id || item.id.trim() === "") {
169
+ const generatedId = generateIdWithCollisionCheck(item, Array.from(existingIds));
170
+ updatedItem = { ...item, id: generatedId };
171
+ }
172
+ return new Reference(updatedItem);
173
+ }
174
+ /**
175
+ * Get the underlying CSL-JSON item
176
+ */
177
+ getItem() {
178
+ return this.item;
179
+ }
180
+ /**
181
+ * Get the UUID (internal stable identifier)
182
+ */
183
+ getUuid() {
184
+ return this.uuid;
185
+ }
186
+ /**
187
+ * Get the ID (Pandoc citation key / BibTeX-key)
188
+ */
189
+ getId() {
190
+ return this.item.id;
191
+ }
192
+ /**
193
+ * Get the title
194
+ */
195
+ getTitle() {
196
+ return this.item.title;
197
+ }
198
+ /**
199
+ * Get the authors
200
+ */
201
+ getAuthors() {
202
+ return this.item.author;
203
+ }
204
+ /**
205
+ * Get the year from issued date
206
+ */
207
+ getYear() {
208
+ const issued = this.item.issued;
209
+ if (!issued || !issued["date-parts"] || issued["date-parts"].length === 0) {
210
+ return void 0;
211
+ }
212
+ const firstDate = issued["date-parts"][0];
213
+ return firstDate && firstDate.length > 0 ? firstDate[0] : void 0;
214
+ }
215
+ /**
216
+ * Get the DOI
217
+ */
218
+ getDoi() {
219
+ return this.item.DOI;
220
+ }
221
+ /**
222
+ * Get the PMID
223
+ */
224
+ getPmid() {
225
+ return this.item.PMID;
226
+ }
227
+ /**
228
+ * Get the PMCID
229
+ */
230
+ getPmcid() {
231
+ return this.item.PMCID;
232
+ }
233
+ /**
234
+ * Get the URL
235
+ */
236
+ getUrl() {
237
+ return this.item.URL;
238
+ }
239
+ /**
240
+ * Get the keyword
241
+ */
242
+ getKeyword() {
243
+ return this.item.keyword;
244
+ }
245
+ /**
246
+ * Get additional URLs from custom metadata
247
+ */
248
+ getAdditionalUrls() {
249
+ return this.item.custom?.additional_urls;
250
+ }
251
+ /**
252
+ * Get the creation timestamp from custom metadata (immutable)
253
+ */
254
+ getCreatedAt() {
255
+ if (!this.item.custom?.created_at) {
256
+ throw new Error("created_at is missing from custom metadata");
257
+ }
258
+ return this.item.custom.created_at;
259
+ }
260
+ /**
261
+ * Get the last modification timestamp from custom metadata
262
+ */
263
+ getTimestamp() {
264
+ if (!this.item.custom?.timestamp) {
265
+ throw new Error("timestamp is missing from custom metadata");
266
+ }
267
+ return this.item.custom.timestamp;
268
+ }
269
+ /**
270
+ * Update the timestamp to current time
271
+ * Call this whenever the reference is modified
272
+ */
273
+ touch() {
274
+ if (!this.item.custom) {
275
+ throw new Error("custom metadata is missing");
276
+ }
277
+ this.item.custom.timestamp = (/* @__PURE__ */ new Date()).toISOString();
278
+ }
279
+ /**
280
+ * Get the type
281
+ */
282
+ getType() {
283
+ return this.item.type;
284
+ }
285
+ }
286
+ function computeHash(input) {
287
+ return createHash("sha256").update(input, "utf-8").digest("hex");
288
+ }
289
+ async function computeFileHash(filePath) {
290
+ return new Promise((resolve, reject) => {
291
+ const hash = createHash("sha256");
292
+ const stream = createReadStream(filePath);
293
+ stream.on("data", (chunk) => hash.update(chunk));
294
+ stream.on("end", () => resolve(hash.digest("hex")));
295
+ stream.on("error", (error) => reject(error));
296
+ });
297
+ }
298
+ const CslNameSchema = z.object({
299
+ family: z.string().optional(),
300
+ given: z.string().optional(),
301
+ literal: z.string().optional(),
302
+ "dropping-particle": z.string().optional(),
303
+ "non-dropping-particle": z.string().optional(),
304
+ suffix: z.string().optional()
305
+ });
306
+ const CslDateSchema = z.object({
307
+ "date-parts": z.array(z.array(z.number())).optional(),
308
+ raw: z.string().optional(),
309
+ season: z.string().optional(),
310
+ circa: z.boolean().optional(),
311
+ literal: z.string().optional()
312
+ });
313
+ const CslCustomSchema = z.object({
314
+ uuid: z.string(),
315
+ created_at: z.string(),
316
+ timestamp: z.string(),
317
+ additional_urls: z.array(z.string()).optional()
318
+ });
319
+ const CslItemSchema = z.object({
320
+ id: z.string(),
321
+ type: z.string(),
322
+ title: z.string().optional(),
323
+ author: z.array(CslNameSchema).optional(),
324
+ editor: z.array(CslNameSchema).optional(),
325
+ issued: CslDateSchema.optional(),
326
+ accessed: CslDateSchema.optional(),
327
+ "container-title": z.string().optional(),
328
+ volume: z.string().optional(),
329
+ issue: z.string().optional(),
330
+ page: z.string().optional(),
331
+ DOI: z.string().optional(),
332
+ PMID: z.string().optional(),
333
+ PMCID: z.string().optional(),
334
+ ISBN: z.string().optional(),
335
+ ISSN: z.string().optional(),
336
+ URL: z.string().optional(),
337
+ abstract: z.string().optional(),
338
+ publisher: z.string().optional(),
339
+ "publisher-place": z.string().optional(),
340
+ note: z.string().optional(),
341
+ keyword: z.array(z.string()).optional(),
342
+ custom: CslCustomSchema.optional()
343
+ // Allow additional fields
344
+ }).passthrough();
345
+ const CslLibrarySchema = z.array(CslItemSchema);
346
+ function parseKeyword(keyword) {
347
+ if (typeof keyword !== "string") {
348
+ return void 0;
349
+ }
350
+ if (keyword.trim() === "") {
351
+ return void 0;
352
+ }
353
+ const keywords = keyword.split(";").map((k) => k.trim()).filter((k) => k !== "");
354
+ return keywords.length > 0 ? keywords : void 0;
355
+ }
356
+ async function parseCslJson(filePath) {
357
+ const content = await readFile(filePath, "utf-8");
358
+ let rawData;
359
+ try {
360
+ rawData = JSON.parse(content);
361
+ } catch (error) {
362
+ throw new Error(
363
+ `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`
364
+ );
365
+ }
366
+ if (Array.isArray(rawData)) {
367
+ rawData = rawData.map((item) => {
368
+ if (item && typeof item === "object" && "keyword" in item) {
369
+ const itemWithKeyword = item;
370
+ return {
371
+ ...itemWithKeyword,
372
+ keyword: parseKeyword(itemWithKeyword.keyword)
373
+ };
374
+ }
375
+ return item;
376
+ });
377
+ }
378
+ const parseResult = CslLibrarySchema.safeParse(rawData);
379
+ if (!parseResult.success) {
380
+ throw new Error(`Invalid CSL-JSON structure: ${parseResult.error.message}`);
381
+ }
382
+ const library = parseResult.data;
383
+ const processedLibrary = library.map((item) => {
384
+ const updatedCustom = ensureCustomMetadata(item.custom);
385
+ return {
386
+ ...item,
387
+ custom: updatedCustom
388
+ };
389
+ });
390
+ return processedLibrary;
391
+ }
392
+ function serializeKeyword(keywords) {
393
+ if (!keywords || keywords.length === 0) {
394
+ return void 0;
395
+ }
396
+ return keywords.join("; ");
397
+ }
398
+ function serializeCslJson(library) {
399
+ const libraryForJson = library.map((item) => {
400
+ const { keyword, ...rest } = item;
401
+ const serializedKeyword = serializeKeyword(keyword);
402
+ if (serializedKeyword === void 0) {
403
+ return rest;
404
+ }
405
+ return {
406
+ ...rest,
407
+ keyword: serializedKeyword
408
+ };
409
+ });
410
+ return JSON.stringify(libraryForJson, null, 2);
411
+ }
412
+ async function writeCslJson(filePath, library) {
413
+ const dir = dirname(filePath);
414
+ await mkdir(dir, { recursive: true });
415
+ const content = serializeCslJson(library);
416
+ await writeFile(filePath, content, "utf-8");
417
+ }
418
+ class Library {
419
+ filePath;
420
+ references = [];
421
+ currentHash = null;
422
+ // Indices for fast lookup
423
+ uuidIndex = /* @__PURE__ */ new Map();
424
+ idIndex = /* @__PURE__ */ new Map();
425
+ doiIndex = /* @__PURE__ */ new Map();
426
+ pmidIndex = /* @__PURE__ */ new Map();
427
+ constructor(filePath, items) {
428
+ this.filePath = filePath;
429
+ for (const item of items) {
430
+ const ref = new Reference(item);
431
+ this.references.push(ref);
432
+ this.addToIndices(ref);
433
+ }
434
+ }
435
+ /**
436
+ * Load library from file
437
+ */
438
+ static async load(filePath) {
439
+ const items = await parseCslJson(filePath);
440
+ const library = new Library(filePath, items);
441
+ library.currentHash = await computeFileHash(filePath);
442
+ return library;
443
+ }
444
+ /**
445
+ * Save library to file
446
+ */
447
+ async save() {
448
+ const items = this.references.map((ref) => ref.getItem());
449
+ await writeCslJson(this.filePath, items);
450
+ this.currentHash = await computeFileHash(this.filePath);
451
+ }
452
+ /**
453
+ * Add a reference to the library
454
+ */
455
+ add(item) {
456
+ const existingIds = new Set(this.references.map((ref2) => ref2.getId()));
457
+ const ref = Reference.create(item, { existingIds });
458
+ this.references.push(ref);
459
+ this.addToIndices(ref);
460
+ }
461
+ /**
462
+ * Remove a reference by UUID
463
+ */
464
+ removeByUuid(uuid) {
465
+ const ref = this.uuidIndex.get(uuid);
466
+ if (!ref) {
467
+ return false;
468
+ }
469
+ return this.removeReference(ref);
470
+ }
471
+ /**
472
+ * Remove a reference by ID
473
+ */
474
+ removeById(id) {
475
+ const ref = this.idIndex.get(id);
476
+ if (!ref) {
477
+ return false;
478
+ }
479
+ return this.removeReference(ref);
480
+ }
481
+ /**
482
+ * Find a reference by UUID
483
+ */
484
+ findByUuid(uuid) {
485
+ return this.uuidIndex.get(uuid);
486
+ }
487
+ /**
488
+ * Find a reference by ID
489
+ */
490
+ findById(id) {
491
+ return this.idIndex.get(id);
492
+ }
493
+ /**
494
+ * Find a reference by DOI
495
+ */
496
+ findByDoi(doi) {
497
+ return this.doiIndex.get(doi);
498
+ }
499
+ /**
500
+ * Find a reference by PMID
501
+ */
502
+ findByPmid(pmid) {
503
+ return this.pmidIndex.get(pmid);
504
+ }
505
+ /**
506
+ * Get all references
507
+ */
508
+ getAll() {
509
+ return [...this.references];
510
+ }
511
+ /**
512
+ * Get the file path
513
+ */
514
+ getFilePath() {
515
+ return this.filePath;
516
+ }
517
+ /**
518
+ * Get the current file hash
519
+ * Returns null if the library has not been loaded or saved yet
520
+ */
521
+ getCurrentHash() {
522
+ return this.currentHash;
523
+ }
524
+ /**
525
+ * Add reference to all indices
526
+ */
527
+ addToIndices(ref) {
528
+ this.uuidIndex.set(ref.getUuid(), ref);
529
+ this.idIndex.set(ref.getId(), ref);
530
+ const doi = ref.getDoi();
531
+ if (doi) {
532
+ this.doiIndex.set(doi, ref);
533
+ }
534
+ const pmid = ref.getPmid();
535
+ if (pmid) {
536
+ this.pmidIndex.set(pmid, ref);
537
+ }
538
+ }
539
+ /**
540
+ * Remove reference from all indices and array
541
+ */
542
+ removeReference(ref) {
543
+ const index = this.references.indexOf(ref);
544
+ if (index === -1) {
545
+ return false;
546
+ }
547
+ this.references.splice(index, 1);
548
+ this.uuidIndex.delete(ref.getUuid());
549
+ this.idIndex.delete(ref.getId());
550
+ const doi = ref.getDoi();
551
+ if (doi) {
552
+ this.doiIndex.delete(doi);
553
+ }
554
+ const pmid = ref.getPmid();
555
+ if (pmid) {
556
+ this.pmidIndex.delete(pmid);
557
+ }
558
+ return true;
559
+ }
560
+ }
561
+ const logLevelSchema = z.enum(["silent", "info", "debug"]);
562
+ const backupConfigSchema = z.object({
563
+ maxGenerations: z.number().int().positive(),
564
+ maxAgeDays: z.number().int().positive(),
565
+ directory: z.string().min(1)
566
+ });
567
+ const watchConfigSchema = z.object({
568
+ enabled: z.boolean(),
569
+ debounceMs: z.number().int().nonnegative(),
570
+ pollIntervalMs: z.number().int().positive(),
571
+ retryIntervalMs: z.number().int().positive(),
572
+ maxRetries: z.number().int().nonnegative()
573
+ });
574
+ const serverConfigSchema = z.object({
575
+ autoStart: z.boolean(),
576
+ autoStopMinutes: z.number().int().nonnegative()
577
+ });
578
+ const configSchema = z.object({
579
+ library: z.string().min(1),
580
+ logLevel: logLevelSchema,
581
+ backup: backupConfigSchema,
582
+ watch: watchConfigSchema,
583
+ server: serverConfigSchema
584
+ });
585
+ const partialConfigSchema = z.object({
586
+ library: z.string().min(1).optional(),
587
+ logLevel: logLevelSchema.optional(),
588
+ log_level: logLevelSchema.optional(),
589
+ // snake_case support
590
+ backup: z.object({
591
+ maxGenerations: z.number().int().positive().optional(),
592
+ max_generations: z.number().int().positive().optional(),
593
+ maxAgeDays: z.number().int().positive().optional(),
594
+ max_age_days: z.number().int().positive().optional(),
595
+ directory: z.string().min(1).optional()
596
+ }).optional(),
597
+ watch: z.object({
598
+ enabled: z.boolean().optional(),
599
+ debounceMs: z.number().int().nonnegative().optional(),
600
+ debounce_ms: z.number().int().nonnegative().optional(),
601
+ pollIntervalMs: z.number().int().positive().optional(),
602
+ poll_interval_ms: z.number().int().positive().optional(),
603
+ retryIntervalMs: z.number().int().positive().optional(),
604
+ retry_interval_ms: z.number().int().positive().optional(),
605
+ maxRetries: z.number().int().nonnegative().optional(),
606
+ max_retries: z.number().int().nonnegative().optional()
607
+ }).optional(),
608
+ server: z.object({
609
+ autoStart: z.boolean().optional(),
610
+ auto_start: z.boolean().optional(),
611
+ autoStopMinutes: z.number().int().nonnegative().optional(),
612
+ auto_stop_minutes: z.number().int().nonnegative().optional()
613
+ }).optional()
614
+ }).passthrough();
615
+ function normalizeBackupConfig(backup) {
616
+ const normalized = {};
617
+ const maxGenerations = backup.maxGenerations ?? backup.max_generations;
618
+ if (maxGenerations !== void 0) {
619
+ normalized.maxGenerations = maxGenerations;
620
+ }
621
+ const maxAgeDays = backup.maxAgeDays ?? backup.max_age_days;
622
+ if (maxAgeDays !== void 0) {
623
+ normalized.maxAgeDays = maxAgeDays;
624
+ }
625
+ if (backup.directory !== void 0) {
626
+ normalized.directory = backup.directory;
627
+ }
628
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
629
+ }
630
+ function normalizeWatchConfig(watch) {
631
+ const normalized = {};
632
+ if (watch.enabled !== void 0) {
633
+ normalized.enabled = watch.enabled;
634
+ }
635
+ const debounceMs = watch.debounceMs ?? watch.debounce_ms;
636
+ if (debounceMs !== void 0) {
637
+ normalized.debounceMs = debounceMs;
638
+ }
639
+ const pollIntervalMs = watch.pollIntervalMs ?? watch.poll_interval_ms;
640
+ if (pollIntervalMs !== void 0) {
641
+ normalized.pollIntervalMs = pollIntervalMs;
642
+ }
643
+ const retryIntervalMs = watch.retryIntervalMs ?? watch.retry_interval_ms;
644
+ if (retryIntervalMs !== void 0) {
645
+ normalized.retryIntervalMs = retryIntervalMs;
646
+ }
647
+ const maxRetries = watch.maxRetries ?? watch.max_retries;
648
+ if (maxRetries !== void 0) {
649
+ normalized.maxRetries = maxRetries;
650
+ }
651
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
652
+ }
653
+ function normalizeServerConfig(server) {
654
+ const normalized = {};
655
+ const autoStart = server.autoStart ?? server.auto_start;
656
+ if (autoStart !== void 0) {
657
+ normalized.autoStart = autoStart;
658
+ }
659
+ const autoStopMinutes = server.autoStopMinutes ?? server.auto_stop_minutes;
660
+ if (autoStopMinutes !== void 0) {
661
+ normalized.autoStopMinutes = autoStopMinutes;
662
+ }
663
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
664
+ }
665
+ function normalizePartialConfig(partial) {
666
+ const normalized = {};
667
+ if (partial.library !== void 0) {
668
+ normalized.library = partial.library;
669
+ }
670
+ const logLevel = partial.logLevel ?? partial.log_level;
671
+ if (logLevel !== void 0) {
672
+ normalized.logLevel = logLevel;
673
+ }
674
+ if (partial.backup !== void 0) {
675
+ const backup = normalizeBackupConfig(
676
+ partial.backup
677
+ );
678
+ if (backup) {
679
+ normalized.backup = backup;
680
+ }
681
+ }
682
+ if (partial.watch !== void 0) {
683
+ const watch = normalizeWatchConfig(partial.watch);
684
+ if (watch) {
685
+ normalized.watch = watch;
686
+ }
687
+ }
688
+ if (partial.server !== void 0) {
689
+ const server = normalizeServerConfig(
690
+ partial.server
691
+ );
692
+ if (server) {
693
+ normalized.server = server;
694
+ }
695
+ }
696
+ return normalized;
697
+ }
698
+ function getDefaultBackupDirectory() {
699
+ return join(tmpdir(), "reference-manager", "backups");
700
+ }
701
+ function getDefaultLibraryPath() {
702
+ return join(homedir(), ".reference-manager", "csl.library.json");
703
+ }
704
+ function getDefaultUserConfigPath() {
705
+ return join(homedir(), ".reference-manager", "config.toml");
706
+ }
707
+ function getDefaultCurrentDirConfigFilename() {
708
+ return ".reference-manager.config.toml";
709
+ }
710
+ const defaultConfig = {
711
+ library: getDefaultLibraryPath(),
712
+ logLevel: "info",
713
+ backup: {
714
+ maxGenerations: 50,
715
+ maxAgeDays: 365,
716
+ directory: getDefaultBackupDirectory()
717
+ },
718
+ watch: {
719
+ enabled: true,
720
+ debounceMs: 500,
721
+ pollIntervalMs: 5e3,
722
+ retryIntervalMs: 200,
723
+ maxRetries: 10
724
+ },
725
+ server: {
726
+ autoStart: false,
727
+ autoStopMinutes: 0
728
+ }
729
+ };
730
+ function loadTOMLFile(path) {
731
+ if (!existsSync(path)) {
732
+ return null;
733
+ }
734
+ try {
735
+ const content = readFileSync(path, "utf-8");
736
+ const parsed = parse(content);
737
+ const validated = partialConfigSchema.parse(parsed);
738
+ return validated;
739
+ } catch (error) {
740
+ throw new Error(
741
+ `Failed to load config from ${path}: ${error instanceof Error ? error.message : String(error)}`
742
+ );
743
+ }
744
+ }
745
+ function mergeConfigs(base, ...overrides) {
746
+ const result = { ...base };
747
+ for (const override of overrides) {
748
+ if (!override) continue;
749
+ if (override.library !== void 0) {
750
+ result.library = override.library;
751
+ }
752
+ if (override.logLevel !== void 0) {
753
+ result.logLevel = override.logLevel;
754
+ }
755
+ if (override.backup !== void 0) {
756
+ result.backup = {
757
+ ...result.backup,
758
+ ...override.backup
759
+ };
760
+ }
761
+ if (override.watch !== void 0) {
762
+ result.watch = {
763
+ ...result.watch,
764
+ ...override.watch
765
+ };
766
+ }
767
+ if (override.server !== void 0) {
768
+ result.server = {
769
+ ...result.server,
770
+ ...override.server
771
+ };
772
+ }
773
+ }
774
+ return result;
775
+ }
776
+ function fillDefaults(partial) {
777
+ return {
778
+ library: partial.library ?? defaultConfig.library,
779
+ logLevel: partial.logLevel ?? defaultConfig.logLevel,
780
+ backup: {
781
+ maxGenerations: partial.backup?.maxGenerations ?? defaultConfig.backup.maxGenerations,
782
+ maxAgeDays: partial.backup?.maxAgeDays ?? defaultConfig.backup.maxAgeDays,
783
+ directory: partial.backup?.directory ?? defaultConfig.backup.directory
784
+ },
785
+ watch: {
786
+ enabled: partial.watch?.enabled ?? defaultConfig.watch.enabled,
787
+ debounceMs: partial.watch?.debounceMs ?? defaultConfig.watch.debounceMs,
788
+ pollIntervalMs: partial.watch?.pollIntervalMs ?? defaultConfig.watch.pollIntervalMs,
789
+ retryIntervalMs: partial.watch?.retryIntervalMs ?? defaultConfig.watch.retryIntervalMs,
790
+ maxRetries: partial.watch?.maxRetries ?? defaultConfig.watch.maxRetries
791
+ },
792
+ server: {
793
+ autoStart: partial.server?.autoStart ?? defaultConfig.server.autoStart,
794
+ autoStopMinutes: partial.server?.autoStopMinutes ?? defaultConfig.server.autoStopMinutes
795
+ }
796
+ };
797
+ }
798
+ function loadConfig(options = {}) {
799
+ const cwd = options.cwd ?? process.cwd();
800
+ const userConfigPath = options.userConfigPath ?? getDefaultUserConfigPath();
801
+ const userConfig = loadTOMLFile(userConfigPath);
802
+ const envConfigPath = process.env.REFERENCE_MANAGER_CONFIG;
803
+ const envConfig = envConfigPath ? loadTOMLFile(envConfigPath) : null;
804
+ const currentConfigPath = join(cwd, getDefaultCurrentDirConfigFilename());
805
+ const currentConfig = loadTOMLFile(currentConfigPath);
806
+ const normalizedUser = userConfig ? normalizePartialConfig(userConfig) : null;
807
+ const normalizedEnv = envConfig ? normalizePartialConfig(envConfig) : null;
808
+ const normalizedCurrent = currentConfig ? normalizePartialConfig(currentConfig) : null;
809
+ const merged = mergeConfigs(
810
+ {},
811
+ normalizedUser,
812
+ normalizedEnv,
813
+ normalizedCurrent,
814
+ options.overrides
815
+ );
816
+ const config = fillDefaults(merged);
817
+ try {
818
+ return configSchema.parse(config);
819
+ } catch (error) {
820
+ throw new Error(
821
+ `Invalid configuration: ${error instanceof Error ? error.message : String(error)}`
822
+ );
823
+ }
824
+ }
825
+ const VALID_FIELDS = /* @__PURE__ */ new Set([
826
+ "author",
827
+ "title",
828
+ "year",
829
+ "doi",
830
+ "pmid",
831
+ "pmcid",
832
+ "url",
833
+ "keyword"
834
+ ]);
835
+ function isWhitespace(query, index) {
836
+ return /\s/.test(query.charAt(index));
837
+ }
838
+ function isQuote(query, index) {
839
+ return query.charAt(index) === '"';
840
+ }
841
+ function tokenize(query) {
842
+ const tokens = [];
843
+ let i = 0;
844
+ while (i < query.length) {
845
+ if (isWhitespace(query, i)) {
846
+ i++;
847
+ continue;
848
+ }
849
+ const result = parseNextToken(query, i);
850
+ if (result.token) {
851
+ tokens.push(result.token);
852
+ }
853
+ i = result.nextIndex;
854
+ }
855
+ return {
856
+ original: query,
857
+ tokens
858
+ };
859
+ }
860
+ function hasWhitespaceBetween(query, start, end) {
861
+ for (let j = start; j < end; j++) {
862
+ if (isWhitespace(query, j)) {
863
+ return true;
864
+ }
865
+ }
866
+ return false;
867
+ }
868
+ function tryParseFieldValue(query, startIndex) {
869
+ const colonIndex = query.indexOf(":", startIndex);
870
+ if (colonIndex === -1) {
871
+ return null;
872
+ }
873
+ if (hasWhitespaceBetween(query, startIndex, colonIndex)) {
874
+ return null;
875
+ }
876
+ const fieldName = query.substring(startIndex, colonIndex);
877
+ if (!VALID_FIELDS.has(fieldName)) {
878
+ return null;
879
+ }
880
+ const afterColon = colonIndex + 1;
881
+ if (afterColon >= query.length || isWhitespace(query, afterColon)) {
882
+ return { token: null, nextIndex: afterColon };
883
+ }
884
+ if (isQuote(query, afterColon)) {
885
+ const quoteResult = parseQuotedValue(query, afterColon);
886
+ if (quoteResult.value !== null) {
887
+ return {
888
+ token: {
889
+ raw: query.substring(startIndex, quoteResult.nextIndex),
890
+ value: quoteResult.value,
891
+ field: fieldName,
892
+ isPhrase: true
893
+ },
894
+ nextIndex: quoteResult.nextIndex
895
+ };
896
+ }
897
+ return null;
898
+ }
899
+ const valueResult = parseUnquotedValue(query, afterColon);
900
+ return {
901
+ token: {
902
+ raw: query.substring(startIndex, valueResult.nextIndex),
903
+ value: valueResult.value,
904
+ field: fieldName,
905
+ isPhrase: false
906
+ },
907
+ nextIndex: valueResult.nextIndex
908
+ };
909
+ }
910
+ function parseQuotedToken(query, startIndex) {
911
+ const quoteResult = parseQuotedValue(query, startIndex);
912
+ if (quoteResult.value !== null) {
913
+ return {
914
+ token: {
915
+ raw: query.substring(startIndex, quoteResult.nextIndex),
916
+ value: quoteResult.value,
917
+ isPhrase: true
918
+ },
919
+ nextIndex: quoteResult.nextIndex
920
+ };
921
+ }
922
+ if (quoteResult.nextIndex > startIndex) {
923
+ return { token: null, nextIndex: quoteResult.nextIndex };
924
+ }
925
+ const valueResult = parseUnquotedValue(query, startIndex, true);
926
+ return {
927
+ token: {
928
+ raw: valueResult.value,
929
+ value: valueResult.value,
930
+ isPhrase: false
931
+ },
932
+ nextIndex: valueResult.nextIndex
933
+ };
934
+ }
935
+ function parseRegularToken(query, startIndex) {
936
+ const valueResult = parseUnquotedValue(query, startIndex);
937
+ return {
938
+ token: {
939
+ raw: valueResult.value,
940
+ value: valueResult.value,
941
+ isPhrase: false
942
+ },
943
+ nextIndex: valueResult.nextIndex
944
+ };
945
+ }
946
+ function parseNextToken(query, startIndex) {
947
+ const fieldResult = tryParseFieldValue(query, startIndex);
948
+ if (fieldResult !== null) {
949
+ return fieldResult;
950
+ }
951
+ if (isQuote(query, startIndex)) {
952
+ return parseQuotedToken(query, startIndex);
953
+ }
954
+ return parseRegularToken(query, startIndex);
955
+ }
956
+ function parseQuotedValue(query, startIndex) {
957
+ if (!isQuote(query, startIndex)) {
958
+ return { value: null, nextIndex: startIndex };
959
+ }
960
+ let i = startIndex + 1;
961
+ const valueStart = i;
962
+ while (i < query.length && !isQuote(query, i)) {
963
+ i++;
964
+ }
965
+ if (i >= query.length) {
966
+ return { value: null, nextIndex: startIndex };
967
+ }
968
+ const value = query.substring(valueStart, i);
969
+ i++;
970
+ if (value.trim() === "") {
971
+ return { value: null, nextIndex: i };
972
+ }
973
+ return { value, nextIndex: i };
974
+ }
975
+ function parseUnquotedValue(query, startIndex, includeQuotes = false) {
976
+ let i = startIndex;
977
+ while (i < query.length && !isWhitespace(query, i)) {
978
+ if (!includeQuotes && isQuote(query, i)) {
979
+ break;
980
+ }
981
+ i++;
982
+ }
983
+ return {
984
+ value: query.substring(startIndex, i),
985
+ nextIndex: i
986
+ };
987
+ }
988
+ function normalize(text) {
989
+ let normalized = text.normalize("NFKC");
990
+ normalized = normalized.toLowerCase();
991
+ normalized = normalized.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "");
992
+ normalized = normalized.replace(/[^\p{L}\p{N}/\s]/gu, " ");
993
+ normalized = normalized.replace(/\s+/g, " ").trim();
994
+ return normalized;
995
+ }
996
+ const ID_FIELDS = /* @__PURE__ */ new Set(["DOI", "PMID", "PMCID", "URL"]);
997
+ function extractYear$2(reference) {
998
+ if (reference.issued?.["date-parts"]?.[0]?.[0]) {
999
+ return String(reference.issued["date-parts"][0][0]);
1000
+ }
1001
+ return "0000";
1002
+ }
1003
+ function extractAuthors(reference) {
1004
+ if (!reference.author || reference.author.length === 0) {
1005
+ return "";
1006
+ }
1007
+ return reference.author.map((author) => {
1008
+ const family = author.family || "";
1009
+ const givenInitial = author.given ? author.given[0] : "";
1010
+ return givenInitial ? `${family} ${givenInitial}` : family;
1011
+ }).join(" ");
1012
+ }
1013
+ function getFieldValue(reference, field) {
1014
+ if (field === "year") {
1015
+ return extractYear$2(reference);
1016
+ }
1017
+ if (field === "author") {
1018
+ return extractAuthors(reference);
1019
+ }
1020
+ const value = reference[field];
1021
+ if (typeof value === "string") {
1022
+ return value;
1023
+ }
1024
+ if (field.startsWith("custom.")) {
1025
+ const customField = field.substring(7);
1026
+ const customValue = reference.custom?.[customField];
1027
+ if (typeof customValue === "string") {
1028
+ return customValue;
1029
+ }
1030
+ }
1031
+ return null;
1032
+ }
1033
+ function matchUrl(queryValue, reference) {
1034
+ if (reference.URL === queryValue) {
1035
+ return {
1036
+ field: "URL",
1037
+ strength: "exact",
1038
+ value: reference.URL
1039
+ };
1040
+ }
1041
+ const additionalUrls = reference.custom?.additional_urls;
1042
+ if (Array.isArray(additionalUrls)) {
1043
+ for (const url of additionalUrls) {
1044
+ if (typeof url === "string" && url === queryValue) {
1045
+ return {
1046
+ field: "custom.additional_urls",
1047
+ strength: "exact",
1048
+ value: url
1049
+ };
1050
+ }
1051
+ }
1052
+ }
1053
+ return null;
1054
+ }
1055
+ function matchKeyword(queryValue, reference) {
1056
+ if (!reference.keyword || !Array.isArray(reference.keyword)) {
1057
+ return null;
1058
+ }
1059
+ const normalizedQuery = normalize(queryValue);
1060
+ for (const keyword of reference.keyword) {
1061
+ if (typeof keyword === "string") {
1062
+ const normalizedKeyword = normalize(keyword);
1063
+ if (normalizedKeyword.includes(normalizedQuery)) {
1064
+ return {
1065
+ field: "keyword",
1066
+ strength: "partial",
1067
+ value: keyword
1068
+ };
1069
+ }
1070
+ }
1071
+ }
1072
+ return null;
1073
+ }
1074
+ const FIELD_MAP = {
1075
+ author: "author",
1076
+ title: "title",
1077
+ doi: "DOI",
1078
+ pmid: "PMID",
1079
+ pmcid: "PMCID"
1080
+ };
1081
+ function matchYearField(tokenValue, reference) {
1082
+ const year = extractYear$2(reference);
1083
+ if (year === tokenValue) {
1084
+ return {
1085
+ field: "year",
1086
+ strength: "exact",
1087
+ value: year
1088
+ };
1089
+ }
1090
+ return null;
1091
+ }
1092
+ function matchFieldValue(field, tokenValue, reference) {
1093
+ const fieldValue = getFieldValue(reference, field);
1094
+ if (fieldValue === null) {
1095
+ return null;
1096
+ }
1097
+ if (ID_FIELDS.has(field)) {
1098
+ if (fieldValue === tokenValue) {
1099
+ return {
1100
+ field,
1101
+ strength: "exact",
1102
+ value: fieldValue
1103
+ };
1104
+ }
1105
+ return null;
1106
+ }
1107
+ const normalizedFieldValue = normalize(fieldValue);
1108
+ const normalizedQuery = normalize(tokenValue);
1109
+ if (normalizedFieldValue.includes(normalizedQuery)) {
1110
+ return {
1111
+ field,
1112
+ strength: "partial",
1113
+ value: fieldValue
1114
+ };
1115
+ }
1116
+ return null;
1117
+ }
1118
+ function matchSpecificField(token, reference) {
1119
+ const matches = [];
1120
+ const fieldToSearch = token.field;
1121
+ if (fieldToSearch === "url") {
1122
+ const urlMatch = matchUrl(token.value, reference);
1123
+ if (urlMatch) matches.push(urlMatch);
1124
+ return matches;
1125
+ }
1126
+ if (fieldToSearch === "year") {
1127
+ const yearMatch = matchYearField(token.value, reference);
1128
+ if (yearMatch) matches.push(yearMatch);
1129
+ return matches;
1130
+ }
1131
+ if (fieldToSearch === "keyword") {
1132
+ const keywordMatch = matchKeyword(token.value, reference);
1133
+ if (keywordMatch) matches.push(keywordMatch);
1134
+ return matches;
1135
+ }
1136
+ const actualField = FIELD_MAP[fieldToSearch] || fieldToSearch;
1137
+ const match = matchFieldValue(actualField, token.value, reference);
1138
+ if (match) matches.push(match);
1139
+ return matches;
1140
+ }
1141
+ const STANDARD_SEARCH_FIELDS = [
1142
+ "title",
1143
+ "author",
1144
+ "container-title",
1145
+ "publisher",
1146
+ "DOI",
1147
+ "PMID",
1148
+ "PMCID",
1149
+ "abstract"
1150
+ ];
1151
+ function matchSingleField(field, tokenValue, reference) {
1152
+ if (field === "year") {
1153
+ return matchYearField(tokenValue, reference);
1154
+ }
1155
+ if (field === "URL") {
1156
+ return matchUrl(tokenValue, reference);
1157
+ }
1158
+ if (field === "keyword") {
1159
+ return matchKeyword(tokenValue, reference);
1160
+ }
1161
+ return matchFieldValue(field, tokenValue, reference);
1162
+ }
1163
+ function matchAllFields(token, reference) {
1164
+ const matches = [];
1165
+ const specialFields = ["year", "URL", "keyword"];
1166
+ for (const field of specialFields) {
1167
+ const match = matchSingleField(field, token.value, reference);
1168
+ if (match) matches.push(match);
1169
+ }
1170
+ for (const field of STANDARD_SEARCH_FIELDS) {
1171
+ const match = matchFieldValue(field, token.value, reference);
1172
+ if (match) matches.push(match);
1173
+ }
1174
+ return matches;
1175
+ }
1176
+ function matchToken(token, reference) {
1177
+ if (token.field) {
1178
+ return matchSpecificField(token, reference);
1179
+ }
1180
+ return matchAllFields(token, reference);
1181
+ }
1182
+ function matchReference(reference, tokens) {
1183
+ if (tokens.length === 0) {
1184
+ return null;
1185
+ }
1186
+ const tokenMatches = [];
1187
+ let overallStrength = "none";
1188
+ for (const token of tokens) {
1189
+ const matches = matchToken(token, reference);
1190
+ if (matches.length === 0) {
1191
+ return null;
1192
+ }
1193
+ const tokenStrength = matches.some((m) => m.strength === "exact") ? "exact" : "partial";
1194
+ if (tokenStrength === "exact") {
1195
+ overallStrength = "exact";
1196
+ } else if (tokenStrength === "partial" && overallStrength === "none") {
1197
+ overallStrength = "partial";
1198
+ }
1199
+ tokenMatches.push({
1200
+ token,
1201
+ matches
1202
+ });
1203
+ }
1204
+ const score = overallStrength === "exact" ? 100 + tokenMatches.length : 50 + tokenMatches.length;
1205
+ return {
1206
+ reference,
1207
+ tokenMatches,
1208
+ overallStrength,
1209
+ score
1210
+ };
1211
+ }
1212
+ function search(references, tokens) {
1213
+ const results = [];
1214
+ for (const reference of references) {
1215
+ const match = matchReference(reference, tokens);
1216
+ if (match) {
1217
+ results.push(match);
1218
+ }
1219
+ }
1220
+ return results;
1221
+ }
1222
+ function extractYear$1(reference) {
1223
+ if (reference.issued?.["date-parts"]?.[0]?.[0]) {
1224
+ return String(reference.issued["date-parts"][0][0]);
1225
+ }
1226
+ return "0000";
1227
+ }
1228
+ function extractFirstAuthorFamily(reference) {
1229
+ if (!reference.author || reference.author.length === 0) {
1230
+ return "";
1231
+ }
1232
+ return reference.author[0]?.family || "";
1233
+ }
1234
+ function extractTitle(reference) {
1235
+ return reference.title || "";
1236
+ }
1237
+ function compareStrength(a, b) {
1238
+ const strengthOrder = { exact: 2, partial: 1, none: 0 };
1239
+ return strengthOrder[b] - strengthOrder[a];
1240
+ }
1241
+ function compareYear(a, b) {
1242
+ const yearA = extractYear$1(a);
1243
+ const yearB = extractYear$1(b);
1244
+ return Number(yearB) - Number(yearA);
1245
+ }
1246
+ function compareAuthor(a, b) {
1247
+ const authorA = extractFirstAuthorFamily(a).toLowerCase();
1248
+ const authorB = extractFirstAuthorFamily(b).toLowerCase();
1249
+ if (authorA === "" && authorB !== "") return 1;
1250
+ if (authorA !== "" && authorB === "") return -1;
1251
+ return authorA.localeCompare(authorB);
1252
+ }
1253
+ function compareTitle(a, b) {
1254
+ const titleA = extractTitle(a).toLowerCase();
1255
+ const titleB = extractTitle(b).toLowerCase();
1256
+ if (titleA === "" && titleB !== "") return 1;
1257
+ if (titleA !== "" && titleB === "") return -1;
1258
+ return titleA.localeCompare(titleB);
1259
+ }
1260
+ function sortResults(results) {
1261
+ const indexed = results.map((result, index) => ({ result, index }));
1262
+ const sorted = indexed.sort((a, b) => {
1263
+ const strengthDiff = compareStrength(a.result.overallStrength, b.result.overallStrength);
1264
+ if (strengthDiff !== 0) return strengthDiff;
1265
+ const yearDiff = compareYear(a.result.reference, b.result.reference);
1266
+ if (yearDiff !== 0) return yearDiff;
1267
+ const authorDiff = compareAuthor(a.result.reference, b.result.reference);
1268
+ if (authorDiff !== 0) return authorDiff;
1269
+ const titleDiff = compareTitle(a.result.reference, b.result.reference);
1270
+ if (titleDiff !== 0) return titleDiff;
1271
+ return a.index - b.index;
1272
+ });
1273
+ return sorted.map((item) => item.result);
1274
+ }
1275
+ function normalizeDoi(doi) {
1276
+ const normalized = doi.replace(/^https?:\/\/doi\.org\//i, "").replace(/^https?:\/\/dx\.doi\.org\//i, "").replace(/^doi:/i, "");
1277
+ return normalized;
1278
+ }
1279
+ function extractYear(item) {
1280
+ const dateParts = item.issued?.["date-parts"]?.[0];
1281
+ if (!dateParts || dateParts.length === 0) {
1282
+ return null;
1283
+ }
1284
+ return String(dateParts[0]);
1285
+ }
1286
+ function normalizeAuthors(item) {
1287
+ if (!item.author || item.author.length === 0) {
1288
+ return null;
1289
+ }
1290
+ const authorStrings = item.author.map((author) => {
1291
+ const family = author.family || "";
1292
+ const givenInitial = author.given ? author.given.charAt(0) : "";
1293
+ return `${family} ${givenInitial}`.trim();
1294
+ });
1295
+ return normalize(authorStrings.join(" "));
1296
+ }
1297
+ function checkDoiMatch(item, existing) {
1298
+ if (!item.DOI || !existing.DOI) {
1299
+ return null;
1300
+ }
1301
+ const normalizedItemDoi = normalizeDoi(item.DOI);
1302
+ const normalizedExistingDoi = normalizeDoi(existing.DOI);
1303
+ if (normalizedItemDoi === normalizedExistingDoi) {
1304
+ return {
1305
+ type: "doi",
1306
+ existing,
1307
+ details: {
1308
+ doi: normalizedExistingDoi
1309
+ }
1310
+ };
1311
+ }
1312
+ return null;
1313
+ }
1314
+ function checkPmidMatch(item, existing) {
1315
+ if (!item.PMID || !existing.PMID) {
1316
+ return null;
1317
+ }
1318
+ if (item.PMID === existing.PMID) {
1319
+ return {
1320
+ type: "pmid",
1321
+ existing,
1322
+ details: {
1323
+ pmid: existing.PMID
1324
+ }
1325
+ };
1326
+ }
1327
+ return null;
1328
+ }
1329
+ function checkTitleAuthorYearMatch(item, existing) {
1330
+ const itemTitle = item.title ? normalize(item.title) : null;
1331
+ const existingTitle = existing.title ? normalize(existing.title) : null;
1332
+ const itemAuthors = normalizeAuthors(item);
1333
+ const existingAuthors = normalizeAuthors(existing);
1334
+ const itemYear = extractYear(item);
1335
+ const existingYear = extractYear(existing);
1336
+ if (!itemTitle || !existingTitle || !itemAuthors || !existingAuthors || !itemYear || !existingYear) {
1337
+ return null;
1338
+ }
1339
+ if (itemTitle === existingTitle && itemAuthors === existingAuthors && itemYear === existingYear) {
1340
+ return {
1341
+ type: "title-author-year",
1342
+ existing,
1343
+ details: {
1344
+ normalizedTitle: existingTitle,
1345
+ normalizedAuthors: existingAuthors,
1346
+ year: existingYear
1347
+ }
1348
+ };
1349
+ }
1350
+ return null;
1351
+ }
1352
+ function checkSingleDuplicate(item, existing) {
1353
+ const doiMatch = checkDoiMatch(item, existing);
1354
+ if (doiMatch) {
1355
+ return doiMatch;
1356
+ }
1357
+ const pmidMatch = checkPmidMatch(item, existing);
1358
+ if (pmidMatch) {
1359
+ return pmidMatch;
1360
+ }
1361
+ return checkTitleAuthorYearMatch(item, existing);
1362
+ }
1363
+ function detectDuplicate(item, existingReferences) {
1364
+ const matches = [];
1365
+ const itemUuid = item.custom?.uuid;
1366
+ for (const existing of existingReferences) {
1367
+ if (itemUuid && existing.custom?.uuid === itemUuid) {
1368
+ continue;
1369
+ }
1370
+ const match = checkSingleDuplicate(item, existing);
1371
+ if (match) {
1372
+ matches.push(match);
1373
+ }
1374
+ }
1375
+ return {
1376
+ isDuplicate: matches.length > 0,
1377
+ matches
1378
+ };
1379
+ }
1380
+ export {
1381
+ generateUuid as A,
1382
+ isValidUuid as B,
1383
+ CslLibrarySchema as C,
1384
+ ensureCustomMetadata as D,
1385
+ extractUuidFromCustom as E,
1386
+ Library as L,
1387
+ Reference as R,
1388
+ computeHash as a,
1389
+ backupConfigSchema as b,
1390
+ computeFileHash as c,
1391
+ configSchema as d,
1392
+ defaultConfig as e,
1393
+ getDefaultCurrentDirConfigFilename as f,
1394
+ getDefaultBackupDirectory as g,
1395
+ getDefaultLibraryPath as h,
1396
+ getDefaultUserConfigPath as i,
1397
+ logLevelSchema as j,
1398
+ normalize as k,
1399
+ loadConfig as l,
1400
+ sortResults as m,
1401
+ normalizePartialConfig as n,
1402
+ detectDuplicate as o,
1403
+ partialConfigSchema as p,
1404
+ CslItemSchema as q,
1405
+ parseCslJson as r,
1406
+ search as s,
1407
+ tokenize as t,
1408
+ serializeCslJson as u,
1409
+ writeCslJson as v,
1410
+ watchConfigSchema as w,
1411
+ generateId as x,
1412
+ generateIdWithCollisionCheck as y,
1413
+ normalizeText as z
1414
+ };
1415
+ //# sourceMappingURL=detector-BF8Mcc72.js.map