@ncukondo/reference-manager 0.1.0 → 0.3.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 (116) hide show
  1. package/README.md +40 -0
  2. package/dist/chunks/detector-DHztTaFY.js +619 -0
  3. package/dist/chunks/detector-DHztTaFY.js.map +1 -0
  4. package/dist/chunks/{detector-BF8Mcc72.js → loader-mQ25o6cV.js} +303 -664
  5. package/dist/chunks/loader-mQ25o6cV.js.map +1 -0
  6. package/dist/chunks/search-Be9vzUIH.js +29541 -0
  7. package/dist/chunks/search-Be9vzUIH.js.map +1 -0
  8. package/dist/cli/commands/add.d.ts +44 -16
  9. package/dist/cli/commands/add.d.ts.map +1 -1
  10. package/dist/cli/commands/cite.d.ts +49 -0
  11. package/dist/cli/commands/cite.d.ts.map +1 -0
  12. package/dist/cli/commands/fulltext.d.ts +101 -0
  13. package/dist/cli/commands/fulltext.d.ts.map +1 -0
  14. package/dist/cli/commands/index.d.ts +14 -10
  15. package/dist/cli/commands/index.d.ts.map +1 -1
  16. package/dist/cli/commands/list.d.ts +23 -6
  17. package/dist/cli/commands/list.d.ts.map +1 -1
  18. package/dist/cli/commands/remove.d.ts +47 -12
  19. package/dist/cli/commands/remove.d.ts.map +1 -1
  20. package/dist/cli/commands/search.d.ts +24 -7
  21. package/dist/cli/commands/search.d.ts.map +1 -1
  22. package/dist/cli/commands/update.d.ts +26 -13
  23. package/dist/cli/commands/update.d.ts.map +1 -1
  24. package/dist/cli/execution-context.d.ts +60 -0
  25. package/dist/cli/execution-context.d.ts.map +1 -0
  26. package/dist/cli/helpers.d.ts +18 -0
  27. package/dist/cli/helpers.d.ts.map +1 -1
  28. package/dist/cli/index.d.ts.map +1 -1
  29. package/dist/cli/server-client.d.ts +73 -10
  30. package/dist/cli/server-client.d.ts.map +1 -1
  31. package/dist/cli.js +1200 -528
  32. package/dist/cli.js.map +1 -1
  33. package/dist/config/csl-styles.d.ts +83 -0
  34. package/dist/config/csl-styles.d.ts.map +1 -0
  35. package/dist/config/defaults.d.ts +10 -0
  36. package/dist/config/defaults.d.ts.map +1 -1
  37. package/dist/config/loader.d.ts.map +1 -1
  38. package/dist/config/schema.d.ts +84 -0
  39. package/dist/config/schema.d.ts.map +1 -1
  40. package/dist/core/csl-json/types.d.ts +15 -3
  41. package/dist/core/csl-json/types.d.ts.map +1 -1
  42. package/dist/core/library.d.ts +60 -0
  43. package/dist/core/library.d.ts.map +1 -1
  44. package/dist/features/format/bibtex.d.ts +6 -0
  45. package/dist/features/format/bibtex.d.ts.map +1 -0
  46. package/dist/features/format/citation-csl.d.ts +41 -0
  47. package/dist/features/format/citation-csl.d.ts.map +1 -0
  48. package/dist/features/format/citation-fallback.d.ts +24 -0
  49. package/dist/features/format/citation-fallback.d.ts.map +1 -0
  50. package/dist/features/format/index.d.ts +10 -0
  51. package/dist/features/format/index.d.ts.map +1 -0
  52. package/dist/features/format/json.d.ts +6 -0
  53. package/dist/features/format/json.d.ts.map +1 -0
  54. package/dist/features/format/pretty.d.ts +6 -0
  55. package/dist/features/format/pretty.d.ts.map +1 -0
  56. package/dist/features/fulltext/filename.d.ts +17 -0
  57. package/dist/features/fulltext/filename.d.ts.map +1 -0
  58. package/dist/features/fulltext/index.d.ts +7 -0
  59. package/dist/features/fulltext/index.d.ts.map +1 -0
  60. package/dist/features/fulltext/manager.d.ts +109 -0
  61. package/dist/features/fulltext/manager.d.ts.map +1 -0
  62. package/dist/features/fulltext/types.d.ts +12 -0
  63. package/dist/features/fulltext/types.d.ts.map +1 -0
  64. package/dist/features/import/cache.d.ts +37 -0
  65. package/dist/features/import/cache.d.ts.map +1 -0
  66. package/dist/features/import/detector.d.ts +42 -0
  67. package/dist/features/import/detector.d.ts.map +1 -0
  68. package/dist/features/import/fetcher.d.ts +49 -0
  69. package/dist/features/import/fetcher.d.ts.map +1 -0
  70. package/dist/features/import/importer.d.ts +61 -0
  71. package/dist/features/import/importer.d.ts.map +1 -0
  72. package/dist/features/import/index.d.ts +8 -0
  73. package/dist/features/import/index.d.ts.map +1 -0
  74. package/dist/features/import/normalizer.d.ts +15 -0
  75. package/dist/features/import/normalizer.d.ts.map +1 -0
  76. package/dist/features/import/parser.d.ts +33 -0
  77. package/dist/features/import/parser.d.ts.map +1 -0
  78. package/dist/features/import/rate-limiter.d.ts +45 -0
  79. package/dist/features/import/rate-limiter.d.ts.map +1 -0
  80. package/dist/features/operations/add.d.ts +65 -0
  81. package/dist/features/operations/add.d.ts.map +1 -0
  82. package/dist/features/operations/cite.d.ts +48 -0
  83. package/dist/features/operations/cite.d.ts.map +1 -0
  84. package/dist/features/operations/list.d.ts +28 -0
  85. package/dist/features/operations/list.d.ts.map +1 -0
  86. package/dist/features/operations/remove.d.ts +29 -0
  87. package/dist/features/operations/remove.d.ts.map +1 -0
  88. package/dist/features/operations/search.d.ts +30 -0
  89. package/dist/features/operations/search.d.ts.map +1 -0
  90. package/dist/features/operations/update.d.ts +39 -0
  91. package/dist/features/operations/update.d.ts.map +1 -0
  92. package/dist/index.js +18 -16
  93. package/dist/index.js.map +1 -1
  94. package/dist/server/index.d.ts +3 -1
  95. package/dist/server/index.d.ts.map +1 -1
  96. package/dist/server/routes/add.d.ts +11 -0
  97. package/dist/server/routes/add.d.ts.map +1 -0
  98. package/dist/server/routes/cite.d.ts +9 -0
  99. package/dist/server/routes/cite.d.ts.map +1 -0
  100. package/dist/server/routes/list.d.ts +25 -0
  101. package/dist/server/routes/list.d.ts.map +1 -0
  102. package/dist/server/routes/references.d.ts.map +1 -1
  103. package/dist/server/routes/search.d.ts +26 -0
  104. package/dist/server/routes/search.d.ts.map +1 -0
  105. package/dist/server.js +215 -32
  106. package/dist/server.js.map +1 -1
  107. package/package.json +15 -4
  108. package/dist/chunks/detector-BF8Mcc72.js.map +0 -1
  109. package/dist/cli/output/bibtex.d.ts +0 -6
  110. package/dist/cli/output/bibtex.d.ts.map +0 -1
  111. package/dist/cli/output/index.d.ts +0 -7
  112. package/dist/cli/output/index.d.ts.map +0 -1
  113. package/dist/cli/output/json.d.ts +0 -6
  114. package/dist/cli/output/json.d.ts.map +0 -1
  115. package/dist/cli/output/pretty.d.ts +0 -6
  116. package/dist/cli/output/pretty.d.ts.map +0 -1
package/README.md CHANGED
@@ -7,6 +7,7 @@ A local reference management tool using CSL-JSON as the single source of truth.
7
7
  - **CSL-JSON Native**: Uses CSL-JSON format as the primary data model
8
8
  - **Command-line Interface**: Comprehensive CLI with search, add, update, remove commands
9
9
  - **HTTP Server**: Optional background server for improved performance
10
+ - **Fulltext Management**: Attach PDF and Markdown files to references
10
11
  - **Duplicate Detection**: Automatic detection via DOI, PMID, or title+author+year
11
12
  - **Smart Search**: Full-text search with field-specific queries
12
13
  - **File Monitoring**: Automatic reload on external changes
@@ -66,6 +67,39 @@ ref server status
66
67
  ref server stop
67
68
  ```
68
69
 
70
+ ### Fulltext Management
71
+
72
+ Attach PDF and Markdown files to references for full-text storage.
73
+
74
+ ```bash
75
+ # Attach a PDF to a reference
76
+ ref fulltext attach smith-2020 ~/papers/smith-2020.pdf
77
+
78
+ # Attach a Markdown notes file
79
+ ref fulltext attach smith-2020 ~/notes/smith-2020.md
80
+
81
+ # Attach with explicit type (when extension doesn't match)
82
+ ref fulltext attach smith-2020 --pdf ~/downloads/paper.txt
83
+
84
+ # Move file instead of copy
85
+ ref fulltext attach smith-2020 ~/papers/smith-2020.pdf --move
86
+
87
+ # Overwrite existing attachment
88
+ ref fulltext attach smith-2020 ~/papers/smith-2020-revised.pdf --force
89
+
90
+ # Get attached file path
91
+ ref fulltext get smith-2020 --pdf
92
+
93
+ # Output file content to stdout
94
+ ref fulltext get smith-2020 --pdf --stdout
95
+
96
+ # Detach file (keeps file on disk)
97
+ ref fulltext detach smith-2020 --pdf
98
+
99
+ # Detach and delete file
100
+ ref fulltext detach smith-2020 --pdf --delete
101
+ ```
102
+
69
103
  ### Output Formats
70
104
 
71
105
  ```bash
@@ -98,8 +132,14 @@ max_age_days = 30
98
132
  [server]
99
133
  auto_start = true
100
134
  auto_stop_minutes = 60
135
+
136
+ [fulltext]
137
+ directory = "~/references/fulltext"
101
138
  ```
102
139
 
140
+ Environment variables:
141
+ - `REFERENCE_MANAGER_FULLTEXT_DIR`: Override fulltext directory
142
+
103
143
  See `spec/architecture/cli.md` for full configuration options.
104
144
 
105
145
  ## Development
@@ -0,0 +1,619 @@
1
+ import { z } from "zod";
2
+ const CslNameSchema = z.object({
3
+ family: z.string().optional(),
4
+ given: z.string().optional(),
5
+ literal: z.string().optional(),
6
+ "dropping-particle": z.string().optional(),
7
+ "non-dropping-particle": z.string().optional(),
8
+ suffix: z.string().optional()
9
+ });
10
+ const CslDateSchema = z.object({
11
+ "date-parts": z.array(z.array(z.number())).optional(),
12
+ raw: z.string().optional(),
13
+ season: z.string().optional(),
14
+ circa: z.boolean().optional(),
15
+ literal: z.string().optional()
16
+ });
17
+ const CslFulltextSchema = z.object({
18
+ pdf: z.string().optional(),
19
+ markdown: z.string().optional()
20
+ });
21
+ const CslCustomSchema = z.object({
22
+ uuid: z.string(),
23
+ created_at: z.string(),
24
+ timestamp: z.string(),
25
+ additional_urls: z.array(z.string()).optional(),
26
+ fulltext: CslFulltextSchema.optional()
27
+ }).passthrough();
28
+ const CslItemSchema = z.object({
29
+ id: z.string(),
30
+ type: z.string(),
31
+ title: z.string().optional(),
32
+ author: z.array(CslNameSchema).optional(),
33
+ editor: z.array(CslNameSchema).optional(),
34
+ issued: CslDateSchema.optional(),
35
+ accessed: CslDateSchema.optional(),
36
+ "container-title": z.string().optional(),
37
+ volume: z.string().optional(),
38
+ issue: z.string().optional(),
39
+ page: z.string().optional(),
40
+ DOI: z.string().optional(),
41
+ PMID: z.string().optional(),
42
+ PMCID: z.string().optional(),
43
+ ISBN: z.string().optional(),
44
+ ISSN: z.string().optional(),
45
+ URL: z.string().optional(),
46
+ abstract: z.string().optional(),
47
+ publisher: z.string().optional(),
48
+ "publisher-place": z.string().optional(),
49
+ note: z.string().optional(),
50
+ keyword: z.array(z.string()).optional(),
51
+ custom: CslCustomSchema.optional()
52
+ // Allow additional fields
53
+ }).passthrough();
54
+ const CslLibrarySchema = z.array(CslItemSchema);
55
+ const VALID_FIELDS = /* @__PURE__ */ new Set([
56
+ "author",
57
+ "title",
58
+ "year",
59
+ "doi",
60
+ "pmid",
61
+ "pmcid",
62
+ "url",
63
+ "keyword"
64
+ ]);
65
+ function isWhitespace(query, index) {
66
+ return /\s/.test(query.charAt(index));
67
+ }
68
+ function isQuote(query, index) {
69
+ return query.charAt(index) === '"';
70
+ }
71
+ function tokenize(query) {
72
+ const tokens = [];
73
+ let i = 0;
74
+ while (i < query.length) {
75
+ if (isWhitespace(query, i)) {
76
+ i++;
77
+ continue;
78
+ }
79
+ const result = parseNextToken(query, i);
80
+ if (result.token) {
81
+ tokens.push(result.token);
82
+ }
83
+ i = result.nextIndex;
84
+ }
85
+ return {
86
+ original: query,
87
+ tokens
88
+ };
89
+ }
90
+ function hasWhitespaceBetween(query, start, end) {
91
+ for (let j = start; j < end; j++) {
92
+ if (isWhitespace(query, j)) {
93
+ return true;
94
+ }
95
+ }
96
+ return false;
97
+ }
98
+ function tryParseFieldValue(query, startIndex) {
99
+ const colonIndex = query.indexOf(":", startIndex);
100
+ if (colonIndex === -1) {
101
+ return null;
102
+ }
103
+ if (hasWhitespaceBetween(query, startIndex, colonIndex)) {
104
+ return null;
105
+ }
106
+ const fieldName = query.substring(startIndex, colonIndex);
107
+ if (!VALID_FIELDS.has(fieldName)) {
108
+ return null;
109
+ }
110
+ const afterColon = colonIndex + 1;
111
+ if (afterColon >= query.length || isWhitespace(query, afterColon)) {
112
+ return { token: null, nextIndex: afterColon };
113
+ }
114
+ if (isQuote(query, afterColon)) {
115
+ const quoteResult = parseQuotedValue(query, afterColon);
116
+ if (quoteResult.value !== null) {
117
+ return {
118
+ token: {
119
+ raw: query.substring(startIndex, quoteResult.nextIndex),
120
+ value: quoteResult.value,
121
+ field: fieldName,
122
+ isPhrase: true
123
+ },
124
+ nextIndex: quoteResult.nextIndex
125
+ };
126
+ }
127
+ return null;
128
+ }
129
+ const valueResult = parseUnquotedValue(query, afterColon);
130
+ return {
131
+ token: {
132
+ raw: query.substring(startIndex, valueResult.nextIndex),
133
+ value: valueResult.value,
134
+ field: fieldName,
135
+ isPhrase: false
136
+ },
137
+ nextIndex: valueResult.nextIndex
138
+ };
139
+ }
140
+ function parseQuotedToken(query, startIndex) {
141
+ const quoteResult = parseQuotedValue(query, startIndex);
142
+ if (quoteResult.value !== null) {
143
+ return {
144
+ token: {
145
+ raw: query.substring(startIndex, quoteResult.nextIndex),
146
+ value: quoteResult.value,
147
+ isPhrase: true
148
+ },
149
+ nextIndex: quoteResult.nextIndex
150
+ };
151
+ }
152
+ if (quoteResult.nextIndex > startIndex) {
153
+ return { token: null, nextIndex: quoteResult.nextIndex };
154
+ }
155
+ const valueResult = parseUnquotedValue(query, startIndex, true);
156
+ return {
157
+ token: {
158
+ raw: valueResult.value,
159
+ value: valueResult.value,
160
+ isPhrase: false
161
+ },
162
+ nextIndex: valueResult.nextIndex
163
+ };
164
+ }
165
+ function parseRegularToken(query, startIndex) {
166
+ const valueResult = parseUnquotedValue(query, startIndex);
167
+ return {
168
+ token: {
169
+ raw: valueResult.value,
170
+ value: valueResult.value,
171
+ isPhrase: false
172
+ },
173
+ nextIndex: valueResult.nextIndex
174
+ };
175
+ }
176
+ function parseNextToken(query, startIndex) {
177
+ const fieldResult = tryParseFieldValue(query, startIndex);
178
+ if (fieldResult !== null) {
179
+ return fieldResult;
180
+ }
181
+ if (isQuote(query, startIndex)) {
182
+ return parseQuotedToken(query, startIndex);
183
+ }
184
+ return parseRegularToken(query, startIndex);
185
+ }
186
+ function parseQuotedValue(query, startIndex) {
187
+ if (!isQuote(query, startIndex)) {
188
+ return { value: null, nextIndex: startIndex };
189
+ }
190
+ let i = startIndex + 1;
191
+ const valueStart = i;
192
+ while (i < query.length && !isQuote(query, i)) {
193
+ i++;
194
+ }
195
+ if (i >= query.length) {
196
+ return { value: null, nextIndex: startIndex };
197
+ }
198
+ const value = query.substring(valueStart, i);
199
+ i++;
200
+ if (value.trim() === "") {
201
+ return { value: null, nextIndex: i };
202
+ }
203
+ return { value, nextIndex: i };
204
+ }
205
+ function parseUnquotedValue(query, startIndex, includeQuotes = false) {
206
+ let i = startIndex;
207
+ while (i < query.length && !isWhitespace(query, i)) {
208
+ if (!includeQuotes && isQuote(query, i)) {
209
+ break;
210
+ }
211
+ i++;
212
+ }
213
+ return {
214
+ value: query.substring(startIndex, i),
215
+ nextIndex: i
216
+ };
217
+ }
218
+ function normalize(text) {
219
+ let normalized = text.normalize("NFKC");
220
+ normalized = normalized.toLowerCase();
221
+ normalized = normalized.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "");
222
+ normalized = normalized.replace(/[^\p{L}\p{N}/\s]/gu, " ");
223
+ normalized = normalized.replace(/\s+/g, " ").trim();
224
+ return normalized;
225
+ }
226
+ const ID_FIELDS = /* @__PURE__ */ new Set(["DOI", "PMID", "PMCID", "URL"]);
227
+ function extractYear$2(reference) {
228
+ if (reference.issued?.["date-parts"]?.[0]?.[0]) {
229
+ return String(reference.issued["date-parts"][0][0]);
230
+ }
231
+ return "0000";
232
+ }
233
+ function extractAuthors(reference) {
234
+ if (!reference.author || reference.author.length === 0) {
235
+ return "";
236
+ }
237
+ return reference.author.map((author) => {
238
+ const family = author.family || "";
239
+ const givenInitial = author.given ? author.given[0] : "";
240
+ return givenInitial ? `${family} ${givenInitial}` : family;
241
+ }).join(" ");
242
+ }
243
+ function getFieldValue(reference, field) {
244
+ if (field === "year") {
245
+ return extractYear$2(reference);
246
+ }
247
+ if (field === "author") {
248
+ return extractAuthors(reference);
249
+ }
250
+ const value = reference[field];
251
+ if (typeof value === "string") {
252
+ return value;
253
+ }
254
+ if (field.startsWith("custom.")) {
255
+ const customField = field.substring(7);
256
+ const customValue = reference.custom?.[customField];
257
+ if (typeof customValue === "string") {
258
+ return customValue;
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+ function matchUrl(queryValue, reference) {
264
+ if (reference.URL === queryValue) {
265
+ return {
266
+ field: "URL",
267
+ strength: "exact",
268
+ value: reference.URL
269
+ };
270
+ }
271
+ const additionalUrls = reference.custom?.additional_urls;
272
+ if (Array.isArray(additionalUrls)) {
273
+ for (const url of additionalUrls) {
274
+ if (typeof url === "string" && url === queryValue) {
275
+ return {
276
+ field: "custom.additional_urls",
277
+ strength: "exact",
278
+ value: url
279
+ };
280
+ }
281
+ }
282
+ }
283
+ return null;
284
+ }
285
+ function matchKeyword(queryValue, reference) {
286
+ if (!reference.keyword || !Array.isArray(reference.keyword)) {
287
+ return null;
288
+ }
289
+ const normalizedQuery = normalize(queryValue);
290
+ for (const keyword of reference.keyword) {
291
+ if (typeof keyword === "string") {
292
+ const normalizedKeyword = normalize(keyword);
293
+ if (normalizedKeyword.includes(normalizedQuery)) {
294
+ return {
295
+ field: "keyword",
296
+ strength: "partial",
297
+ value: keyword
298
+ };
299
+ }
300
+ }
301
+ }
302
+ return null;
303
+ }
304
+ const FIELD_MAP = {
305
+ author: "author",
306
+ title: "title",
307
+ doi: "DOI",
308
+ pmid: "PMID",
309
+ pmcid: "PMCID"
310
+ };
311
+ function matchYearField(tokenValue, reference) {
312
+ const year = extractYear$2(reference);
313
+ if (year === tokenValue) {
314
+ return {
315
+ field: "year",
316
+ strength: "exact",
317
+ value: year
318
+ };
319
+ }
320
+ return null;
321
+ }
322
+ function matchFieldValue(field, tokenValue, reference) {
323
+ const fieldValue = getFieldValue(reference, field);
324
+ if (fieldValue === null) {
325
+ return null;
326
+ }
327
+ if (ID_FIELDS.has(field)) {
328
+ if (fieldValue === tokenValue) {
329
+ return {
330
+ field,
331
+ strength: "exact",
332
+ value: fieldValue
333
+ };
334
+ }
335
+ return null;
336
+ }
337
+ const normalizedFieldValue = normalize(fieldValue);
338
+ const normalizedQuery = normalize(tokenValue);
339
+ if (normalizedFieldValue.includes(normalizedQuery)) {
340
+ return {
341
+ field,
342
+ strength: "partial",
343
+ value: fieldValue
344
+ };
345
+ }
346
+ return null;
347
+ }
348
+ function matchSpecificField(token, reference) {
349
+ const matches = [];
350
+ const fieldToSearch = token.field;
351
+ if (fieldToSearch === "url") {
352
+ const urlMatch = matchUrl(token.value, reference);
353
+ if (urlMatch) matches.push(urlMatch);
354
+ return matches;
355
+ }
356
+ if (fieldToSearch === "year") {
357
+ const yearMatch = matchYearField(token.value, reference);
358
+ if (yearMatch) matches.push(yearMatch);
359
+ return matches;
360
+ }
361
+ if (fieldToSearch === "keyword") {
362
+ const keywordMatch = matchKeyword(token.value, reference);
363
+ if (keywordMatch) matches.push(keywordMatch);
364
+ return matches;
365
+ }
366
+ const actualField = FIELD_MAP[fieldToSearch] || fieldToSearch;
367
+ const match = matchFieldValue(actualField, token.value, reference);
368
+ if (match) matches.push(match);
369
+ return matches;
370
+ }
371
+ const STANDARD_SEARCH_FIELDS = [
372
+ "title",
373
+ "author",
374
+ "container-title",
375
+ "publisher",
376
+ "DOI",
377
+ "PMID",
378
+ "PMCID",
379
+ "abstract"
380
+ ];
381
+ function matchSingleField(field, tokenValue, reference) {
382
+ if (field === "year") {
383
+ return matchYearField(tokenValue, reference);
384
+ }
385
+ if (field === "URL") {
386
+ return matchUrl(tokenValue, reference);
387
+ }
388
+ if (field === "keyword") {
389
+ return matchKeyword(tokenValue, reference);
390
+ }
391
+ return matchFieldValue(field, tokenValue, reference);
392
+ }
393
+ function matchAllFields(token, reference) {
394
+ const matches = [];
395
+ const specialFields = ["year", "URL", "keyword"];
396
+ for (const field of specialFields) {
397
+ const match = matchSingleField(field, token.value, reference);
398
+ if (match) matches.push(match);
399
+ }
400
+ for (const field of STANDARD_SEARCH_FIELDS) {
401
+ const match = matchFieldValue(field, token.value, reference);
402
+ if (match) matches.push(match);
403
+ }
404
+ return matches;
405
+ }
406
+ function matchToken(token, reference) {
407
+ if (token.field) {
408
+ return matchSpecificField(token, reference);
409
+ }
410
+ return matchAllFields(token, reference);
411
+ }
412
+ function matchReference(reference, tokens) {
413
+ if (tokens.length === 0) {
414
+ return null;
415
+ }
416
+ const tokenMatches = [];
417
+ let overallStrength = "none";
418
+ for (const token of tokens) {
419
+ const matches = matchToken(token, reference);
420
+ if (matches.length === 0) {
421
+ return null;
422
+ }
423
+ const tokenStrength = matches.some((m) => m.strength === "exact") ? "exact" : "partial";
424
+ if (tokenStrength === "exact") {
425
+ overallStrength = "exact";
426
+ } else if (tokenStrength === "partial" && overallStrength === "none") {
427
+ overallStrength = "partial";
428
+ }
429
+ tokenMatches.push({
430
+ token,
431
+ matches
432
+ });
433
+ }
434
+ const score = overallStrength === "exact" ? 100 + tokenMatches.length : 50 + tokenMatches.length;
435
+ return {
436
+ reference,
437
+ tokenMatches,
438
+ overallStrength,
439
+ score
440
+ };
441
+ }
442
+ function search(references, tokens) {
443
+ const results = [];
444
+ for (const reference of references) {
445
+ const match = matchReference(reference, tokens);
446
+ if (match) {
447
+ results.push(match);
448
+ }
449
+ }
450
+ return results;
451
+ }
452
+ function extractYear$1(reference) {
453
+ if (reference.issued?.["date-parts"]?.[0]?.[0]) {
454
+ return String(reference.issued["date-parts"][0][0]);
455
+ }
456
+ return "0000";
457
+ }
458
+ function extractFirstAuthorFamily(reference) {
459
+ if (!reference.author || reference.author.length === 0) {
460
+ return "";
461
+ }
462
+ return reference.author[0]?.family || "";
463
+ }
464
+ function extractTitle(reference) {
465
+ return reference.title || "";
466
+ }
467
+ function compareStrength(a, b) {
468
+ const strengthOrder = { exact: 2, partial: 1, none: 0 };
469
+ return strengthOrder[b] - strengthOrder[a];
470
+ }
471
+ function compareYear(a, b) {
472
+ const yearA = extractYear$1(a);
473
+ const yearB = extractYear$1(b);
474
+ return Number(yearB) - Number(yearA);
475
+ }
476
+ function compareAuthor(a, b) {
477
+ const authorA = extractFirstAuthorFamily(a).toLowerCase();
478
+ const authorB = extractFirstAuthorFamily(b).toLowerCase();
479
+ if (authorA === "" && authorB !== "") return 1;
480
+ if (authorA !== "" && authorB === "") return -1;
481
+ return authorA.localeCompare(authorB);
482
+ }
483
+ function compareTitle(a, b) {
484
+ const titleA = extractTitle(a).toLowerCase();
485
+ const titleB = extractTitle(b).toLowerCase();
486
+ if (titleA === "" && titleB !== "") return 1;
487
+ if (titleA !== "" && titleB === "") return -1;
488
+ return titleA.localeCompare(titleB);
489
+ }
490
+ function sortResults(results) {
491
+ const indexed = results.map((result, index) => ({ result, index }));
492
+ const sorted = indexed.sort((a, b) => {
493
+ const strengthDiff = compareStrength(a.result.overallStrength, b.result.overallStrength);
494
+ if (strengthDiff !== 0) return strengthDiff;
495
+ const yearDiff = compareYear(a.result.reference, b.result.reference);
496
+ if (yearDiff !== 0) return yearDiff;
497
+ const authorDiff = compareAuthor(a.result.reference, b.result.reference);
498
+ if (authorDiff !== 0) return authorDiff;
499
+ const titleDiff = compareTitle(a.result.reference, b.result.reference);
500
+ if (titleDiff !== 0) return titleDiff;
501
+ return a.index - b.index;
502
+ });
503
+ return sorted.map((item) => item.result);
504
+ }
505
+ function normalizeDoi(doi) {
506
+ const normalized = doi.replace(/^https?:\/\/doi\.org\//i, "").replace(/^https?:\/\/dx\.doi\.org\//i, "").replace(/^doi:/i, "");
507
+ return normalized;
508
+ }
509
+ function extractYear(item) {
510
+ const dateParts = item.issued?.["date-parts"]?.[0];
511
+ if (!dateParts || dateParts.length === 0) {
512
+ return null;
513
+ }
514
+ return String(dateParts[0]);
515
+ }
516
+ function normalizeAuthors(item) {
517
+ if (!item.author || item.author.length === 0) {
518
+ return null;
519
+ }
520
+ const authorStrings = item.author.map((author) => {
521
+ const family = author.family || "";
522
+ const givenInitial = author.given ? author.given.charAt(0) : "";
523
+ return `${family} ${givenInitial}`.trim();
524
+ });
525
+ return normalize(authorStrings.join(" "));
526
+ }
527
+ function checkDoiMatch(item, existing) {
528
+ if (!item.DOI || !existing.DOI) {
529
+ return null;
530
+ }
531
+ const normalizedItemDoi = normalizeDoi(item.DOI);
532
+ const normalizedExistingDoi = normalizeDoi(existing.DOI);
533
+ if (normalizedItemDoi === normalizedExistingDoi) {
534
+ return {
535
+ type: "doi",
536
+ existing,
537
+ details: {
538
+ doi: normalizedExistingDoi
539
+ }
540
+ };
541
+ }
542
+ return null;
543
+ }
544
+ function checkPmidMatch(item, existing) {
545
+ if (!item.PMID || !existing.PMID) {
546
+ return null;
547
+ }
548
+ if (item.PMID === existing.PMID) {
549
+ return {
550
+ type: "pmid",
551
+ existing,
552
+ details: {
553
+ pmid: existing.PMID
554
+ }
555
+ };
556
+ }
557
+ return null;
558
+ }
559
+ function checkTitleAuthorYearMatch(item, existing) {
560
+ const itemTitle = item.title ? normalize(item.title) : null;
561
+ const existingTitle = existing.title ? normalize(existing.title) : null;
562
+ const itemAuthors = normalizeAuthors(item);
563
+ const existingAuthors = normalizeAuthors(existing);
564
+ const itemYear = extractYear(item);
565
+ const existingYear = extractYear(existing);
566
+ if (!itemTitle || !existingTitle || !itemAuthors || !existingAuthors || !itemYear || !existingYear) {
567
+ return null;
568
+ }
569
+ if (itemTitle === existingTitle && itemAuthors === existingAuthors && itemYear === existingYear) {
570
+ return {
571
+ type: "title-author-year",
572
+ existing,
573
+ details: {
574
+ normalizedTitle: existingTitle,
575
+ normalizedAuthors: existingAuthors,
576
+ year: existingYear
577
+ }
578
+ };
579
+ }
580
+ return null;
581
+ }
582
+ function checkSingleDuplicate(item, existing) {
583
+ const doiMatch = checkDoiMatch(item, existing);
584
+ if (doiMatch) {
585
+ return doiMatch;
586
+ }
587
+ const pmidMatch = checkPmidMatch(item, existing);
588
+ if (pmidMatch) {
589
+ return pmidMatch;
590
+ }
591
+ return checkTitleAuthorYearMatch(item, existing);
592
+ }
593
+ function detectDuplicate(item, existingReferences) {
594
+ const matches = [];
595
+ const itemUuid = item.custom?.uuid;
596
+ for (const existing of existingReferences) {
597
+ if (itemUuid && existing.custom?.uuid === itemUuid) {
598
+ continue;
599
+ }
600
+ const match = checkSingleDuplicate(item, existing);
601
+ if (match) {
602
+ matches.push(match);
603
+ }
604
+ }
605
+ return {
606
+ isDuplicate: matches.length > 0,
607
+ matches
608
+ };
609
+ }
610
+ export {
611
+ CslLibrarySchema as C,
612
+ sortResults as a,
613
+ CslItemSchema as b,
614
+ detectDuplicate as d,
615
+ normalize as n,
616
+ search as s,
617
+ tokenize as t
618
+ };
619
+ //# sourceMappingURL=detector-DHztTaFY.js.map