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.
Files changed (306) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/PLAN-tables-and-postprocess.md +850 -0
  3. package/README.md +33 -0
  4. package/bin/rev.js +12 -131
  5. package/bin/rev.ts +145 -0
  6. package/dist/bin/rev.d.ts +9 -0
  7. package/dist/bin/rev.d.ts.map +1 -0
  8. package/dist/bin/rev.js +118 -0
  9. package/dist/bin/rev.js.map +1 -0
  10. package/dist/lib/annotations.d.ts +91 -0
  11. package/dist/lib/annotations.d.ts.map +1 -0
  12. package/dist/lib/annotations.js +554 -0
  13. package/dist/lib/annotations.js.map +1 -0
  14. package/dist/lib/build.d.ts +171 -0
  15. package/dist/lib/build.d.ts.map +1 -0
  16. package/dist/lib/build.js +755 -0
  17. package/dist/lib/build.js.map +1 -0
  18. package/dist/lib/citations.d.ts +34 -0
  19. package/dist/lib/citations.d.ts.map +1 -0
  20. package/dist/lib/citations.js +140 -0
  21. package/dist/lib/citations.js.map +1 -0
  22. package/dist/lib/commands/build.d.ts +13 -0
  23. package/dist/lib/commands/build.d.ts.map +1 -0
  24. package/dist/lib/commands/build.js +678 -0
  25. package/dist/lib/commands/build.js.map +1 -0
  26. package/dist/lib/commands/citations.d.ts +11 -0
  27. package/dist/lib/commands/citations.d.ts.map +1 -0
  28. package/dist/lib/commands/citations.js +428 -0
  29. package/dist/lib/commands/citations.js.map +1 -0
  30. package/dist/lib/commands/comments.d.ts +11 -0
  31. package/dist/lib/commands/comments.d.ts.map +1 -0
  32. package/dist/lib/commands/comments.js +883 -0
  33. package/dist/lib/commands/comments.js.map +1 -0
  34. package/dist/lib/commands/context.d.ts +35 -0
  35. package/dist/lib/commands/context.d.ts.map +1 -0
  36. package/dist/lib/commands/context.js +59 -0
  37. package/dist/lib/commands/context.js.map +1 -0
  38. package/dist/lib/commands/core.d.ts +11 -0
  39. package/dist/lib/commands/core.d.ts.map +1 -0
  40. package/dist/lib/commands/core.js +246 -0
  41. package/dist/lib/commands/core.js.map +1 -0
  42. package/dist/lib/commands/doi.d.ts +11 -0
  43. package/dist/lib/commands/doi.d.ts.map +1 -0
  44. package/dist/lib/commands/doi.js +373 -0
  45. package/dist/lib/commands/doi.js.map +1 -0
  46. package/dist/lib/commands/history.d.ts +11 -0
  47. package/dist/lib/commands/history.d.ts.map +1 -0
  48. package/dist/lib/commands/history.js +245 -0
  49. package/dist/lib/commands/history.js.map +1 -0
  50. package/dist/lib/commands/index.d.ts +28 -0
  51. package/dist/lib/commands/index.d.ts.map +1 -0
  52. package/dist/lib/commands/index.js +35 -0
  53. package/dist/lib/commands/index.js.map +1 -0
  54. package/dist/lib/commands/init.d.ts +11 -0
  55. package/dist/lib/commands/init.d.ts.map +1 -0
  56. package/dist/lib/commands/init.js +209 -0
  57. package/dist/lib/commands/init.js.map +1 -0
  58. package/dist/lib/commands/response.d.ts +11 -0
  59. package/dist/lib/commands/response.d.ts.map +1 -0
  60. package/dist/lib/commands/response.js +317 -0
  61. package/dist/lib/commands/response.js.map +1 -0
  62. package/dist/lib/commands/sections.d.ts +11 -0
  63. package/dist/lib/commands/sections.d.ts.map +1 -0
  64. package/dist/lib/commands/sections.js +1071 -0
  65. package/dist/lib/commands/sections.js.map +1 -0
  66. package/dist/lib/commands/utilities.d.ts +19 -0
  67. package/dist/lib/commands/utilities.d.ts.map +1 -0
  68. package/dist/lib/commands/utilities.js +2009 -0
  69. package/dist/lib/commands/utilities.js.map +1 -0
  70. package/dist/lib/comment-realign.d.ts +50 -0
  71. package/dist/lib/comment-realign.d.ts.map +1 -0
  72. package/dist/lib/comment-realign.js +372 -0
  73. package/dist/lib/comment-realign.js.map +1 -0
  74. package/dist/lib/config.d.ts +41 -0
  75. package/dist/lib/config.d.ts.map +1 -0
  76. package/dist/lib/config.js +76 -0
  77. package/dist/lib/config.js.map +1 -0
  78. package/dist/lib/crossref.d.ts +108 -0
  79. package/dist/lib/crossref.d.ts.map +1 -0
  80. package/dist/lib/crossref.js +597 -0
  81. package/dist/lib/crossref.js.map +1 -0
  82. package/dist/lib/dependencies.d.ts +30 -0
  83. package/dist/lib/dependencies.d.ts.map +1 -0
  84. package/dist/lib/dependencies.js +95 -0
  85. package/dist/lib/dependencies.js.map +1 -0
  86. package/dist/lib/doi-cache.d.ts +29 -0
  87. package/dist/lib/doi-cache.d.ts.map +1 -0
  88. package/dist/lib/doi-cache.js +104 -0
  89. package/dist/lib/doi-cache.js.map +1 -0
  90. package/dist/lib/doi.d.ts +65 -0
  91. package/dist/lib/doi.d.ts.map +1 -0
  92. package/dist/lib/doi.js +710 -0
  93. package/dist/lib/doi.js.map +1 -0
  94. package/dist/lib/equations.d.ts +61 -0
  95. package/dist/lib/equations.d.ts.map +1 -0
  96. package/dist/lib/equations.js +445 -0
  97. package/dist/lib/equations.js.map +1 -0
  98. package/dist/lib/errors.d.ts +60 -0
  99. package/dist/lib/errors.d.ts.map +1 -0
  100. package/dist/lib/errors.js +303 -0
  101. package/dist/lib/errors.js.map +1 -0
  102. package/dist/lib/format.d.ts +104 -0
  103. package/dist/lib/format.d.ts.map +1 -0
  104. package/dist/lib/format.js +416 -0
  105. package/dist/lib/format.js.map +1 -0
  106. package/dist/lib/git.d.ts +88 -0
  107. package/dist/lib/git.d.ts.map +1 -0
  108. package/dist/lib/git.js +304 -0
  109. package/dist/lib/git.js.map +1 -0
  110. package/dist/lib/grammar.d.ts +62 -0
  111. package/dist/lib/grammar.d.ts.map +1 -0
  112. package/dist/lib/grammar.js +244 -0
  113. package/dist/lib/grammar.js.map +1 -0
  114. package/dist/lib/image-registry.d.ts +68 -0
  115. package/dist/lib/image-registry.d.ts.map +1 -0
  116. package/dist/lib/image-registry.js +112 -0
  117. package/dist/lib/image-registry.js.map +1 -0
  118. package/dist/lib/import.d.ts +184 -0
  119. package/dist/lib/import.d.ts.map +1 -0
  120. package/dist/lib/import.js +1581 -0
  121. package/dist/lib/import.js.map +1 -0
  122. package/dist/lib/journals.d.ts +55 -0
  123. package/dist/lib/journals.d.ts.map +1 -0
  124. package/dist/lib/journals.js +417 -0
  125. package/dist/lib/journals.js.map +1 -0
  126. package/dist/lib/merge.d.ts +138 -0
  127. package/dist/lib/merge.d.ts.map +1 -0
  128. package/dist/lib/merge.js +603 -0
  129. package/dist/lib/merge.js.map +1 -0
  130. package/dist/lib/orcid.d.ts +36 -0
  131. package/dist/lib/orcid.d.ts.map +1 -0
  132. package/dist/lib/orcid.js +117 -0
  133. package/dist/lib/orcid.js.map +1 -0
  134. package/dist/lib/pdf-comments.d.ts +95 -0
  135. package/dist/lib/pdf-comments.d.ts.map +1 -0
  136. package/dist/lib/pdf-comments.js +192 -0
  137. package/dist/lib/pdf-comments.js.map +1 -0
  138. package/dist/lib/pdf-import.d.ts +118 -0
  139. package/dist/lib/pdf-import.d.ts.map +1 -0
  140. package/dist/lib/pdf-import.js +397 -0
  141. package/dist/lib/pdf-import.js.map +1 -0
  142. package/dist/lib/plugins.d.ts +76 -0
  143. package/dist/lib/plugins.d.ts.map +1 -0
  144. package/dist/lib/plugins.js +235 -0
  145. package/dist/lib/plugins.js.map +1 -0
  146. package/dist/lib/postprocess.d.ts +42 -0
  147. package/dist/lib/postprocess.d.ts.map +1 -0
  148. package/dist/lib/postprocess.js +138 -0
  149. package/dist/lib/postprocess.js.map +1 -0
  150. package/dist/lib/pptx-template.d.ts +59 -0
  151. package/dist/lib/pptx-template.d.ts.map +1 -0
  152. package/dist/lib/pptx-template.js +613 -0
  153. package/dist/lib/pptx-template.js.map +1 -0
  154. package/dist/lib/pptx-themes.d.ts +80 -0
  155. package/dist/lib/pptx-themes.d.ts.map +1 -0
  156. package/dist/lib/pptx-themes.js +818 -0
  157. package/dist/lib/pptx-themes.js.map +1 -0
  158. package/dist/lib/protect-restore.d.ts +137 -0
  159. package/dist/lib/protect-restore.d.ts.map +1 -0
  160. package/dist/lib/protect-restore.js +394 -0
  161. package/dist/lib/protect-restore.js.map +1 -0
  162. package/dist/lib/rate-limiter.d.ts +27 -0
  163. package/dist/lib/rate-limiter.d.ts.map +1 -0
  164. package/dist/lib/rate-limiter.js +79 -0
  165. package/dist/lib/rate-limiter.js.map +1 -0
  166. package/dist/lib/response.d.ts +41 -0
  167. package/dist/lib/response.d.ts.map +1 -0
  168. package/dist/lib/response.js +150 -0
  169. package/dist/lib/response.js.map +1 -0
  170. package/dist/lib/review.d.ts +35 -0
  171. package/dist/lib/review.d.ts.map +1 -0
  172. package/dist/lib/review.js +263 -0
  173. package/dist/lib/review.js.map +1 -0
  174. package/dist/lib/schema.d.ts +66 -0
  175. package/dist/lib/schema.d.ts.map +1 -0
  176. package/dist/lib/schema.js +339 -0
  177. package/dist/lib/schema.js.map +1 -0
  178. package/dist/lib/scientific-words.d.ts +6 -0
  179. package/dist/lib/scientific-words.d.ts.map +1 -0
  180. package/dist/lib/scientific-words.js +66 -0
  181. package/dist/lib/scientific-words.js.map +1 -0
  182. package/dist/lib/sections.d.ts +40 -0
  183. package/dist/lib/sections.d.ts.map +1 -0
  184. package/dist/lib/sections.js +288 -0
  185. package/dist/lib/sections.js.map +1 -0
  186. package/dist/lib/slides.d.ts +86 -0
  187. package/dist/lib/slides.d.ts.map +1 -0
  188. package/dist/lib/slides.js +676 -0
  189. package/dist/lib/slides.js.map +1 -0
  190. package/dist/lib/spelling.d.ts +76 -0
  191. package/dist/lib/spelling.d.ts.map +1 -0
  192. package/dist/lib/spelling.js +272 -0
  193. package/dist/lib/spelling.js.map +1 -0
  194. package/dist/lib/templates.d.ts +30 -0
  195. package/dist/lib/templates.d.ts.map +1 -0
  196. package/dist/lib/templates.js +504 -0
  197. package/dist/lib/templates.js.map +1 -0
  198. package/dist/lib/themes.d.ts +85 -0
  199. package/dist/lib/themes.d.ts.map +1 -0
  200. package/dist/lib/themes.js +652 -0
  201. package/dist/lib/themes.js.map +1 -0
  202. package/dist/lib/trackchanges.d.ts +51 -0
  203. package/dist/lib/trackchanges.d.ts.map +1 -0
  204. package/dist/lib/trackchanges.js +202 -0
  205. package/dist/lib/trackchanges.js.map +1 -0
  206. package/dist/lib/tui.d.ts +76 -0
  207. package/dist/lib/tui.d.ts.map +1 -0
  208. package/dist/lib/tui.js +377 -0
  209. package/dist/lib/tui.js.map +1 -0
  210. package/dist/lib/types.d.ts +447 -0
  211. package/dist/lib/types.d.ts.map +1 -0
  212. package/dist/lib/types.js +6 -0
  213. package/dist/lib/types.js.map +1 -0
  214. package/dist/lib/undo.d.ts +57 -0
  215. package/dist/lib/undo.d.ts.map +1 -0
  216. package/dist/lib/undo.js +185 -0
  217. package/dist/lib/undo.js.map +1 -0
  218. package/dist/lib/utils.d.ts +16 -0
  219. package/dist/lib/utils.d.ts.map +1 -0
  220. package/dist/lib/utils.js +40 -0
  221. package/dist/lib/utils.js.map +1 -0
  222. package/dist/lib/variables.d.ts +42 -0
  223. package/dist/lib/variables.d.ts.map +1 -0
  224. package/dist/lib/variables.js +141 -0
  225. package/dist/lib/variables.js.map +1 -0
  226. package/dist/lib/word.d.ts +80 -0
  227. package/dist/lib/word.d.ts.map +1 -0
  228. package/dist/lib/word.js +360 -0
  229. package/dist/lib/word.js.map +1 -0
  230. package/dist/lib/wordcomments.d.ts +51 -0
  231. package/dist/lib/wordcomments.d.ts.map +1 -0
  232. package/dist/lib/wordcomments.js +587 -0
  233. package/dist/lib/wordcomments.js.map +1 -0
  234. package/eslint.config.js +27 -0
  235. package/lib/annotations.ts +622 -0
  236. package/lib/apply-buildup-colors.py +88 -0
  237. package/lib/build.ts +1013 -0
  238. package/lib/{citations.js → citations.ts} +38 -27
  239. package/lib/commands/{build.js → build.ts} +80 -27
  240. package/lib/commands/{citations.js → citations.ts} +36 -18
  241. package/lib/commands/{comments.js → comments.ts} +187 -54
  242. package/lib/commands/{context.js → context.ts} +18 -8
  243. package/lib/commands/{core.js → core.ts} +34 -20
  244. package/lib/commands/{doi.js → doi.ts} +32 -16
  245. package/lib/commands/{history.js → history.ts} +25 -12
  246. package/lib/commands/{index.js → index.ts} +9 -5
  247. package/lib/commands/{init.js → init.ts} +20 -8
  248. package/lib/commands/{response.js → response.ts} +47 -20
  249. package/lib/commands/{sections.js → sections.ts} +273 -68
  250. package/lib/commands/{utilities.js → utilities.ts} +338 -158
  251. package/lib/{comment-realign.js → comment-realign.ts} +117 -45
  252. package/lib/config.ts +84 -0
  253. package/lib/{crossref.js → crossref.ts} +213 -138
  254. package/lib/dependencies.ts +106 -0
  255. package/lib/doi-cache.ts +115 -0
  256. package/lib/{doi.js → doi.ts} +115 -281
  257. package/lib/{equations.js → equations.ts} +60 -64
  258. package/lib/{errors.js → errors.ts} +56 -48
  259. package/lib/{format.js → format.ts} +137 -63
  260. package/lib/{git.js → git.ts} +66 -63
  261. package/lib/{grammar.js → grammar.ts} +45 -32
  262. package/lib/image-registry.ts +180 -0
  263. package/lib/import.ts +2060 -0
  264. package/lib/journals.ts +505 -0
  265. package/lib/{merge.js → merge.ts} +185 -135
  266. package/lib/{orcid.js → orcid.ts} +17 -22
  267. package/lib/{pdf-comments.js → pdf-comments.ts} +76 -18
  268. package/lib/{pdf-import.js → pdf-import.ts} +148 -70
  269. package/lib/{plugins.js → plugins.ts} +82 -39
  270. package/lib/postprocess.ts +188 -0
  271. package/lib/pptx-color-filter.lua +37 -0
  272. package/lib/pptx-template.ts +625 -0
  273. package/lib/pptx-themes/academic.pptx +0 -0
  274. package/lib/pptx-themes/corporate.pptx +0 -0
  275. package/lib/pptx-themes/dark.pptx +0 -0
  276. package/lib/pptx-themes/default.pptx +0 -0
  277. package/lib/pptx-themes/minimal.pptx +0 -0
  278. package/lib/pptx-themes/plant.pptx +0 -0
  279. package/lib/pptx-themes.ts +896 -0
  280. package/lib/protect-restore.ts +516 -0
  281. package/lib/rate-limiter.ts +94 -0
  282. package/lib/{response.js → response.ts} +36 -21
  283. package/lib/{review.js → review.ts} +53 -43
  284. package/lib/{schema.js → schema.ts} +70 -25
  285. package/lib/{sections.js → sections.ts} +71 -76
  286. package/lib/slides.ts +793 -0
  287. package/lib/{spelling.js → spelling.ts} +43 -59
  288. package/lib/{templates.js → templates.ts} +20 -17
  289. package/lib/themes.ts +742 -0
  290. package/lib/{trackchanges.js → trackchanges.ts} +52 -23
  291. package/lib/types.ts +509 -0
  292. package/lib/{undo.js → undo.ts} +75 -52
  293. package/lib/utils.ts +41 -0
  294. package/lib/{variables.js → variables.ts} +60 -54
  295. package/lib/word.ts +428 -0
  296. package/lib/{wordcomments.js → wordcomments.ts} +94 -40
  297. package/package.json +15 -5
  298. package/skill/REFERENCE.md +67 -0
  299. package/tsconfig.json +26 -0
  300. package/lib/annotations.js +0 -414
  301. package/lib/build.js +0 -639
  302. package/lib/config.js +0 -79
  303. package/lib/import.js +0 -1145
  304. package/lib/journals.js +0 -629
  305. package/lib/word.js +0 -225
  306. /package/lib/{scientific-words.js → scientific-words.ts} +0 -0
@@ -4,201 +4,9 @@
4
4
  */
5
5
 
6
6
  import * as fs from 'fs';
7
- import * as path from 'path';
8
- import * as os from 'os';
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].toLowerCase();
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].trim() : null;
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].trim().slice(0, 60) : '';
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].trim() : '';
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].trim() : '';
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 normalizeText(text) {
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(title, author = '', year = null, journal = '') {
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 = normalizeText(title);
810
- const normalizedJournal = normalizeText(journal);
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 = normalizeText(itemTitle);
818
- const normalizedItemJournal = normalizeText(itemJournal);
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 = normalizeText(dcTitle);
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(bibPath, options = {}) {
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
- const parts = firstAuthor.split(',');
1008
- author = parts[0]?.trim() || '';
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) {