docrev 0.8.1 → 0.8.5
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/.claude/settings.local.json +9 -0
- package/PLAN-tables-and-postprocess.md +850 -0
- package/README.md +33 -0
- package/bin/rev.js +12 -131
- package/bin/rev.ts +145 -0
- package/dist/bin/rev.d.ts +9 -0
- package/dist/bin/rev.d.ts.map +1 -0
- package/dist/bin/rev.js +118 -0
- package/dist/bin/rev.js.map +1 -0
- package/dist/lib/annotations.d.ts +91 -0
- package/dist/lib/annotations.d.ts.map +1 -0
- package/dist/lib/annotations.js +554 -0
- package/dist/lib/annotations.js.map +1 -0
- package/dist/lib/build.d.ts +171 -0
- package/dist/lib/build.d.ts.map +1 -0
- package/dist/lib/build.js +755 -0
- package/dist/lib/build.js.map +1 -0
- package/dist/lib/citations.d.ts +34 -0
- package/dist/lib/citations.d.ts.map +1 -0
- package/dist/lib/citations.js +140 -0
- package/dist/lib/citations.js.map +1 -0
- package/dist/lib/commands/build.d.ts +13 -0
- package/dist/lib/commands/build.d.ts.map +1 -0
- package/dist/lib/commands/build.js +678 -0
- package/dist/lib/commands/build.js.map +1 -0
- package/dist/lib/commands/citations.d.ts +11 -0
- package/dist/lib/commands/citations.d.ts.map +1 -0
- package/dist/lib/commands/citations.js +428 -0
- package/dist/lib/commands/citations.js.map +1 -0
- package/dist/lib/commands/comments.d.ts +11 -0
- package/dist/lib/commands/comments.d.ts.map +1 -0
- package/dist/lib/commands/comments.js +883 -0
- package/dist/lib/commands/comments.js.map +1 -0
- package/dist/lib/commands/context.d.ts +35 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/context.js +59 -0
- package/dist/lib/commands/context.js.map +1 -0
- package/dist/lib/commands/core.d.ts +11 -0
- package/dist/lib/commands/core.d.ts.map +1 -0
- package/dist/lib/commands/core.js +246 -0
- package/dist/lib/commands/core.js.map +1 -0
- package/dist/lib/commands/doi.d.ts +11 -0
- package/dist/lib/commands/doi.d.ts.map +1 -0
- package/dist/lib/commands/doi.js +373 -0
- package/dist/lib/commands/doi.js.map +1 -0
- package/dist/lib/commands/history.d.ts +11 -0
- package/dist/lib/commands/history.d.ts.map +1 -0
- package/dist/lib/commands/history.js +245 -0
- package/dist/lib/commands/history.js.map +1 -0
- package/dist/lib/commands/index.d.ts +28 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/index.js +35 -0
- package/dist/lib/commands/index.js.map +1 -0
- package/dist/lib/commands/init.d.ts +11 -0
- package/dist/lib/commands/init.d.ts.map +1 -0
- package/dist/lib/commands/init.js +209 -0
- package/dist/lib/commands/init.js.map +1 -0
- package/dist/lib/commands/response.d.ts +11 -0
- package/dist/lib/commands/response.d.ts.map +1 -0
- package/dist/lib/commands/response.js +317 -0
- package/dist/lib/commands/response.js.map +1 -0
- package/dist/lib/commands/sections.d.ts +11 -0
- package/dist/lib/commands/sections.d.ts.map +1 -0
- package/dist/lib/commands/sections.js +1071 -0
- package/dist/lib/commands/sections.js.map +1 -0
- package/dist/lib/commands/utilities.d.ts +19 -0
- package/dist/lib/commands/utilities.d.ts.map +1 -0
- package/dist/lib/commands/utilities.js +2009 -0
- package/dist/lib/commands/utilities.js.map +1 -0
- package/dist/lib/comment-realign.d.ts +50 -0
- package/dist/lib/comment-realign.d.ts.map +1 -0
- package/dist/lib/comment-realign.js +372 -0
- package/dist/lib/comment-realign.js.map +1 -0
- package/dist/lib/config.d.ts +41 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +76 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/crossref.d.ts +108 -0
- package/dist/lib/crossref.d.ts.map +1 -0
- package/dist/lib/crossref.js +597 -0
- package/dist/lib/crossref.js.map +1 -0
- package/dist/lib/dependencies.d.ts +30 -0
- package/dist/lib/dependencies.d.ts.map +1 -0
- package/dist/lib/dependencies.js +95 -0
- package/dist/lib/dependencies.js.map +1 -0
- package/dist/lib/doi-cache.d.ts +29 -0
- package/dist/lib/doi-cache.d.ts.map +1 -0
- package/dist/lib/doi-cache.js +104 -0
- package/dist/lib/doi-cache.js.map +1 -0
- package/dist/lib/doi.d.ts +65 -0
- package/dist/lib/doi.d.ts.map +1 -0
- package/dist/lib/doi.js +710 -0
- package/dist/lib/doi.js.map +1 -0
- package/dist/lib/equations.d.ts +61 -0
- package/dist/lib/equations.d.ts.map +1 -0
- package/dist/lib/equations.js +445 -0
- package/dist/lib/equations.js.map +1 -0
- package/dist/lib/errors.d.ts +60 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +303 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/format.d.ts +104 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +416 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/git.d.ts +88 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +304 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/grammar.d.ts +62 -0
- package/dist/lib/grammar.d.ts.map +1 -0
- package/dist/lib/grammar.js +244 -0
- package/dist/lib/grammar.js.map +1 -0
- package/dist/lib/image-registry.d.ts +68 -0
- package/dist/lib/image-registry.d.ts.map +1 -0
- package/dist/lib/image-registry.js +112 -0
- package/dist/lib/image-registry.js.map +1 -0
- package/dist/lib/import.d.ts +184 -0
- package/dist/lib/import.d.ts.map +1 -0
- package/dist/lib/import.js +1581 -0
- package/dist/lib/import.js.map +1 -0
- package/dist/lib/journals.d.ts +55 -0
- package/dist/lib/journals.d.ts.map +1 -0
- package/dist/lib/journals.js +417 -0
- package/dist/lib/journals.js.map +1 -0
- package/dist/lib/merge.d.ts +138 -0
- package/dist/lib/merge.d.ts.map +1 -0
- package/dist/lib/merge.js +603 -0
- package/dist/lib/merge.js.map +1 -0
- package/dist/lib/orcid.d.ts +36 -0
- package/dist/lib/orcid.d.ts.map +1 -0
- package/dist/lib/orcid.js +117 -0
- package/dist/lib/orcid.js.map +1 -0
- package/dist/lib/pdf-comments.d.ts +95 -0
- package/dist/lib/pdf-comments.d.ts.map +1 -0
- package/dist/lib/pdf-comments.js +192 -0
- package/dist/lib/pdf-comments.js.map +1 -0
- package/dist/lib/pdf-import.d.ts +118 -0
- package/dist/lib/pdf-import.d.ts.map +1 -0
- package/dist/lib/pdf-import.js +397 -0
- package/dist/lib/pdf-import.js.map +1 -0
- package/dist/lib/plugins.d.ts +76 -0
- package/dist/lib/plugins.d.ts.map +1 -0
- package/dist/lib/plugins.js +235 -0
- package/dist/lib/plugins.js.map +1 -0
- package/dist/lib/postprocess.d.ts +42 -0
- package/dist/lib/postprocess.d.ts.map +1 -0
- package/dist/lib/postprocess.js +138 -0
- package/dist/lib/postprocess.js.map +1 -0
- package/dist/lib/pptx-template.d.ts +59 -0
- package/dist/lib/pptx-template.d.ts.map +1 -0
- package/dist/lib/pptx-template.js +613 -0
- package/dist/lib/pptx-template.js.map +1 -0
- package/dist/lib/pptx-themes.d.ts +80 -0
- package/dist/lib/pptx-themes.d.ts.map +1 -0
- package/dist/lib/pptx-themes.js +818 -0
- package/dist/lib/pptx-themes.js.map +1 -0
- package/dist/lib/protect-restore.d.ts +137 -0
- package/dist/lib/protect-restore.d.ts.map +1 -0
- package/dist/lib/protect-restore.js +394 -0
- package/dist/lib/protect-restore.js.map +1 -0
- package/dist/lib/rate-limiter.d.ts +27 -0
- package/dist/lib/rate-limiter.d.ts.map +1 -0
- package/dist/lib/rate-limiter.js +79 -0
- package/dist/lib/rate-limiter.js.map +1 -0
- package/dist/lib/response.d.ts +41 -0
- package/dist/lib/response.d.ts.map +1 -0
- package/dist/lib/response.js +150 -0
- package/dist/lib/response.js.map +1 -0
- package/dist/lib/review.d.ts +35 -0
- package/dist/lib/review.d.ts.map +1 -0
- package/dist/lib/review.js +263 -0
- package/dist/lib/review.js.map +1 -0
- package/dist/lib/schema.d.ts +66 -0
- package/dist/lib/schema.d.ts.map +1 -0
- package/dist/lib/schema.js +339 -0
- package/dist/lib/schema.js.map +1 -0
- package/dist/lib/scientific-words.d.ts +6 -0
- package/dist/lib/scientific-words.d.ts.map +1 -0
- package/dist/lib/scientific-words.js +66 -0
- package/dist/lib/scientific-words.js.map +1 -0
- package/dist/lib/sections.d.ts +40 -0
- package/dist/lib/sections.d.ts.map +1 -0
- package/dist/lib/sections.js +288 -0
- package/dist/lib/sections.js.map +1 -0
- package/dist/lib/slides.d.ts +86 -0
- package/dist/lib/slides.d.ts.map +1 -0
- package/dist/lib/slides.js +676 -0
- package/dist/lib/slides.js.map +1 -0
- package/dist/lib/spelling.d.ts +76 -0
- package/dist/lib/spelling.d.ts.map +1 -0
- package/dist/lib/spelling.js +272 -0
- package/dist/lib/spelling.js.map +1 -0
- package/dist/lib/templates.d.ts +30 -0
- package/dist/lib/templates.d.ts.map +1 -0
- package/dist/lib/templates.js +504 -0
- package/dist/lib/templates.js.map +1 -0
- package/dist/lib/themes.d.ts +85 -0
- package/dist/lib/themes.d.ts.map +1 -0
- package/dist/lib/themes.js +652 -0
- package/dist/lib/themes.js.map +1 -0
- package/dist/lib/trackchanges.d.ts +51 -0
- package/dist/lib/trackchanges.d.ts.map +1 -0
- package/dist/lib/trackchanges.js +202 -0
- package/dist/lib/trackchanges.js.map +1 -0
- package/dist/lib/tui.d.ts +76 -0
- package/dist/lib/tui.d.ts.map +1 -0
- package/dist/lib/tui.js +377 -0
- package/dist/lib/tui.js.map +1 -0
- package/dist/lib/types.d.ts +447 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +6 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/undo.d.ts +57 -0
- package/dist/lib/undo.d.ts.map +1 -0
- package/dist/lib/undo.js +185 -0
- package/dist/lib/undo.js.map +1 -0
- package/dist/lib/utils.d.ts +16 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +40 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/variables.d.ts +42 -0
- package/dist/lib/variables.d.ts.map +1 -0
- package/dist/lib/variables.js +141 -0
- package/dist/lib/variables.js.map +1 -0
- package/dist/lib/word.d.ts +80 -0
- package/dist/lib/word.d.ts.map +1 -0
- package/dist/lib/word.js +360 -0
- package/dist/lib/word.js.map +1 -0
- package/dist/lib/wordcomments.d.ts +51 -0
- package/dist/lib/wordcomments.d.ts.map +1 -0
- package/dist/lib/wordcomments.js +587 -0
- package/dist/lib/wordcomments.js.map +1 -0
- package/eslint.config.js +27 -0
- package/lib/annotations.ts +622 -0
- package/lib/apply-buildup-colors.py +88 -0
- package/lib/build.ts +1013 -0
- package/lib/{citations.js → citations.ts} +38 -27
- package/lib/commands/{build.js → build.ts} +80 -27
- package/lib/commands/{citations.js → citations.ts} +36 -18
- package/lib/commands/{comments.js → comments.ts} +187 -54
- package/lib/commands/{context.js → context.ts} +18 -8
- package/lib/commands/{core.js → core.ts} +34 -20
- package/lib/commands/{doi.js → doi.ts} +32 -16
- package/lib/commands/{history.js → history.ts} +25 -12
- package/lib/commands/{index.js → index.ts} +9 -5
- package/lib/commands/{init.js → init.ts} +20 -8
- package/lib/commands/{response.js → response.ts} +47 -20
- package/lib/commands/{sections.js → sections.ts} +273 -68
- package/lib/commands/{utilities.js → utilities.ts} +338 -158
- package/lib/{comment-realign.js → comment-realign.ts} +117 -45
- package/lib/config.ts +84 -0
- package/lib/{crossref.js → crossref.ts} +213 -138
- package/lib/dependencies.ts +106 -0
- package/lib/doi-cache.ts +115 -0
- package/lib/{doi.js → doi.ts} +115 -281
- package/lib/{equations.js → equations.ts} +60 -64
- package/lib/{errors.js → errors.ts} +56 -48
- package/lib/{format.js → format.ts} +137 -63
- package/lib/{git.js → git.ts} +66 -63
- package/lib/{grammar.js → grammar.ts} +45 -32
- package/lib/image-registry.ts +180 -0
- package/lib/import.ts +2060 -0
- package/lib/journals.ts +505 -0
- package/lib/{merge.js → merge.ts} +185 -135
- package/lib/{orcid.js → orcid.ts} +17 -22
- package/lib/{pdf-comments.js → pdf-comments.ts} +76 -18
- package/lib/{pdf-import.js → pdf-import.ts} +148 -70
- package/lib/{plugins.js → plugins.ts} +82 -39
- package/lib/postprocess.ts +188 -0
- package/lib/pptx-color-filter.lua +37 -0
- package/lib/pptx-template.ts +625 -0
- package/lib/pptx-themes/academic.pptx +0 -0
- package/lib/pptx-themes/corporate.pptx +0 -0
- package/lib/pptx-themes/dark.pptx +0 -0
- package/lib/pptx-themes/default.pptx +0 -0
- package/lib/pptx-themes/minimal.pptx +0 -0
- package/lib/pptx-themes/plant.pptx +0 -0
- package/lib/pptx-themes.ts +896 -0
- package/lib/protect-restore.ts +516 -0
- package/lib/rate-limiter.ts +94 -0
- package/lib/{response.js → response.ts} +36 -21
- package/lib/{review.js → review.ts} +53 -43
- package/lib/{schema.js → schema.ts} +70 -25
- package/lib/{sections.js → sections.ts} +71 -76
- package/lib/slides.ts +793 -0
- package/lib/{spelling.js → spelling.ts} +43 -59
- package/lib/{templates.js → templates.ts} +20 -17
- package/lib/themes.ts +742 -0
- package/lib/{trackchanges.js → trackchanges.ts} +52 -23
- package/lib/types.ts +509 -0
- package/lib/{undo.js → undo.ts} +75 -52
- package/lib/utils.ts +41 -0
- package/lib/{variables.js → variables.ts} +60 -54
- package/lib/word.ts +428 -0
- package/lib/{wordcomments.js → wordcomments.ts} +94 -40
- package/package.json +15 -5
- package/skill/REFERENCE.md +67 -0
- package/tsconfig.json +26 -0
- package/lib/annotations.js +0 -414
- package/lib/build.js +0 -639
- package/lib/config.js +0 -79
- package/lib/import.js +0 -1145
- package/lib/journals.js +0 -629
- package/lib/word.js +0 -225
- /package/lib/{scientific-words.js → scientific-words.ts} +0 -0
package/lib/{doi.js → doi.ts}
RENAMED
|
@@ -4,201 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as fs from 'fs';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
// ============================================================================
|
|
11
|
-
// Rate Limiter - Prevents API abuse with exponential backoff
|
|
12
|
-
// ============================================================================
|
|
13
|
-
|
|
14
|
-
class RateLimiter {
|
|
15
|
-
constructor(options = {}) {
|
|
16
|
-
this.minDelay = options.minDelay || 100; // Min delay between requests (ms)
|
|
17
|
-
this.maxDelay = options.maxDelay || 30000; // Max delay after backoff (ms)
|
|
18
|
-
this.maxRetries = options.maxRetries || 3; // Max retry attempts
|
|
19
|
-
this.backoffFactor = options.backoffFactor || 2;
|
|
20
|
-
this.lastRequestTime = 0;
|
|
21
|
-
this.currentDelay = this.minDelay;
|
|
22
|
-
this.consecutiveErrors = 0;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async wait() {
|
|
26
|
-
const now = Date.now();
|
|
27
|
-
const elapsed = now - this.lastRequestTime;
|
|
28
|
-
if (elapsed < this.currentDelay) {
|
|
29
|
-
await new Promise(r => setTimeout(r, this.currentDelay - elapsed));
|
|
30
|
-
}
|
|
31
|
-
this.lastRequestTime = Date.now();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
onSuccess() {
|
|
35
|
-
// Gradually reduce delay on success
|
|
36
|
-
this.consecutiveErrors = 0;
|
|
37
|
-
this.currentDelay = Math.max(this.minDelay, this.currentDelay / this.backoffFactor);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
onError(statusCode) {
|
|
41
|
-
this.consecutiveErrors++;
|
|
42
|
-
// Exponential backoff
|
|
43
|
-
if (statusCode === 429 || statusCode >= 500) {
|
|
44
|
-
this.currentDelay = Math.min(this.maxDelay, this.currentDelay * this.backoffFactor);
|
|
45
|
-
}
|
|
46
|
-
return this.consecutiveErrors <= this.maxRetries;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async fetchWithRetry(url, options = {}) {
|
|
50
|
-
let lastError;
|
|
51
|
-
|
|
52
|
-
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
53
|
-
await this.wait();
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const response = await fetch(url, options);
|
|
57
|
-
|
|
58
|
-
if (response.status === 429) {
|
|
59
|
-
// Rate limited - back off
|
|
60
|
-
const retryAfter = response.headers.get('Retry-After');
|
|
61
|
-
const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : this.currentDelay * 2;
|
|
62
|
-
this.currentDelay = Math.min(this.maxDelay, delay);
|
|
63
|
-
if (!this.onError(429)) break;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (response.status >= 500 && attempt < this.maxRetries) {
|
|
68
|
-
// Server error - retry with backoff
|
|
69
|
-
if (!this.onError(response.status)) break;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
this.onSuccess();
|
|
74
|
-
return response;
|
|
75
|
-
} catch (err) {
|
|
76
|
-
lastError = err;
|
|
77
|
-
if (!this.onError(0)) break;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
throw lastError || new Error('Max retries exceeded');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Shared rate limiters for different APIs
|
|
86
|
-
const crossrefLimiter = new RateLimiter({ minDelay: 100, maxDelay: 10000 });
|
|
87
|
-
const dataciteLimiter = new RateLimiter({ minDelay: 100, maxDelay: 10000 });
|
|
88
|
-
const doiOrgLimiter = new RateLimiter({ minDelay: 200, maxDelay: 15000 });
|
|
89
|
-
|
|
90
|
-
// ============================================================================
|
|
91
|
-
// DOI Cache - Reduces API calls for repeated lookups
|
|
92
|
-
// ============================================================================
|
|
93
|
-
|
|
94
|
-
const CACHE_FILE = path.join(os.homedir(), '.rev-doi-cache.json');
|
|
95
|
-
const CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
|
|
96
|
-
|
|
97
|
-
let doiCache = null;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Load DOI cache from disk
|
|
101
|
-
* @returns {object}
|
|
102
|
-
*/
|
|
103
|
-
function loadCache() {
|
|
104
|
-
if (doiCache !== null) return doiCache;
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
if (fs.existsSync(CACHE_FILE)) {
|
|
108
|
-
const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
|
|
109
|
-
doiCache = data;
|
|
110
|
-
return doiCache;
|
|
111
|
-
}
|
|
112
|
-
} catch {
|
|
113
|
-
// Ignore cache errors
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
doiCache = { entries: {}, version: 1 };
|
|
117
|
-
return doiCache;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Save DOI cache to disk
|
|
122
|
-
*/
|
|
123
|
-
function saveCache() {
|
|
124
|
-
if (!doiCache) return;
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
fs.writeFileSync(CACHE_FILE, JSON.stringify(doiCache, null, 2), 'utf-8');
|
|
128
|
-
} catch {
|
|
129
|
-
// Ignore cache write errors
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Get cached DOI result
|
|
135
|
-
* @param {string} doi
|
|
136
|
-
* @returns {object|null}
|
|
137
|
-
*/
|
|
138
|
-
function getCachedDoi(doi) {
|
|
139
|
-
const cache = loadCache();
|
|
140
|
-
const entry = cache.entries[doi];
|
|
141
|
-
|
|
142
|
-
if (!entry) return null;
|
|
143
|
-
|
|
144
|
-
// Check if cache entry is expired
|
|
145
|
-
if (Date.now() - entry.timestamp > CACHE_TTL) {
|
|
146
|
-
delete cache.entries[doi];
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return entry.result;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Cache a DOI result
|
|
155
|
-
* @param {string} doi
|
|
156
|
-
* @param {object} result
|
|
157
|
-
*/
|
|
158
|
-
function cacheDoi(doi, result) {
|
|
159
|
-
const cache = loadCache();
|
|
160
|
-
cache.entries[doi] = {
|
|
161
|
-
result,
|
|
162
|
-
timestamp: Date.now(),
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
// Limit cache size - remove oldest entries if over 1000
|
|
166
|
-
const entries = Object.entries(cache.entries);
|
|
167
|
-
if (entries.length > 1000) {
|
|
168
|
-
entries
|
|
169
|
-
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
|
170
|
-
.slice(0, entries.length - 800)
|
|
171
|
-
.forEach(([key]) => delete cache.entries[key]);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
saveCache();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Clear the DOI cache
|
|
179
|
-
*/
|
|
180
|
-
export function clearDoiCache() {
|
|
181
|
-
doiCache = { entries: {}, version: 1 };
|
|
182
|
-
try {
|
|
183
|
-
if (fs.existsSync(CACHE_FILE)) {
|
|
184
|
-
fs.unlinkSync(CACHE_FILE);
|
|
185
|
-
}
|
|
186
|
-
} catch {
|
|
187
|
-
// Ignore
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Get DOI cache statistics
|
|
193
|
-
* @returns {{ size: number, path: string }}
|
|
194
|
-
*/
|
|
195
|
-
export function getDoiCacheStats() {
|
|
196
|
-
const cache = loadCache();
|
|
197
|
-
return {
|
|
198
|
-
size: Object.keys(cache.entries).length,
|
|
199
|
-
path: CACHE_FILE,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
7
|
+
import type { BibEntry, DoiCheckResult, BibtexFetchResult, DoiLookupResult, BibCheckResult } from './types.js';
|
|
8
|
+
import { crossrefLimiter, dataciteLimiter, doiOrgLimiter } from './rate-limiter.js';
|
|
9
|
+
import { getCachedDoi, cacheDoi } from './doi-cache.js';
|
|
202
10
|
|
|
203
11
|
// Entry types that typically don't have DOIs
|
|
204
12
|
const NO_DOI_TYPES = new Set([
|
|
@@ -224,25 +32,23 @@ const EXPECT_DOI_TYPES = new Set([
|
|
|
224
32
|
|
|
225
33
|
/**
|
|
226
34
|
* Parse .bib file and extract entries with DOI info
|
|
227
|
-
* @param {string} bibPath
|
|
228
|
-
* @returns {Array<{key: string, type: string, doi: string|null, title: string, skip: boolean, line: number}>}
|
|
229
35
|
*/
|
|
230
|
-
export function parseBibEntries(bibPath) {
|
|
36
|
+
export function parseBibEntries(bibPath: string): BibEntry[] {
|
|
231
37
|
if (!fs.existsSync(bibPath)) {
|
|
232
38
|
return [];
|
|
233
39
|
}
|
|
234
40
|
|
|
235
41
|
const content = fs.readFileSync(bibPath, 'utf-8');
|
|
236
|
-
const entries = [];
|
|
42
|
+
const entries: BibEntry[] = [];
|
|
237
43
|
const lines = content.split('\n');
|
|
238
44
|
|
|
239
45
|
// Pattern for bib entries: @type{key,
|
|
240
46
|
const entryPattern = /@(\w+)\s*\{\s*([^,\s]+)\s*,/g;
|
|
241
47
|
|
|
242
|
-
let match;
|
|
48
|
+
let match: RegExpExecArray | null;
|
|
243
49
|
while ((match = entryPattern.exec(content)) !== null) {
|
|
244
|
-
const type = match[1]
|
|
245
|
-
const key = match[2]
|
|
50
|
+
const type = match[1]!.toLowerCase();
|
|
51
|
+
const key = match[2]!;
|
|
246
52
|
const startPos = match.index;
|
|
247
53
|
|
|
248
54
|
// Find the line number
|
|
@@ -273,7 +79,7 @@ export function parseBibEntries(bibPath) {
|
|
|
273
79
|
|
|
274
80
|
// Extract DOI field
|
|
275
81
|
const doiMatch = entryContent.match(/\bdoi\s*=\s*[{"]([^}"]+)[}"]/i);
|
|
276
|
-
let doi = doiMatch ? doiMatch[1]
|
|
82
|
+
let doi = doiMatch ? doiMatch[1]!.trim() : null;
|
|
277
83
|
|
|
278
84
|
// Clean DOI - remove URL prefix if present
|
|
279
85
|
if (doi) {
|
|
@@ -282,19 +88,19 @@ export function parseBibEntries(bibPath) {
|
|
|
282
88
|
|
|
283
89
|
// Extract title for display
|
|
284
90
|
const titleMatch = entryContent.match(/\btitle\s*=\s*[{"]([^}"]+)[}"]/i);
|
|
285
|
-
const title = titleMatch ? titleMatch[1]
|
|
91
|
+
const title = titleMatch ? titleMatch[1]!.trim().slice(0, 60) : '';
|
|
286
92
|
|
|
287
93
|
// Extract author for lookup
|
|
288
94
|
const authorMatch = entryContent.match(/\bauthor\s*=\s*[{"]([^}"]+)[}"]/i);
|
|
289
|
-
const authorRaw = authorMatch ? authorMatch[1]
|
|
95
|
+
const authorRaw = authorMatch ? authorMatch[1]!.trim() : '';
|
|
290
96
|
|
|
291
97
|
// Extract year
|
|
292
98
|
const yearMatch = entryContent.match(/\byear\s*=\s*[{"]?(\d{4})[}""]?/i);
|
|
293
|
-
const year = yearMatch ? parseInt(yearMatch[1]) : null;
|
|
99
|
+
const year = yearMatch ? parseInt(yearMatch[1]!) : null;
|
|
294
100
|
|
|
295
101
|
// Extract journal
|
|
296
102
|
const journalMatch = entryContent.match(/\bjournal\s*=\s*[{"]([^}"]+)[}"]/i);
|
|
297
|
-
const journal = journalMatch ? journalMatch[1]
|
|
103
|
+
const journal = journalMatch ? journalMatch[1]!.trim() : '';
|
|
298
104
|
|
|
299
105
|
// Check for skip marker: nodoi = {true} or nodoi = true
|
|
300
106
|
const skipMatch = entryContent.match(/\bnodoi\s*=\s*[{"]?(true|yes|1)[}""]?/i);
|
|
@@ -311,7 +117,7 @@ export function parseBibEntries(bibPath) {
|
|
|
311
117
|
entries.push({
|
|
312
118
|
key,
|
|
313
119
|
type,
|
|
314
|
-
doi,
|
|
120
|
+
doi: doi || null,
|
|
315
121
|
title,
|
|
316
122
|
authorRaw,
|
|
317
123
|
year,
|
|
@@ -328,10 +134,8 @@ export function parseBibEntries(bibPath) {
|
|
|
328
134
|
|
|
329
135
|
/**
|
|
330
136
|
* Validate DOI format
|
|
331
|
-
* @param {string} doi
|
|
332
|
-
* @returns {boolean}
|
|
333
137
|
*/
|
|
334
|
-
export function isValidDoiFormat(doi) {
|
|
138
|
+
export function isValidDoiFormat(doi: string): boolean {
|
|
335
139
|
if (!doi) return false;
|
|
336
140
|
// DOI format: 10.prefix/suffix
|
|
337
141
|
// Prefix is 4+ digits, suffix can contain most characters
|
|
@@ -340,10 +144,8 @@ export function isValidDoiFormat(doi) {
|
|
|
340
144
|
|
|
341
145
|
/**
|
|
342
146
|
* Check if DOI resolves via DataCite (for Zenodo, Figshare, etc.)
|
|
343
|
-
* @param {string} doi
|
|
344
|
-
* @returns {Promise<{valid: boolean, metadata?: object, error?: string}>}
|
|
345
147
|
*/
|
|
346
|
-
async function checkDoiDataCite(doi) {
|
|
148
|
+
async function checkDoiDataCite(doi: string): Promise<DoiCheckResult> {
|
|
347
149
|
try {
|
|
348
150
|
const response = await dataciteLimiter.fetchWithRetry(
|
|
349
151
|
`https://api.datacite.org/dois/${encodeURIComponent(doi)}`,
|
|
@@ -363,7 +165,7 @@ async function checkDoiDataCite(doi) {
|
|
|
363
165
|
return { valid: false, error: `HTTP ${response.status}` };
|
|
364
166
|
}
|
|
365
167
|
|
|
366
|
-
const data = await response.json();
|
|
168
|
+
const data = await response.json() as any;
|
|
367
169
|
const attrs = data.data?.attributes;
|
|
368
170
|
|
|
369
171
|
if (!attrs) {
|
|
@@ -375,26 +177,26 @@ async function checkDoiDataCite(doi) {
|
|
|
375
177
|
source: 'datacite',
|
|
376
178
|
metadata: {
|
|
377
179
|
title: attrs.titles?.[0]?.title || '',
|
|
378
|
-
authors: attrs.creators?.map(c => `${c.givenName || ''} ${c.familyName || ''}`.trim()) || [],
|
|
180
|
+
authors: attrs.creators?.map((c: any) => `${c.givenName || ''} ${c.familyName || ''}`.trim()) || [],
|
|
379
181
|
year: attrs.publicationYear,
|
|
380
182
|
journal: attrs.publisher || '',
|
|
381
183
|
type: attrs.types?.resourceTypeGeneral || '',
|
|
382
184
|
},
|
|
383
185
|
};
|
|
384
186
|
} catch (err) {
|
|
385
|
-
return { valid: false, error: err.message };
|
|
187
|
+
return { valid: false, error: (err as Error).message };
|
|
386
188
|
}
|
|
387
189
|
}
|
|
388
190
|
|
|
191
|
+
interface CheckDoiOptions {
|
|
192
|
+
skipCache?: boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
389
195
|
/**
|
|
390
196
|
* Check if DOI resolves (exists) - tries Crossref first, then DataCite
|
|
391
197
|
* Results are cached for 7 days to reduce API calls.
|
|
392
|
-
* @param {string} doi
|
|
393
|
-
* @param {object} options
|
|
394
|
-
* @param {boolean} options.skipCache - Skip cache lookup
|
|
395
|
-
* @returns {Promise<{valid: boolean, source?: string, metadata?: object, error?: string, cached?: boolean}>}
|
|
396
198
|
*/
|
|
397
|
-
export async function checkDoi(doi, options = {}) {
|
|
199
|
+
export async function checkDoi(doi: string, options: CheckDoiOptions = {}): Promise<DoiCheckResult & { cached?: boolean }> {
|
|
398
200
|
if (!isValidDoiFormat(doi)) {
|
|
399
201
|
return { valid: false, error: 'Invalid DOI format' };
|
|
400
202
|
}
|
|
@@ -403,7 +205,7 @@ export async function checkDoi(doi, options = {}) {
|
|
|
403
205
|
if (!options.skipCache) {
|
|
404
206
|
const cached = getCachedDoi(doi);
|
|
405
207
|
if (cached) {
|
|
406
|
-
return { ...cached, cached: true };
|
|
208
|
+
return { ...cached, cached: true } as DoiCheckResult & { cached?: boolean };
|
|
407
209
|
}
|
|
408
210
|
}
|
|
409
211
|
|
|
@@ -450,15 +252,15 @@ export async function checkDoi(doi, options = {}) {
|
|
|
450
252
|
return { valid: false, error: `HTTP ${response.status}` };
|
|
451
253
|
}
|
|
452
254
|
|
|
453
|
-
const data = await response.json();
|
|
255
|
+
const data = await response.json() as any;
|
|
454
256
|
const work = data.message;
|
|
455
257
|
|
|
456
|
-
const result = {
|
|
258
|
+
const result: DoiCheckResult = {
|
|
457
259
|
valid: true,
|
|
458
260
|
source: 'crossref',
|
|
459
261
|
metadata: {
|
|
460
262
|
title: work.title?.[0] || '',
|
|
461
|
-
authors: work.author?.map(a => `${a.given || ''} ${a.family || ''}`.trim()) || [],
|
|
263
|
+
authors: work.author?.map((a: any) => `${a.given || ''} ${a.family || ''}`.trim()) || [],
|
|
462
264
|
year: work.published?.['date-parts']?.[0]?.[0] || work.created?.['date-parts']?.[0]?.[0],
|
|
463
265
|
journal: work['container-title']?.[0] || '',
|
|
464
266
|
type: work.type,
|
|
@@ -469,16 +271,14 @@ export async function checkDoi(doi, options = {}) {
|
|
|
469
271
|
return result;
|
|
470
272
|
} catch (err) {
|
|
471
273
|
// Don't cache network errors
|
|
472
|
-
return { valid: false, error: err.message };
|
|
274
|
+
return { valid: false, error: (err as Error).message };
|
|
473
275
|
}
|
|
474
276
|
}
|
|
475
277
|
|
|
476
278
|
/**
|
|
477
279
|
* Fetch BibTeX from DOI using content negotiation
|
|
478
|
-
* @param {string} doi
|
|
479
|
-
* @returns {Promise<{success: boolean, bibtex?: string, error?: string}>}
|
|
480
280
|
*/
|
|
481
|
-
export async function fetchBibtex(doi) {
|
|
281
|
+
export async function fetchBibtex(doi: string): Promise<BibtexFetchResult> {
|
|
482
282
|
// Clean DOI
|
|
483
283
|
doi = doi.replace(/^https?:\/\/(dx\.)?doi\.org\//i, '');
|
|
484
284
|
|
|
@@ -510,21 +310,23 @@ export async function fetchBibtex(doi) {
|
|
|
510
310
|
|
|
511
311
|
return { success: true, bibtex: bibtex.trim() };
|
|
512
312
|
} catch (err) {
|
|
513
|
-
return { success: false, error: err.message };
|
|
313
|
+
return { success: false, error: (err as Error).message };
|
|
514
314
|
}
|
|
515
315
|
}
|
|
516
316
|
|
|
317
|
+
interface CheckBibDoisOptions {
|
|
318
|
+
checkMissing?: boolean;
|
|
319
|
+
parallel?: number;
|
|
320
|
+
}
|
|
321
|
+
|
|
517
322
|
/**
|
|
518
323
|
* Check all DOIs in a .bib file
|
|
519
|
-
* @param {string} bibPath
|
|
520
|
-
* @param {object} options
|
|
521
|
-
* @returns {Promise<{entries: Array, valid: number, invalid: number, missing: number, skipped: number}>}
|
|
522
324
|
*/
|
|
523
|
-
export async function checkBibDois(bibPath, options = {}) {
|
|
325
|
+
export async function checkBibDois(bibPath: string, options: CheckBibDoisOptions = {}): Promise<BibCheckResult> {
|
|
524
326
|
const { checkMissing = false, parallel = 5 } = options;
|
|
525
327
|
|
|
526
328
|
const entries = parseBibEntries(bibPath);
|
|
527
|
-
const results = [];
|
|
329
|
+
const results: Array<BibEntry & { status: string; message?: string; metadata?: object }> = [];
|
|
528
330
|
|
|
529
331
|
let valid = 0;
|
|
530
332
|
let invalid = 0;
|
|
@@ -588,14 +390,20 @@ export async function checkBibDois(bibPath, options = {}) {
|
|
|
588
390
|
return { entries: results, valid, invalid, missing, skipped };
|
|
589
391
|
}
|
|
590
392
|
|
|
393
|
+
interface DataCiteItem {
|
|
394
|
+
id: string;
|
|
395
|
+
attributes: {
|
|
396
|
+
titles?: Array<{ title: string }>;
|
|
397
|
+
creators?: Array<{ givenName?: string; familyName?: string }>;
|
|
398
|
+
publicationYear: number;
|
|
399
|
+
publisher?: string;
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
591
403
|
/**
|
|
592
404
|
* Search DataCite API (for Zenodo, Figshare, etc.)
|
|
593
|
-
* @param {string} title
|
|
594
|
-
* @param {string} author
|
|
595
|
-
* @param {number} year
|
|
596
|
-
* @returns {Promise<Array>}
|
|
597
405
|
*/
|
|
598
|
-
async function searchDataCite(title, author = '', year = null) {
|
|
406
|
+
async function searchDataCite(title: string, author: string = '', year: number | null = null): Promise<any[]> {
|
|
599
407
|
try {
|
|
600
408
|
// DataCite query syntax
|
|
601
409
|
let query = `titles.title:${title.replace(/[{}]/g, '')}`;
|
|
@@ -623,7 +431,7 @@ async function searchDataCite(title, author = '', year = null) {
|
|
|
623
431
|
|
|
624
432
|
if (!response.ok) return [];
|
|
625
433
|
|
|
626
|
-
const data = await response.json();
|
|
434
|
+
const data = await response.json() as { data?: DataCiteItem[] };
|
|
627
435
|
const items = data.data || [];
|
|
628
436
|
|
|
629
437
|
return items.map(item => {
|
|
@@ -645,10 +453,8 @@ async function searchDataCite(title, author = '', year = null) {
|
|
|
645
453
|
|
|
646
454
|
/**
|
|
647
455
|
* Normalize text for comparison (lowercase, remove special chars)
|
|
648
|
-
* @param {string} text
|
|
649
|
-
* @returns {string}
|
|
650
456
|
*/
|
|
651
|
-
function
|
|
457
|
+
function normalizeForMatching(text: string): string {
|
|
652
458
|
return (text || '')
|
|
653
459
|
.toLowerCase()
|
|
654
460
|
.replace(/[{}\\]/g, '') // Remove LaTeX braces
|
|
@@ -659,12 +465,8 @@ function normalizeText(text) {
|
|
|
659
465
|
|
|
660
466
|
/**
|
|
661
467
|
* Check if DOI looks like a supplement, figure, or review (not the main paper)
|
|
662
|
-
* @param {string} doi
|
|
663
|
-
* @param {string} title
|
|
664
|
-
* @param {string} journal
|
|
665
|
-
* @returns {boolean}
|
|
666
468
|
*/
|
|
667
|
-
function isSupplementOrReview(doi, title = '', journal = '') {
|
|
469
|
+
function isSupplementOrReview(doi: string, title: string = '', journal: string = ''): boolean {
|
|
668
470
|
const doiLower = (doi || '').toLowerCase();
|
|
669
471
|
const titleLower = (title || '').toLowerCase();
|
|
670
472
|
const journalLower = (journal || '').toLowerCase();
|
|
@@ -687,15 +489,26 @@ function isSupplementOrReview(doi, title = '', journal = '') {
|
|
|
687
489
|
return false;
|
|
688
490
|
}
|
|
689
491
|
|
|
492
|
+
interface CrossrefItem {
|
|
493
|
+
DOI: string;
|
|
494
|
+
title?: string[];
|
|
495
|
+
author?: Array<{ given?: string; family?: string }>;
|
|
496
|
+
'published-print'?: { 'date-parts': number[][] };
|
|
497
|
+
'published-online'?: { 'date-parts': number[][] };
|
|
498
|
+
'container-title'?: string[];
|
|
499
|
+
score?: number;
|
|
500
|
+
type?: string;
|
|
501
|
+
}
|
|
502
|
+
|
|
690
503
|
/**
|
|
691
504
|
* Search for DOI by title and author using Crossref API (+ DataCite fallback)
|
|
692
|
-
* @param {string} title
|
|
693
|
-
* @param {string} author - First author's last name
|
|
694
|
-
* @param {number} year - Publication year (optional, improves accuracy)
|
|
695
|
-
* @param {string} journal - Expected journal name (optional, improves accuracy)
|
|
696
|
-
* @returns {Promise<{found: boolean, doi?: string, confidence?: number, metadata?: object, error?: string}>}
|
|
697
505
|
*/
|
|
698
|
-
export async function lookupDoi(
|
|
506
|
+
export async function lookupDoi(
|
|
507
|
+
title: string,
|
|
508
|
+
author: string = '',
|
|
509
|
+
year: number | null = null,
|
|
510
|
+
journal: string = ''
|
|
511
|
+
): Promise<DoiLookupResult> {
|
|
699
512
|
if (!title || title.length < 10) {
|
|
700
513
|
return { found: false, error: 'Title too short for reliable search' };
|
|
701
514
|
}
|
|
@@ -714,7 +527,7 @@ export async function lookupDoi(title, author = '', year = null, journal = '') {
|
|
|
714
527
|
query = `${query} ${journal}`;
|
|
715
528
|
}
|
|
716
529
|
|
|
717
|
-
let items = [];
|
|
530
|
+
let items: CrossrefItem[] = [];
|
|
718
531
|
|
|
719
532
|
// Try structured bibliographic query first (more accurate)
|
|
720
533
|
const structuredParams = new URLSearchParams({
|
|
@@ -739,7 +552,7 @@ export async function lookupDoi(title, author = '', year = null, journal = '') {
|
|
|
739
552
|
);
|
|
740
553
|
|
|
741
554
|
if (response.ok) {
|
|
742
|
-
const data = await response.json();
|
|
555
|
+
const data = await response.json() as { message?: { items?: CrossrefItem[] } };
|
|
743
556
|
items = data.message?.items || [];
|
|
744
557
|
}
|
|
745
558
|
|
|
@@ -761,7 +574,7 @@ export async function lookupDoi(title, author = '', year = null, journal = '') {
|
|
|
761
574
|
);
|
|
762
575
|
|
|
763
576
|
if (response2.ok) {
|
|
764
|
-
const data = await response2.json();
|
|
577
|
+
const data = await response2.json() as { message?: { items?: CrossrefItem[] } };
|
|
765
578
|
const newItems = data.message?.items || [];
|
|
766
579
|
// Merge results, avoiding duplicates
|
|
767
580
|
const existingDois = new Set(items.map(i => i.DOI));
|
|
@@ -791,7 +604,7 @@ export async function lookupDoi(title, author = '', year = null, journal = '') {
|
|
|
791
604
|
);
|
|
792
605
|
|
|
793
606
|
if (response.ok) {
|
|
794
|
-
const data = await response.json();
|
|
607
|
+
const data = await response.json() as { message?: { items?: CrossrefItem[] } };
|
|
795
608
|
items = data.message?.items || [];
|
|
796
609
|
}
|
|
797
610
|
}
|
|
@@ -806,16 +619,16 @@ export async function lookupDoi(title, author = '', year = null, journal = '') {
|
|
|
806
619
|
return { found: false, error: 'No results found' };
|
|
807
620
|
}
|
|
808
621
|
|
|
809
|
-
const normalizedSearchTitle =
|
|
810
|
-
const normalizedJournal =
|
|
622
|
+
const normalizedSearchTitle = normalizeForMatching(title);
|
|
623
|
+
const normalizedJournal = normalizeForMatching(journal);
|
|
811
624
|
|
|
812
625
|
// Score the results
|
|
813
626
|
const scored = items.map(item => {
|
|
814
627
|
let score = 0;
|
|
815
628
|
const itemTitle = item.title?.[0] || '';
|
|
816
629
|
const itemJournal = item['container-title']?.[0] || '';
|
|
817
|
-
const normalizedItemTitle =
|
|
818
|
-
const normalizedItemJournal =
|
|
630
|
+
const normalizedItemTitle = normalizeForMatching(itemTitle);
|
|
631
|
+
const normalizedItemJournal = normalizeForMatching(itemJournal);
|
|
819
632
|
|
|
820
633
|
// === PENALTY: Supplement/figure/review DOIs ===
|
|
821
634
|
if (isSupplementOrReview(item.DOI, itemTitle, itemJournal)) {
|
|
@@ -908,8 +721,12 @@ export async function lookupDoi(title, author = '', year = null, journal = '') {
|
|
|
908
721
|
const mainPapers = scored.filter(s => !s.isSupplement);
|
|
909
722
|
const best = mainPapers.length > 0 ? mainPapers[0] : scored[0];
|
|
910
723
|
|
|
724
|
+
if (!best) {
|
|
725
|
+
return { found: false, error: 'No valid results found' };
|
|
726
|
+
}
|
|
727
|
+
|
|
911
728
|
// Confidence thresholds
|
|
912
|
-
let confidence = 'low';
|
|
729
|
+
let confidence: 'low' | 'medium' | 'high' = 'low';
|
|
913
730
|
if (best.score >= 120) confidence = 'high';
|
|
914
731
|
else if (best.score >= 70) confidence = 'medium';
|
|
915
732
|
|
|
@@ -920,7 +737,7 @@ export async function lookupDoi(title, author = '', year = null, journal = '') {
|
|
|
920
737
|
// Score DataCite results with same logic
|
|
921
738
|
for (const dcItem of dataciteItems) {
|
|
922
739
|
const dcTitle = dcItem.title?.[0] || '';
|
|
923
|
-
const normalizedDcTitle =
|
|
740
|
+
const normalizedDcTitle = normalizeForMatching(dcTitle);
|
|
924
741
|
let dcScore = 0;
|
|
925
742
|
|
|
926
743
|
// Title match
|
|
@@ -945,12 +762,11 @@ export async function lookupDoi(title, author = '', year = null, journal = '') {
|
|
|
945
762
|
score: dcScore,
|
|
946
763
|
metadata: {
|
|
947
764
|
title: dcTitle,
|
|
948
|
-
authors: dcItem.author?.map(a => `${a.given || ''} ${a.family || ''}`.trim()) || [],
|
|
765
|
+
authors: dcItem.author?.map((a: any) => `${a.given || ''} ${a.family || ''}`.trim()) || [],
|
|
949
766
|
year: dcYear,
|
|
950
767
|
journal: dcItem['container-title']?.[0] || '',
|
|
951
768
|
},
|
|
952
769
|
alternatives: scored.slice(0, 2),
|
|
953
|
-
source: 'datacite',
|
|
954
770
|
};
|
|
955
771
|
}
|
|
956
772
|
}
|
|
@@ -965,23 +781,36 @@ export async function lookupDoi(title, author = '', year = null, journal = '') {
|
|
|
965
781
|
metadata: {
|
|
966
782
|
title: best.title,
|
|
967
783
|
authors: best.authors,
|
|
968
|
-
year: best.year,
|
|
784
|
+
year: best.year || 0,
|
|
969
785
|
journal: best.journal,
|
|
970
786
|
},
|
|
971
787
|
alternatives: scored.filter(s => s.doi !== best.doi).slice(0, 3),
|
|
972
788
|
};
|
|
973
789
|
} catch (err) {
|
|
974
|
-
return { found: false, error: err.message };
|
|
790
|
+
return { found: false, error: (err as Error).message };
|
|
975
791
|
}
|
|
976
792
|
}
|
|
977
793
|
|
|
794
|
+
interface LookupMissingDoisOptions {
|
|
795
|
+
parallel?: number;
|
|
796
|
+
onProgress?: (current: number, total: number) => void;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
interface LookupMissingDoiResult {
|
|
800
|
+
key: string;
|
|
801
|
+
title: string;
|
|
802
|
+
type: string;
|
|
803
|
+
journal: string;
|
|
804
|
+
result: DoiLookupResult;
|
|
805
|
+
}
|
|
806
|
+
|
|
978
807
|
/**
|
|
979
808
|
* Look up DOIs for all entries missing them in a .bib file
|
|
980
|
-
* @param {string} bibPath
|
|
981
|
-
* @param {object} options
|
|
982
|
-
* @returns {Promise<Array<{key: string, result: object}>>}
|
|
983
809
|
*/
|
|
984
|
-
export async function lookupMissingDois(
|
|
810
|
+
export async function lookupMissingDois(
|
|
811
|
+
bibPath: string,
|
|
812
|
+
options: LookupMissingDoisOptions = {}
|
|
813
|
+
): Promise<LookupMissingDoiResult[]> {
|
|
985
814
|
const { parallel = 3, onProgress } = options;
|
|
986
815
|
|
|
987
816
|
const entries = parseBibEntries(bibPath);
|
|
@@ -991,7 +820,7 @@ export async function lookupMissingDois(bibPath, options = {}) {
|
|
|
991
820
|
!NO_DOI_TYPES.has(e.type)
|
|
992
821
|
);
|
|
993
822
|
|
|
994
|
-
const results = [];
|
|
823
|
+
const results: LookupMissingDoiResult[] = [];
|
|
995
824
|
|
|
996
825
|
for (let i = 0; i < missing.length; i += parallel) {
|
|
997
826
|
const batch = missing.slice(i, i + parallel);
|
|
@@ -1004,8 +833,10 @@ export async function lookupMissingDois(bibPath, options = {}) {
|
|
|
1004
833
|
if (entry.authorRaw) {
|
|
1005
834
|
// Try to get first author's last name
|
|
1006
835
|
const firstAuthor = entry.authorRaw.split(' and ')[0];
|
|
1007
|
-
|
|
1008
|
-
|
|
836
|
+
if (firstAuthor) {
|
|
837
|
+
const parts = firstAuthor.split(',');
|
|
838
|
+
author = parts[0]?.trim() || '';
|
|
839
|
+
}
|
|
1009
840
|
}
|
|
1010
841
|
|
|
1011
842
|
const result = await lookupDoi(entry.title, author, entry.year, entry.journal);
|
|
@@ -1035,13 +866,16 @@ export async function lookupMissingDois(bibPath, options = {}) {
|
|
|
1035
866
|
return results;
|
|
1036
867
|
}
|
|
1037
868
|
|
|
869
|
+
interface AddToBibResult {
|
|
870
|
+
success: boolean;
|
|
871
|
+
key?: string;
|
|
872
|
+
error?: string;
|
|
873
|
+
}
|
|
874
|
+
|
|
1038
875
|
/**
|
|
1039
876
|
* Add a BibTeX entry to a .bib file
|
|
1040
|
-
* @param {string} bibPath
|
|
1041
|
-
* @param {string} bibtex
|
|
1042
|
-
* @returns {{success: boolean, key?: string, error?: string}}
|
|
1043
877
|
*/
|
|
1044
|
-
export function addToBib(bibPath, bibtex) {
|
|
878
|
+
export function addToBib(bibPath: string, bibtex: string): AddToBibResult {
|
|
1045
879
|
// Extract key from BibTeX
|
|
1046
880
|
const keyMatch = bibtex.match(/@\w+\s*\{\s*([^,\s]+)/);
|
|
1047
881
|
if (!keyMatch) {
|