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
@@ -10,26 +10,81 @@
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import YAML from 'yaml';
13
+ import type {
14
+ RefNumber,
15
+ HardcodedRef,
16
+ DynamicRef,
17
+ FigureInfo,
18
+ Registry,
19
+ RefStatus,
20
+ ConversionResult,
21
+ } from './types.js';
22
+
23
+ // =============================================================================
24
+ // Constants
25
+ // =============================================================================
26
+
27
+ /** Characters of context to check before a reference for deduplication */
28
+ const REF_CONTEXT_WINDOW = 100;
29
+
30
+ /** Minimum word length for similarity calculations */
31
+ const MIN_WORD_LENGTH = 2;
32
+
33
+ // =============================================================================
34
+ // Type Definitions (Internal)
35
+ // =============================================================================
36
+
37
+ /**
38
+ * Reference info (internal use in registry building)
39
+ */
40
+ interface RefInfo {
41
+ label: string;
42
+ num: number;
43
+ isSupp: boolean;
44
+ file: string;
45
+ }
46
+
47
+ /**
48
+ * Parsed reference number components
49
+ */
50
+ interface ParsedRefNumber {
51
+ isSupp: boolean;
52
+ num: number;
53
+ suffix: string | null;
54
+ }
55
+
56
+ /**
57
+ * Detected reference with parsed numbers
58
+ */
59
+ interface DetectedRef {
60
+ type: 'fig' | 'tbl' | 'eq';
61
+ match: string;
62
+ numbers: ParsedRefNumber[];
63
+ position: number;
64
+ }
65
+
66
+ // =============================================================================
67
+ // Internal Helpers
68
+ // =============================================================================
13
69
 
14
70
  /**
15
71
  * Discover section files from a directory by reading config files
16
72
  * Only returns files explicitly defined in rev.yaml or sections.yaml
17
73
  * Returns empty array if no config found (caller should handle this)
18
- *
19
- * @param {string} directory
20
- * @returns {string[]} Ordered list of section filenames, or empty if no config
21
74
  */
22
- function discoverSectionFiles(directory) {
75
+ function discoverSectionFiles(directory: string): string[] {
23
76
  // Try rev.yaml first
24
77
  const revYamlPath = path.join(directory, 'rev.yaml');
25
78
  if (fs.existsSync(revYamlPath)) {
26
79
  try {
27
80
  const config = YAML.parse(fs.readFileSync(revYamlPath, 'utf-8'));
28
81
  if (config.sections && Array.isArray(config.sections) && config.sections.length > 0) {
29
- return config.sections.filter(f => fs.existsSync(path.join(directory, f)));
82
+ return config.sections.filter((f: string) => fs.existsSync(path.join(directory, f)));
83
+ }
84
+ } catch (e) {
85
+ if (process.env.DEBUG) {
86
+ console.warn('crossref: YAML parse error in rev.yaml:', (e as Error).message);
30
87
  }
31
- } catch {
32
- // Ignore yaml errors, try next option
33
88
  }
34
89
  }
35
90
 
@@ -40,12 +95,14 @@ function discoverSectionFiles(directory) {
40
95
  const config = YAML.parse(fs.readFileSync(sectionsPath, 'utf-8'));
41
96
  if (config.sections) {
42
97
  const sectionOrder = Object.entries(config.sections)
43
- .sort((a, b) => (a[1].order ?? 999) - (b[1].order ?? 999))
98
+ .sort((a, b) => ((a[1] as any).order ?? 999) - ((b[1] as any).order ?? 999))
44
99
  .map(([file]) => file);
45
- return sectionOrder.filter(f => fs.existsSync(path.join(directory, f)));
100
+ return sectionOrder.filter((f) => fs.existsSync(path.join(directory, f)));
101
+ }
102
+ } catch (e) {
103
+ if (process.env.DEBUG) {
104
+ console.warn('crossref: YAML parse error in sections.yaml:', (e as Error).message);
46
105
  }
47
- } catch {
48
- // Ignore yaml errors
49
106
  }
50
107
  }
51
108
 
@@ -54,6 +111,10 @@ function discoverSectionFiles(directory) {
54
111
  return [];
55
112
  }
56
113
 
114
+ // =============================================================================
115
+ // Detection Patterns
116
+ // =============================================================================
117
+
57
118
  /**
58
119
  * Patterns for detecting hardcoded references
59
120
  * Matches complex patterns including:
@@ -64,7 +125,7 @@ function discoverSectionFiles(directory) {
64
125
  *
65
126
  * Uses a simpler base pattern and parses the full match for lists
66
127
  */
67
- const DETECTION_PATTERNS = {
128
+ const DETECTION_PATTERNS: Record<string, RegExp> = {
68
129
  // Captures the full reference including lists with "and"
69
130
  // Group 1: type prefix (Figure, Fig., etc.)
70
131
  // Group 2: reference list (parsed by parseReferenceList())
@@ -103,12 +164,17 @@ const ANCHOR_PATTERN = /\{#(fig|tbl|eq):([^}]+)\}/gi;
103
164
  */
104
165
  const REF_PATTERN = /@(fig|tbl|eq):([a-zA-Z0-9_-]+)/gi;
105
166
 
167
+ // =============================================================================
168
+ // Public API
169
+ // =============================================================================
170
+
106
171
  /**
107
172
  * Normalize a reference type to standard form
108
- * @param {string} typeStr - e.g., "Figure", "Fig.", "Figs", "table"
109
- * @returns {string} - "fig", "tbl", or "eq"
110
173
  */
111
- export function normalizeType(typeStr) {
174
+ export function normalizeType(typeStr: string): 'fig' | 'tbl' | 'eq' | string {
175
+ if (typeof typeStr !== 'string') {
176
+ throw new TypeError(`typeStr must be a string, got ${typeof typeStr}`);
177
+ }
112
178
  const lower = typeStr.toLowerCase().replace(/\.$/, '');
113
179
  if (lower.startsWith('fig')) return 'fig';
114
180
  if (lower.startsWith('tab')) return 'tbl';
@@ -118,17 +184,16 @@ export function normalizeType(typeStr) {
118
184
 
119
185
  /**
120
186
  * Parse a reference number, handling supplementary (S1, S2) and letter suffixes (1a, 1b)
121
- * @param {string} numStr - e.g., "1", "S1", "S2", "1a", "S1b"
122
- * @param {string} [suffix] - optional letter suffix already extracted
123
- * @returns {{isSupp: boolean, num: number, suffix: string|null}}
124
187
  */
125
- export function parseRefNumber(numStr, suffix = null) {
126
- if (!numStr) return { isSupp: false, num: 0, suffix };
188
+ export function parseRefNumber(numStr: string, suffix: string | null = null): ParsedRefNumber {
189
+ if (!numStr || typeof numStr !== 'string') {
190
+ return { isSupp: false, num: 0, suffix: suffix || null };
191
+ }
127
192
  const isSupp = numStr.toUpperCase().startsWith('S');
128
193
  const numPart = isSupp ? numStr.slice(1) : numStr;
129
194
  // Extract suffix if embedded in numStr (e.g., "1a")
130
195
  const match = numPart.match(/^(\d+)([a-z])?$/i);
131
- const num = match ? parseInt(match[1], 10) : parseInt(numPart, 10);
196
+ const num = match && match[1] ? parseInt(match[1], 10) : parseInt(numPart, 10);
132
197
  const extractedSuffix = suffix || (match && match[2]) || null;
133
198
  return { isSupp, num, suffix: extractedSuffix ? extractedSuffix.toLowerCase() : null };
134
199
  }
@@ -136,24 +201,21 @@ export function parseRefNumber(numStr, suffix = null) {
136
201
  /**
137
202
  * Parse a reference list string like "1, 2, and 3" or "1a-c" or "1a-3b"
138
203
  * Returns an array of {num, isSupp, suffix} objects
139
- *
140
- * @param {string} listStr - e.g., "1, 2, and 3", "1a-c", "1a-3b", "1a, b, c"
141
- * @returns {Array<{num: number, isSupp: boolean, suffix: string|null}>}
142
204
  */
143
- export function parseReferenceList(listStr) {
144
- const results = [];
145
- if (!listStr) return results;
205
+ export function parseReferenceList(listStr: string): ParsedRefNumber[] {
206
+ const results: ParsedRefNumber[] = [];
207
+ if (!listStr || typeof listStr !== 'string') return results;
146
208
 
147
209
  // Normalize: replace "and" with comma, normalize dashes
148
210
  let normalized = listStr
149
211
  .replace(/\s+and\s+/gi, ', ')
150
- .replace(/[–—]/g, '-') // en-dash, em-dash → hyphen
151
- .replace(/&/g, ', '); // & → comma
212
+ .replace(/[–—]/g, '-') // en-dash, em-dash → hyphen
213
+ .replace(/&/g, ', '); // & → comma
152
214
 
153
215
  // Split by comma (but not by dash, which indicates ranges)
154
- const parts = normalized.split(/\s*,\s*/).filter(p => p.trim());
216
+ const parts = normalized.split(/\s*,\s*/).filter((p) => p.trim());
155
217
 
156
- let lastFullRef = null; // Track the last full reference for implicit prefixes
218
+ let lastFullRef: { num: number; isSupp: boolean } | null = null; // Track the last full reference for implicit prefixes
157
219
 
158
220
  for (const part of parts) {
159
221
  const trimmed = part.trim();
@@ -161,7 +223,9 @@ export function parseReferenceList(listStr) {
161
223
 
162
224
  // Check if this is a range (contains -)
163
225
  if (trimmed.includes('-')) {
164
- const [start, end] = trimmed.split('-').map(s => s.trim());
226
+ const parts = trimmed.split('-').map((s) => s.trim());
227
+ const start = parts[0] || '';
228
+ const end = parts[1] || '';
165
229
 
166
230
  // Check if end is just a letter (e.g., "1a-c" where end is "c")
167
231
  const endIsLetterOnly = /^[a-z]$/i.test(end);
@@ -187,22 +251,23 @@ export function parseReferenceList(listStr) {
187
251
  );
188
252
 
189
253
  for (let n = startRef.num; n <= endRef.num; n++) {
190
- const suffixStart = (n === startRef.num) ? startRef.suffix.charCodeAt(0) : 'a'.charCodeAt(0);
191
- const suffixEnd = (n === endRef.num) ? endRef.suffix.charCodeAt(0) : maxSuffix;
254
+ const suffixStart =
255
+ n === startRef.num ? startRef.suffix.charCodeAt(0) : 'a'.charCodeAt(0);
256
+ const suffixEnd = n === endRef.num ? endRef.suffix.charCodeAt(0) : maxSuffix;
192
257
 
193
258
  for (let s = suffixStart; s <= suffixEnd; s++) {
194
259
  results.push({
195
260
  num: n,
196
261
  isSupp: startRef.isSupp,
197
- suffix: String.fromCharCode(s)
262
+ suffix: String.fromCharCode(s),
198
263
  });
199
264
  }
200
265
  }
201
266
  lastFullRef = { num: endRef.num, isSupp: startRef.isSupp };
202
267
  } else if (startRef.suffix || endRef.suffix) {
203
268
  // Suffix range on same number: "1a-c"
204
- const num = startRef.num || (lastFullRef ? lastFullRef.num : 1);
205
- const isSupp = startRef.isSupp || (lastFullRef ? lastFullRef.isSupp : false);
269
+ const num: number = startRef.num !== 0 ? startRef.num : (lastFullRef ? lastFullRef.num : 1);
270
+ const isSupp: boolean = startRef.isSupp ? startRef.isSupp : (lastFullRef ? lastFullRef.isSupp : false);
206
271
  const startCode = (startRef.suffix || 'a').charCodeAt(0);
207
272
  const endCode = (endRef.suffix || 'a').charCodeAt(0);
208
273
 
@@ -210,7 +275,7 @@ export function parseReferenceList(listStr) {
210
275
  results.push({
211
276
  num,
212
277
  isSupp,
213
- suffix: String.fromCharCode(code)
278
+ suffix: String.fromCharCode(code),
214
279
  });
215
280
  }
216
281
  lastFullRef = { num, isSupp };
@@ -220,7 +285,7 @@ export function parseReferenceList(listStr) {
220
285
  results.push({
221
286
  num: n,
222
287
  isSupp: startRef.isSupp,
223
- suffix: null
288
+ suffix: null,
224
289
  });
225
290
  }
226
291
  lastFullRef = { num: endRef.num, isSupp: startRef.isSupp };
@@ -233,7 +298,7 @@ export function parseReferenceList(listStr) {
233
298
  results.push({
234
299
  num: lastFullRef.num,
235
300
  isSupp: lastFullRef.isSupp,
236
- suffix: trimmed.toLowerCase()
301
+ suffix: trimmed.toLowerCase(),
237
302
  });
238
303
  } else {
239
304
  // Full reference: "1", "1a", "S1", "S1a"
@@ -254,22 +319,15 @@ export function parseReferenceList(listStr) {
254
319
  * IMPORTANT: This function requires either explicit sections or a rev.yaml/sections.yaml config.
255
320
  * It will NOT guess by scanning all .md files, as this leads to incorrect numbering
256
321
  * when temporary files (paper_clean.md, etc.) exist in the directory.
257
- *
258
- * @param {string} directory - Directory containing .md files
259
- * @param {string[]} [sections] - Array of section filenames to scan (recommended).
260
- * If not provided, reads from rev.yaml or sections.yaml.
261
- * Returns empty registry if no sections can be determined.
262
- * @returns {{
263
- * figures: Map<string, {label: string, num: number, isSupp: boolean, file: string}>,
264
- * tables: Map<string, {label: string, num: number, isSupp: boolean, file: string}>,
265
- * equations: Map<string, {label: string, num: number, file: string}>,
266
- * byNumber: {fig: Map<string, string>, tbl: Map<string, string>, eq: Map<string, string>}
267
- * }}
268
322
  */
269
- export function buildRegistry(directory, sections) {
270
- const figures = new Map();
271
- const tables = new Map();
272
- const equations = new Map();
323
+ export function buildRegistry(directory: string, sections?: string[]): Registry {
324
+ if (typeof directory !== 'string') {
325
+ throw new TypeError(`directory must be a string, got ${typeof directory}`);
326
+ }
327
+
328
+ const figures = new Map<string, FigureInfo>();
329
+ const tables = new Map<string, FigureInfo>();
330
+ const equations = new Map<string, FigureInfo>();
273
331
 
274
332
  // Counters for numbering (separate for main and supplementary)
275
333
  let figNum = 0;
@@ -278,11 +336,11 @@ export function buildRegistry(directory, sections) {
278
336
  let tblSuppNum = 0;
279
337
  let eqNum = 0;
280
338
 
281
- let orderedFiles;
339
+ let orderedFiles: string[];
282
340
 
283
341
  if (Array.isArray(sections) && sections.length > 0) {
284
342
  // Use explicitly provided section files - most reliable
285
- orderedFiles = sections.filter(f => fs.existsSync(path.join(directory, f)));
343
+ orderedFiles = sections.filter((f) => fs.existsSync(path.join(directory, f)));
286
344
  } else {
287
345
  // Try to determine sections from config files (rev.yaml or sections.yaml)
288
346
  orderedFiles = discoverSectionFiles(directory);
@@ -291,7 +349,7 @@ export function buildRegistry(directory, sections) {
291
349
  }
292
350
 
293
351
  // Determine if a file is supplementary
294
- const isSupplementary = (filename) =>
352
+ const isSupplementary = (filename: string): boolean =>
295
353
  filename.toLowerCase().includes('supp') || filename.toLowerCase().includes('appendix');
296
354
 
297
355
  // Process each file in order
@@ -301,11 +359,15 @@ export function buildRegistry(directory, sections) {
301
359
  const isSupp = isSupplementary(file);
302
360
 
303
361
  // Find all anchors
304
- let match;
362
+ let match: RegExpExecArray | null;
305
363
  ANCHOR_PATTERN.lastIndex = 0;
306
364
  while ((match = ANCHOR_PATTERN.exec(content)) !== null) {
307
- const type = match[1].toLowerCase();
308
- const label = match[2];
365
+ const typeRaw = match[1];
366
+ const labelRaw = match[2];
367
+ if (!typeRaw || !labelRaw) continue;
368
+
369
+ const type = typeRaw.toLowerCase();
370
+ const label = labelRaw;
309
371
 
310
372
  if (type === 'fig') {
311
373
  if (isSupp) {
@@ -325,13 +387,13 @@ export function buildRegistry(directory, sections) {
325
387
  }
326
388
  } else if (type === 'eq') {
327
389
  eqNum++;
328
- equations.set(label, { label, num: eqNum, file });
390
+ equations.set(label, { label, num: eqNum, isSupp: false, file });
329
391
  }
330
392
  }
331
393
  }
332
394
 
333
395
  // Build reverse lookup: number → label
334
- const byNumber = {
396
+ const byNumber: Registry['byNumber'] = {
335
397
  fig: new Map(),
336
398
  figS: new Map(),
337
399
  tbl: new Map(),
@@ -356,12 +418,14 @@ export function buildRegistry(directory, sections) {
356
418
 
357
419
  /**
358
420
  * Get the display string for a label (e.g., "Figure 1", "Table S2")
359
- * @param {string} type - "fig", "tbl", "eq"
360
- * @param {string} label
361
- * @param {object} registry
362
- * @returns {string|null}
363
421
  */
364
- export function labelToDisplay(type, label, registry) {
422
+ export function labelToDisplay(
423
+ type: 'fig' | 'tbl' | 'eq',
424
+ label: string,
425
+ registry: Registry
426
+ ): string | null {
427
+ if (!registry || !registry.figures) return null;
428
+
365
429
  const collection =
366
430
  type === 'fig' ? registry.figures : type === 'tbl' ? registry.tables : registry.equations;
367
431
 
@@ -376,28 +440,32 @@ export function labelToDisplay(type, label, registry) {
376
440
 
377
441
  /**
378
442
  * Get the label for a display number (e.g., "fig:heatmap" from Figure 1)
379
- * @param {string} type - "fig", "tbl", "eq"
380
- * @param {number} num
381
- * @param {boolean} isSupp
382
- * @param {object} registry
383
- * @returns {string|null}
384
443
  */
385
- export function numberToLabel(type, num, isSupp, registry) {
386
- const key = isSupp ? `${type}S` : type;
444
+ export function numberToLabel(
445
+ type: 'fig' | 'tbl' | 'eq',
446
+ num: number,
447
+ isSupp: boolean,
448
+ registry: Registry
449
+ ): string | null {
450
+ if (!registry || !registry.byNumber) return null;
451
+
452
+ const key = isSupp ? (`${type}S` as keyof Registry['byNumber']) : type;
387
453
  return registry.byNumber[key]?.get(num) || null;
388
454
  }
389
455
 
390
456
  /**
391
457
  * Detect all hardcoded references in text
392
- * @param {string} text
393
- * @returns {Array<{type: string, match: string, numbers: Array<{num: number, isSupp: boolean, suffix: string|null}>, position: number}>}
394
458
  */
395
- export function detectHardcodedRefs(text) {
396
- const refs = [];
459
+ export function detectHardcodedRefs(text: string): DetectedRef[] {
460
+ if (typeof text !== 'string') {
461
+ throw new TypeError(`text must be a string, got ${typeof text}`);
462
+ }
463
+
464
+ const refs: DetectedRef[] = [];
397
465
 
398
466
  for (const [type, pattern] of Object.entries(DETECTION_PATTERNS)) {
399
467
  pattern.lastIndex = 0;
400
- let match;
468
+ let match: RegExpExecArray | null;
401
469
 
402
470
  while ((match = pattern.exec(text)) !== null) {
403
471
  // Pattern groups:
@@ -405,13 +473,14 @@ export function detectHardcodedRefs(text) {
405
473
  // [2] = reference list string (e.g., "1, 2, and 3" or "1a-3b")
406
474
 
407
475
  const listStr = match[2];
476
+ if (!listStr) continue;
408
477
  const numbers = parseReferenceList(listStr);
409
478
 
410
479
  // Skip if no valid numbers were parsed
411
480
  if (numbers.length === 0) continue;
412
481
 
413
482
  refs.push({
414
- type: normalizeType(type),
483
+ type: normalizeType(type) as 'fig' | 'tbl' | 'eq',
415
484
  match: match[0],
416
485
  numbers,
417
486
  position: match.index,
@@ -426,22 +495,21 @@ export function detectHardcodedRefs(text) {
426
495
 
427
496
  /**
428
497
  * Convert hardcoded references to @-style references
429
- * @param {string} text
430
- * @param {object} registry
431
- * @returns {{converted: string, conversions: Array<{from: string, to: string}>, warnings: string[]}}
432
498
  */
433
- export function convertHardcodedRefs(text, registry) {
499
+ export function convertHardcodedRefs(text: string, registry: Registry): ConversionResult {
500
+ // Input validation delegated to detectHardcodedRefs
434
501
  const refs = detectHardcodedRefs(text);
435
- const conversions = [];
436
- const warnings = [];
502
+ const conversions: Array<{ from: string; to: string }> = [];
503
+ const warnings: string[] = [];
437
504
 
438
505
  // Process in reverse order to preserve positions
439
506
  let result = text;
440
507
  for (let i = refs.length - 1; i >= 0; i--) {
441
508
  const ref = refs[i];
509
+ if (!ref) continue;
442
510
 
443
511
  // Build replacement
444
- const labels = [];
512
+ const labels: string[] = [];
445
513
  for (const { num, isSupp } of ref.numbers) {
446
514
  const label = numberToLabel(ref.type, num, isSupp, registry);
447
515
  if (label) {
@@ -455,7 +523,18 @@ export function convertHardcodedRefs(text, registry) {
455
523
 
456
524
  if (labels.length > 0 && !labels.includes(ref.match)) {
457
525
  const replacement = labels.join('; ');
458
- result = result.slice(0, ref.position) + replacement + result.slice(ref.position + ref.match.length);
526
+
527
+ // Skip if the @-syntax already appears in the preceding text
528
+ // This prevents duplication when import restores @fig:x and then we see "Fig. 1"
529
+ // e.g., "@fig:map@fig:map{++@fig:map++}" or "@fig:mapFigure 1" patterns
530
+ const textBefore = result.slice(Math.max(0, ref.position - REF_CONTEXT_WINDOW), ref.position);
531
+ const alreadyHasRef = labels.some((label) => textBefore.includes(label));
532
+ if (alreadyHasRef) {
533
+ continue; // Skip - ref already present nearby
534
+ }
535
+
536
+ result =
537
+ result.slice(0, ref.position) + replacement + result.slice(ref.position + ref.match.length);
459
538
 
460
539
  conversions.push({
461
540
  from: ref.match,
@@ -469,18 +548,23 @@ export function convertHardcodedRefs(text, registry) {
469
548
 
470
549
  /**
471
550
  * Detect @-style references in text
472
- * @param {string} text
473
- * @returns {Array<{type: string, label: string, match: string, position: number}>}
474
551
  */
475
- export function detectDynamicRefs(text) {
476
- const refs = [];
552
+ export function detectDynamicRefs(text: string): DynamicRef[] {
553
+ if (typeof text !== 'string') {
554
+ throw new TypeError(`text must be a string, got ${typeof text}`);
555
+ }
556
+
557
+ const refs: DynamicRef[] = [];
477
558
  REF_PATTERN.lastIndex = 0;
478
- let match;
559
+ let match: RegExpExecArray | null;
479
560
 
480
561
  while ((match = REF_PATTERN.exec(text)) !== null) {
562
+ const type = match[1];
563
+ const label = match[2];
564
+ if (!type || !label) continue;
481
565
  refs.push({
482
- type: match[1],
483
- label: match[2],
566
+ type: type as 'fig' | 'tbl' | 'eq',
567
+ label: label,
484
568
  match: match[0],
485
569
  position: match.index,
486
570
  });
@@ -491,28 +575,23 @@ export function detectDynamicRefs(text) {
491
575
 
492
576
  /**
493
577
  * Get reference status for a file/text
494
- * @param {string} text
495
- * @param {object} registry
496
- * @returns {{
497
- * dynamic: Array,
498
- * hardcoded: Array,
499
- * anchors: {figures: number, tables: number, equations: number}
500
- * }}
501
578
  */
502
- export function getRefStatus(text, registry) {
579
+ export function getRefStatus(text: string, registry: Registry): RefStatus {
503
580
  const dynamic = detectDynamicRefs(text);
504
- const hardcoded = detectHardcodedRefs(text);
581
+ const hardcoded = detectHardcodedRefs(text) as HardcodedRef[];
505
582
 
506
583
  // Count anchors in this text
507
584
  ANCHOR_PATTERN.lastIndex = 0;
508
585
  let figCount = 0,
509
586
  tblCount = 0,
510
587
  eqCount = 0;
511
- let match;
588
+ let match: RegExpExecArray | null;
512
589
  while ((match = ANCHOR_PATTERN.exec(text)) !== null) {
513
- if (match[1] === 'fig') figCount++;
514
- else if (match[1] === 'tbl') tblCount++;
515
- else if (match[1] === 'eq') eqCount++;
590
+ const type = match[1];
591
+ if (!type) continue;
592
+ if (type === 'fig') figCount++;
593
+ else if (type === 'tbl') tblCount++;
594
+ else if (type === 'eq') eqCount++;
516
595
  }
517
596
 
518
597
  return {
@@ -525,20 +604,20 @@ export function getRefStatus(text, registry) {
525
604
  /**
526
605
  * Detect forward references in combined text
527
606
  * A forward reference is a @ref that appears before its {#anchor} definition
528
- *
529
- * @param {string} text - Combined document text
530
- * @returns {{
531
- * forwardRefs: Array<{type: string, label: string, match: string, position: number}>,
532
- * anchorPositions: Map<string, number>
533
- * }}
534
607
  */
535
- export function detectForwardRefs(text) {
608
+ export function detectForwardRefs(text: string): {
609
+ forwardRefs: Array<{ type: string; label: string; match: string; position: number }>;
610
+ anchorPositions: Map<string, number>;
611
+ } {
536
612
  // Build map of anchor positions: "fig:label" -> position
537
- const anchorPositions = new Map();
613
+ const anchorPositions = new Map<string, number>();
538
614
  ANCHOR_PATTERN.lastIndex = 0;
539
- let match;
615
+ let match: RegExpExecArray | null;
540
616
  while ((match = ANCHOR_PATTERN.exec(text)) !== null) {
541
- const key = `${match[1]}:${match[2]}`;
617
+ const type = match[1];
618
+ const label = match[2];
619
+ if (!type || !label) continue;
620
+ const key = `${type}:${label}`;
542
621
  // Only store first occurrence (in case of duplicates)
543
622
  if (!anchorPositions.has(key)) {
544
623
  anchorPositions.set(key, match.index);
@@ -563,31 +642,29 @@ export function detectForwardRefs(text) {
563
642
  * Resolve forward references to display format
564
643
  * Only resolves refs that appear before their anchor definition
565
644
  * Leaves other refs for pandoc-crossref to handle (preserves clickable links)
566
- *
567
- * @param {string} text - Combined document text
568
- * @param {object} registry - Registry from buildRegistry()
569
- * @returns {{
570
- * text: string,
571
- * resolved: Array<{from: string, to: string, position: number}>,
572
- * unresolved: Array<{ref: string, position: number}>
573
- * }}
574
645
  */
575
- export function resolveForwardRefs(text, registry) {
646
+ export function resolveForwardRefs(
647
+ text: string,
648
+ registry: Registry
649
+ ): {
650
+ text: string;
651
+ resolved: Array<{ from: string; to: string; position: number }>;
652
+ unresolved: Array<{ ref: string; position: number }>;
653
+ } {
576
654
  const { forwardRefs } = detectForwardRefs(text);
577
- const resolved = [];
578
- const unresolved = [];
655
+ const resolved: Array<{ from: string; to: string; position: number }> = [];
656
+ const unresolved: Array<{ ref: string; position: number }> = [];
579
657
 
580
658
  // Process in reverse order to preserve positions
581
659
  let result = text;
582
660
  for (let i = forwardRefs.length - 1; i >= 0; i--) {
583
661
  const ref = forwardRefs[i];
584
- const display = labelToDisplay(ref.type, ref.label, registry);
662
+ if (!ref) continue;
663
+ const display = labelToDisplay(ref.type as 'fig' | 'tbl' | 'eq', ref.label, registry);
585
664
 
586
665
  if (display) {
587
666
  result =
588
- result.slice(0, ref.position) +
589
- display +
590
- result.slice(ref.position + ref.match.length);
667
+ result.slice(0, ref.position) + display + result.slice(ref.position + ref.match.length);
591
668
  resolved.push({
592
669
  from: ref.match,
593
670
  to: display,
@@ -606,11 +683,9 @@ export function resolveForwardRefs(text, registry) {
606
683
 
607
684
  /**
608
685
  * Format registry for display
609
- * @param {object} registry
610
- * @returns {string}
611
686
  */
612
- export function formatRegistry(registry) {
613
- const lines = [];
687
+ export function formatRegistry(registry: Registry): string {
688
+ const lines: string[] = [];
614
689
 
615
690
  if (registry.figures.size > 0) {
616
691
  lines.push('Figures:');