@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.
- package/README.md +40 -0
- package/dist/chunks/detector-DHztTaFY.js +619 -0
- package/dist/chunks/detector-DHztTaFY.js.map +1 -0
- package/dist/chunks/{detector-BF8Mcc72.js → loader-mQ25o6cV.js} +303 -664
- package/dist/chunks/loader-mQ25o6cV.js.map +1 -0
- package/dist/chunks/search-Be9vzUIH.js +29541 -0
- package/dist/chunks/search-Be9vzUIH.js.map +1 -0
- package/dist/cli/commands/add.d.ts +44 -16
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/cite.d.ts +49 -0
- package/dist/cli/commands/cite.d.ts.map +1 -0
- package/dist/cli/commands/fulltext.d.ts +101 -0
- package/dist/cli/commands/fulltext.d.ts.map +1 -0
- package/dist/cli/commands/index.d.ts +14 -10
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/list.d.ts +23 -6
- package/dist/cli/commands/list.d.ts.map +1 -1
- package/dist/cli/commands/remove.d.ts +47 -12
- package/dist/cli/commands/remove.d.ts.map +1 -1
- package/dist/cli/commands/search.d.ts +24 -7
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts +26 -13
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/execution-context.d.ts +60 -0
- package/dist/cli/execution-context.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts +18 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/server-client.d.ts +73 -10
- package/dist/cli/server-client.d.ts.map +1 -1
- package/dist/cli.js +1200 -528
- package/dist/cli.js.map +1 -1
- package/dist/config/csl-styles.d.ts +83 -0
- package/dist/config/csl-styles.d.ts.map +1 -0
- package/dist/config/defaults.d.ts +10 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/schema.d.ts +84 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/core/csl-json/types.d.ts +15 -3
- package/dist/core/csl-json/types.d.ts.map +1 -1
- package/dist/core/library.d.ts +60 -0
- package/dist/core/library.d.ts.map +1 -1
- package/dist/features/format/bibtex.d.ts +6 -0
- package/dist/features/format/bibtex.d.ts.map +1 -0
- package/dist/features/format/citation-csl.d.ts +41 -0
- package/dist/features/format/citation-csl.d.ts.map +1 -0
- package/dist/features/format/citation-fallback.d.ts +24 -0
- package/dist/features/format/citation-fallback.d.ts.map +1 -0
- package/dist/features/format/index.d.ts +10 -0
- package/dist/features/format/index.d.ts.map +1 -0
- package/dist/features/format/json.d.ts +6 -0
- package/dist/features/format/json.d.ts.map +1 -0
- package/dist/features/format/pretty.d.ts +6 -0
- package/dist/features/format/pretty.d.ts.map +1 -0
- package/dist/features/fulltext/filename.d.ts +17 -0
- package/dist/features/fulltext/filename.d.ts.map +1 -0
- package/dist/features/fulltext/index.d.ts +7 -0
- package/dist/features/fulltext/index.d.ts.map +1 -0
- package/dist/features/fulltext/manager.d.ts +109 -0
- package/dist/features/fulltext/manager.d.ts.map +1 -0
- package/dist/features/fulltext/types.d.ts +12 -0
- package/dist/features/fulltext/types.d.ts.map +1 -0
- package/dist/features/import/cache.d.ts +37 -0
- package/dist/features/import/cache.d.ts.map +1 -0
- package/dist/features/import/detector.d.ts +42 -0
- package/dist/features/import/detector.d.ts.map +1 -0
- package/dist/features/import/fetcher.d.ts +49 -0
- package/dist/features/import/fetcher.d.ts.map +1 -0
- package/dist/features/import/importer.d.ts +61 -0
- package/dist/features/import/importer.d.ts.map +1 -0
- package/dist/features/import/index.d.ts +8 -0
- package/dist/features/import/index.d.ts.map +1 -0
- package/dist/features/import/normalizer.d.ts +15 -0
- package/dist/features/import/normalizer.d.ts.map +1 -0
- package/dist/features/import/parser.d.ts +33 -0
- package/dist/features/import/parser.d.ts.map +1 -0
- package/dist/features/import/rate-limiter.d.ts +45 -0
- package/dist/features/import/rate-limiter.d.ts.map +1 -0
- package/dist/features/operations/add.d.ts +65 -0
- package/dist/features/operations/add.d.ts.map +1 -0
- package/dist/features/operations/cite.d.ts +48 -0
- package/dist/features/operations/cite.d.ts.map +1 -0
- package/dist/features/operations/list.d.ts +28 -0
- package/dist/features/operations/list.d.ts.map +1 -0
- package/dist/features/operations/remove.d.ts +29 -0
- package/dist/features/operations/remove.d.ts.map +1 -0
- package/dist/features/operations/search.d.ts +30 -0
- package/dist/features/operations/search.d.ts.map +1 -0
- package/dist/features/operations/update.d.ts +39 -0
- package/dist/features/operations/update.d.ts.map +1 -0
- package/dist/index.js +18 -16
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/routes/add.d.ts +11 -0
- package/dist/server/routes/add.d.ts.map +1 -0
- package/dist/server/routes/cite.d.ts +9 -0
- package/dist/server/routes/cite.d.ts.map +1 -0
- package/dist/server/routes/list.d.ts +25 -0
- package/dist/server/routes/list.d.ts.map +1 -0
- package/dist/server/routes/references.d.ts.map +1 -1
- package/dist/server/routes/search.d.ts +26 -0
- package/dist/server/routes/search.d.ts.map +1 -0
- package/dist/server.js +215 -32
- package/dist/server.js.map +1 -1
- package/package.json +15 -4
- package/dist/chunks/detector-BF8Mcc72.js.map +0 -1
- package/dist/cli/output/bibtex.d.ts +0 -6
- package/dist/cli/output/bibtex.d.ts.map +0 -1
- package/dist/cli/output/index.d.ts +0 -7
- package/dist/cli/output/index.d.ts.map +0 -1
- package/dist/cli/output/json.d.ts +0 -6
- package/dist/cli/output/json.d.ts.map +0 -1
- package/dist/cli/output/pretty.d.ts +0 -6
- 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
|