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
@@ -18,14 +18,70 @@ import {
18
18
  convertHardcodedRefs,
19
19
  inlineDiffPreview,
20
20
  } from './context.js';
21
+ import type { Command } from 'commander';
22
+ import * as readline from 'readline';
23
+
24
+ interface DetectedSection {
25
+ header: string;
26
+ content: string;
27
+ file: string;
28
+ }
29
+
30
+ interface ImportStats {
31
+ insertions: number;
32
+ deletions: number;
33
+ substitutions: number;
34
+ comments: number;
35
+ total: number;
36
+ }
37
+
38
+ interface BootstrapOptions {
39
+ output: string;
40
+ dryRun?: boolean;
41
+ }
42
+
43
+ interface ImportOptions {
44
+ output?: string;
45
+ author?: string;
46
+ dryRun?: boolean;
47
+ }
48
+
49
+ interface ExtractOptions {
50
+ output?: string;
51
+ }
52
+
53
+ interface SplitOptions {
54
+ config: string;
55
+ dir: string;
56
+ dryRun?: boolean;
57
+ }
58
+
59
+ interface SyncOptions {
60
+ config: string;
61
+ dir: string;
62
+ crossref?: boolean;
63
+ diff?: boolean;
64
+ force?: boolean;
65
+ dryRun?: boolean;
66
+ }
67
+
68
+ interface MergeOptions {
69
+ base?: string;
70
+ output?: string;
71
+ names?: string;
72
+ strategy: string;
73
+ diffLevel: 'sentence' | 'word';
74
+ dryRun?: boolean;
75
+ sections?: boolean;
76
+ }
21
77
 
22
78
  /**
23
79
  * Detect sections from Word document text
24
80
  * Looks for common academic paper section headers
25
81
  */
26
- function detectSectionsFromWord(text) {
82
+ function detectSectionsFromWord(text: string): DetectedSection[] {
27
83
  const lines = text.split('\n');
28
- const sections = [];
84
+ const sections: DetectedSection[] = [];
29
85
 
30
86
  const headerPatterns = [
31
87
  /^(Abstract|Summary)$/i,
@@ -50,9 +106,9 @@ function detectSectionsFromWord(text) {
50
106
 
51
107
  const numberedHeaderPattern = /^(\d+\.?\s+)(Abstract|Introduction|Background|Methods?|Materials|Results?|Discussion|Conclusions?|References|Acknowledgements?|Appendix)/i;
52
108
 
53
- let currentSection = null;
54
- let currentContent = [];
55
- let preambleContent = [];
109
+ let currentSection: string | null = null;
110
+ let currentContent: string[] = [];
111
+ let preambleContent: string[] = [];
56
112
 
57
113
  for (const line of lines) {
58
114
  const trimmed = line.trim();
@@ -131,7 +187,7 @@ function detectSectionsFromWord(text) {
131
187
  /**
132
188
  * Convert a section header to a filename
133
189
  */
134
- function headerToFilename(header) {
190
+ function headerToFilename(header: string): string {
135
191
  return header
136
192
  .toLowerCase()
137
193
  .replace(/[^a-z0-9]+/g, '-')
@@ -142,7 +198,7 @@ function headerToFilename(header) {
142
198
  /**
143
199
  * Bootstrap a new project from a Word document
144
200
  */
145
- async function bootstrapFromWord(docx, options) {
201
+ async function bootstrapFromWord(docx: string, options: BootstrapOptions): Promise<void> {
146
202
  const outputDir = path.resolve(options.output);
147
203
 
148
204
  console.log(chalk.cyan(`Bootstrapping project from ${path.basename(docx)}...\n`));
@@ -167,7 +223,7 @@ async function bootstrapFromWord(docx, options) {
167
223
  fs.mkdirSync(outputDir, { recursive: true });
168
224
  }
169
225
 
170
- const sectionFiles = [];
226
+ const sectionFiles: string[] = [];
171
227
  for (const section of sections) {
172
228
  const filePath = path.join(outputDir, section.file);
173
229
  const content = `# ${section.header}\n\n${section.content.trim()}\n`;
@@ -231,17 +287,17 @@ async function bootstrapFromWord(docx, options) {
231
287
  console.log(chalk.dim(' rev build # Build PDF and DOCX'));
232
288
  }
233
289
  } catch (err) {
234
- console.error(chalk.red(`Error: ${err.message}`));
235
- if (process.env.DEBUG) console.error(err.stack);
290
+ const error = err as Error;
291
+ console.error(chalk.red(`Error: ${error.message}`));
292
+ if (process.env.DEBUG) console.error(error.stack);
236
293
  process.exit(1);
237
294
  }
238
295
  }
239
296
 
240
297
  /**
241
298
  * Register section commands with the program
242
- * @param {import('commander').Command} program
243
299
  */
244
- export function register(program) {
300
+ export function register(program: Command): void {
245
301
  // ==========================================================================
246
302
  // IMPORT command - Import from Word (bootstrap or diff mode)
247
303
  // ==========================================================================
@@ -254,14 +310,14 @@ export function register(program) {
254
310
  .option('-o, --output <dir>', 'Output directory for bootstrap mode', '.')
255
311
  .option('-a, --author <name>', 'Author name for changes (diff mode)', 'Reviewer')
256
312
  .option('--dry-run', 'Preview without saving')
257
- .action(async (docx, original, options) => {
313
+ .action(async (docx: string, original: string | undefined, options: ImportOptions) => {
258
314
  if (!fs.existsSync(docx)) {
259
315
  console.error(chalk.red(`Error: Word file not found: ${docx}`));
260
316
  process.exit(1);
261
317
  }
262
318
 
263
319
  if (!original) {
264
- await bootstrapFromWord(docx, options);
320
+ await bootstrapFromWord(docx, options as BootstrapOptions);
265
321
  return;
266
322
  }
267
323
 
@@ -307,8 +363,9 @@ export function register(program) {
307
363
  console.log(` 3. ${chalk.bold('rev build docx')} - Rebuild Word doc`);
308
364
 
309
365
  } catch (err) {
310
- console.error(chalk.red(`Error: ${err.message}`));
311
- if (process.env.DEBUG) console.error(err.stack);
366
+ const error = err as Error;
367
+ console.error(chalk.red(`Error: ${error.message}`));
368
+ if (process.env.DEBUG) console.error(error.stack);
312
369
  process.exit(1);
313
370
  }
314
371
  });
@@ -322,7 +379,7 @@ export function register(program) {
322
379
  .description('Extract plain text from Word document (no diff)')
323
380
  .argument('<docx>', 'Word document')
324
381
  .option('-o, --output <file>', 'Output file (default: stdout)')
325
- .action(async (docx, options) => {
382
+ .action(async (docx: string, options: ExtractOptions) => {
326
383
  if (!fs.existsSync(docx)) {
327
384
  console.error(chalk.red(`Error: File not found: ${docx}`));
328
385
  process.exit(1);
@@ -339,7 +396,8 @@ export function register(program) {
339
396
  process.stdout.write(result.value);
340
397
  }
341
398
  } catch (err) {
342
- console.error(chalk.red(`Error: ${err.message}`));
399
+ const error = err as Error;
400
+ console.error(chalk.red(`Error: ${error.message}`));
343
401
  process.exit(1);
344
402
  }
345
403
  });
@@ -355,7 +413,7 @@ export function register(program) {
355
413
  .option('-c, --config <file>', 'Sections config file', 'sections.yaml')
356
414
  .option('-d, --dir <directory>', 'Output directory for section files', '.')
357
415
  .option('--dry-run', 'Preview without writing files')
358
- .action((file, options) => {
416
+ .action((file: string, options: SplitOptions) => {
359
417
  if (!fs.existsSync(file)) {
360
418
  console.error(chalk.red(`File not found: ${file}`));
361
419
  process.exit(1);
@@ -389,7 +447,7 @@ export function register(program) {
389
447
 
390
448
  console.log(` ${chalk.bold(sectionFile)} (${lines} lines)`);
391
449
  if (annotations.total > 0) {
392
- const parts = [];
450
+ const parts: string[] = [];
393
451
  if (annotations.inserts > 0) parts.push(chalk.green(`+${annotations.inserts}`));
394
452
  if (annotations.deletes > 0) parts.push(chalk.red(`-${annotations.deletes}`));
395
453
  if (annotations.substitutes > 0) parts.push(chalk.yellow(`~${annotations.substitutes}`));
@@ -426,7 +484,7 @@ export function register(program) {
426
484
  .option('--no-diff', 'Skip showing diff preview')
427
485
  .option('--force', 'Overwrite files without conflict warning')
428
486
  .option('--dry-run', 'Preview without writing files')
429
- .action(async (docx, sections, options) => {
487
+ .action(async (docx: string | undefined, sections: string[], options: SyncOptions) => {
430
488
  // Auto-detect most recent docx or pdf if not provided
431
489
  if (!docx) {
432
490
  const docxFiles = findFiles('.docx');
@@ -439,7 +497,7 @@ export function register(program) {
439
497
  }
440
498
  const sorted = allFiles
441
499
  .map(f => ({ name: f, mtime: fs.statSync(f).mtime }))
442
- .sort((a, b) => b.mtime - a.mtime);
500
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
443
501
  docx = sorted[0].name;
444
502
  console.log(fmt.status('info', `Using most recent: ${docx}`));
445
503
  console.log();
@@ -482,7 +540,7 @@ export function register(program) {
482
540
  const config = loadConfig(configPath);
483
541
  const mainSection = config.sections?.[0];
484
542
 
485
- if (mainSection) {
543
+ if (mainSection && typeof mainSection === 'string') {
486
544
  const mainPath = path.join(options.dir, mainSection);
487
545
  if (fs.existsSync(mainPath)) {
488
546
  console.log(chalk.dim(`Use 'rev pdf-comments ${docx} --append ${mainSection}' to add comments to markdown.`));
@@ -491,8 +549,9 @@ export function register(program) {
491
549
  }
492
550
  } catch (err) {
493
551
  spin.stop();
494
- console.error(fmt.status('error', `Failed to extract PDF comments: ${err.message}`));
495
- if (process.env.DEBUG) console.error(err.stack);
552
+ const error = err as Error;
553
+ console.error(fmt.status('error', `Failed to extract PDF comments: ${error.message}`));
554
+ if (process.env.DEBUG) console.error(error.stack);
496
555
  process.exit(1);
497
556
  }
498
557
  return;
@@ -509,8 +568,7 @@ export function register(program) {
509
568
 
510
569
  try {
511
570
  const config = loadConfig(configPath);
512
- const mammoth = await import('mammoth');
513
- const { importFromWord, extractWordComments, extractCommentAnchors, insertCommentsIntoMarkdown } = await import('../import.js');
571
+ const { importFromWord, extractWordComments, extractCommentAnchors, insertCommentsIntoMarkdown, extractFromWord } = await import('../import.js');
514
572
 
515
573
  let registry = null;
516
574
  let totalRefConversions = 0;
@@ -519,10 +577,30 @@ export function register(program) {
519
577
  }
520
578
 
521
579
  const comments = await extractWordComments(docx);
522
- const anchors = await extractCommentAnchors(docx);
580
+ const { anchors, fullDocText: xmlDocText } = await extractCommentAnchors(docx);
581
+
582
+ // Use pandoc for extraction to preserve markdown formatting (bold, tables, etc.)
583
+ // Mammoth only extracts plain text which loses all formatting
584
+ const wordExtraction = await extractFromWord(docx, { mediaDir: options.dir });
585
+ let wordText = wordExtraction.text;
586
+ const wordTables = wordExtraction.tables || [];
587
+
588
+ // Restore crossref on FULL text BEFORE splitting into sections
589
+ // This ensures duplicate labels from track changes are handled correctly
590
+ // (the same figure may appear multiple times in old/new versions)
591
+ const { restoreCrossrefFromWord, restoreImagesFromRegistry } = await import('../import.js');
592
+ const crossrefResult = restoreCrossrefFromWord(wordText, options.dir);
593
+ wordText = crossrefResult.text;
594
+ if (crossrefResult.restored > 0) {
595
+ console.log(`Restored ${crossrefResult.restored} crossref reference(s)`);
596
+ }
523
597
 
524
- const wordResult = await mammoth.extractRawText({ path: docx });
525
- const wordText = wordResult.value;
598
+ // Also restore images from registry using shared restoredLabels
599
+ const imageRestoreResult = restoreImagesFromRegistry(wordText, options.dir, crossrefResult.restoredLabels);
600
+ wordText = imageRestoreResult.text;
601
+ if (imageRestoreResult.restored > 0) {
602
+ console.log(`Restored ${imageRestoreResult.restored} image(s) from registry`);
603
+ }
526
604
 
527
605
  let wordSections = extractSectionsFromText(wordText, config.sections);
528
606
 
@@ -554,7 +632,7 @@ export function register(program) {
554
632
 
555
633
  // Conflict detection
556
634
  if (!options.force && !options.dryRun) {
557
- const conflicts = [];
635
+ const conflicts: Array<{ file: string; annotations: number }> = [];
558
636
  for (const section of wordSections) {
559
637
  const sectionPath = path.join(options.dir, section.file);
560
638
  if (fs.existsSync(sectionPath)) {
@@ -576,16 +654,15 @@ export function register(program) {
576
654
  }
577
655
  console.log();
578
656
 
579
- const rl = await import('readline');
580
- const readline = rl.createInterface({
657
+ const rl = readline.createInterface({
581
658
  input: process.stdin,
582
659
  output: process.stdout,
583
660
  });
584
661
 
585
- const answer = await new Promise((resolve) =>
586
- readline.question(chalk.cyan('Continue and overwrite? [y/N] '), resolve)
662
+ const answer = await new Promise<string>((resolve) =>
663
+ rl.question(chalk.cyan('Continue and overwrite? [y/N] '), resolve)
587
664
  );
588
- readline.close();
665
+ rl.close();
589
666
 
590
667
  if (answer.toLowerCase() !== 'y') {
591
668
  console.log(chalk.dim('Aborted. Use --force to skip this check.'));
@@ -595,9 +672,92 @@ export function register(program) {
595
672
  }
596
673
  }
597
674
 
598
- const sectionResults = [];
675
+ const sectionResults: Array<{
676
+ file: string;
677
+ header: string;
678
+ status: string;
679
+ stats?: ImportStats;
680
+ refs?: number;
681
+ }> = [];
599
682
  let totalChanges = 0;
600
683
 
684
+ // Calculate section boundaries in the XML document text for comment filtering
685
+ // Comment positions (docPosition) are relative to xmlDocText, NOT wordText
686
+ // So we must find section headers in xmlDocText to get matching boundaries
687
+ const sectionBoundaries: Array<{ file: string; start: number; end: number }> = [];
688
+ const xmlLower = xmlDocText.toLowerCase();
689
+
690
+ // Standard section header keywords to search for in XML
691
+ // Map from file name pattern to search terms
692
+ const sectionKeywords: Record<string, string[]> = {
693
+ 'abstract': ['abstract', 'summary'],
694
+ 'introduction': ['introduction', 'background'],
695
+ 'methods': ['methods', 'materials and methods', 'methodology'],
696
+ 'results': ['results'],
697
+ 'discussion': ['discussion'],
698
+ 'conclusion': ['conclusion', 'conclusions'],
699
+ };
700
+
701
+ // Helper: find section header (skip labels like "Methods:" in structured abstracts)
702
+ // Real section headers are NOT followed by ":" immediately
703
+ function findSectionHeader(text: string, keyword: string, startFrom: number = 0): number {
704
+ const lower = text.toLowerCase();
705
+ let idx = startFrom;
706
+ while ((idx = lower.indexOf(keyword, idx)) !== -1) {
707
+ // Check what follows the keyword
708
+ const afterKeyword = text.slice(idx + keyword.length, idx + keyword.length + 5);
709
+ // Skip if followed by ":" (this is a label, not a section header)
710
+ // Real headers are followed by text content, a newline, or a subheading
711
+ if (!afterKeyword.startsWith(':') && !afterKeyword.startsWith(' :')) {
712
+ return idx;
713
+ }
714
+ idx++;
715
+ }
716
+ return -1;
717
+ }
718
+
719
+ for (const section of wordSections) {
720
+ const fileBase = section.file.replace(/\.md$/i, '').toLowerCase();
721
+
722
+ // Get keywords for this section
723
+ const keywords = sectionKeywords[fileBase] || [fileBase];
724
+
725
+ // Find the first valid keyword that exists in XML (not a label)
726
+ let headerIdx = -1;
727
+ for (const kw of keywords) {
728
+ const idx = findSectionHeader(xmlDocText, kw, 0);
729
+ if (idx >= 0 && (headerIdx < 0 || idx < headerIdx)) {
730
+ headerIdx = idx;
731
+ }
732
+ }
733
+
734
+ if (headerIdx >= 0) {
735
+ // Find the next section's start to determine end boundary
736
+ let nextHeaderIdx = xmlDocText.length;
737
+ const sectionIdx = wordSections.indexOf(section);
738
+ if (sectionIdx < wordSections.length - 1) {
739
+ const nextFileBase = wordSections[sectionIdx + 1].file.replace(/\.md$/i, '').toLowerCase();
740
+ const nextKeywords = sectionKeywords[nextFileBase] || [nextFileBase];
741
+ for (const nkw of nextKeywords) {
742
+ const foundNext = findSectionHeader(xmlDocText, nkw, headerIdx + 10);
743
+ if (foundNext >= 0 && foundNext < nextHeaderIdx) {
744
+ nextHeaderIdx = foundNext;
745
+ }
746
+ }
747
+ }
748
+
749
+ sectionBoundaries.push({
750
+ file: section.file,
751
+ start: headerIdx,
752
+ end: nextHeaderIdx
753
+ });
754
+
755
+ }
756
+ }
757
+
758
+ // Document length is the XML text length (same coordinate system as docPosition)
759
+ const docLength = xmlDocText.length;
760
+
601
761
  for (const section of wordSections) {
602
762
  const sectionPath = path.join(options.dir, section.file);
603
763
 
@@ -606,7 +766,7 @@ export function register(program) {
606
766
  file: section.file,
607
767
  header: section.header,
608
768
  status: 'skipped',
609
- stats: null,
769
+ stats: undefined,
610
770
  });
611
771
  continue;
612
772
  }
@@ -614,11 +774,12 @@ export function register(program) {
614
774
  const result = await importFromWord(docx, sectionPath, {
615
775
  sectionContent: section.content,
616
776
  author: 'Reviewer',
777
+ wordTables: wordTables,
617
778
  });
618
779
 
619
780
  let { annotated, stats } = result;
620
781
 
621
- let refConversions = [];
782
+ let refConversions: Array<{ from: string; to: string }> = [];
622
783
  if (registry && options.crossref !== false) {
623
784
  const crossrefResult = convertHardcodedRefs(annotated, registry);
624
785
  annotated = crossrefResult.converted;
@@ -628,10 +789,54 @@ export function register(program) {
628
789
 
629
790
  let commentsInserted = 0;
630
791
  if (comments.length > 0 && anchors.size > 0) {
631
- annotated = insertCommentsIntoMarkdown(annotated, comments, anchors, { quiet: true });
632
- commentsInserted = (annotated.match(/\{>>/g) || []).length - (result.annotated?.match(/\{>>/g) || []).length;
633
- if (commentsInserted > 0) {
634
- stats.comments = (stats.comments || 0) + commentsInserted;
792
+ // Filter comments to only those that belong to this section
793
+ // Use exact position matching: docPosition is in xmlDocText coordinates,
794
+ // and sectionBoundaries are also in xmlDocText coordinates (same source!)
795
+ const boundary = sectionBoundaries.find(b => b.file === section.file);
796
+ const isFirstSection = wordSections.indexOf(section) === 0;
797
+ const firstBoundaryStart = sectionBoundaries.length > 0 ? Math.min(...sectionBoundaries.map(b => b.start)) : 0;
798
+
799
+ const sectionComments = comments.filter((c: any) => {
800
+ const anchorData = anchors.get(c.id);
801
+ if (!anchorData) return false;
802
+
803
+ // Use exact position - no scaling needed since both are in xmlDocText coordinates
804
+ if (anchorData.docPosition !== undefined && boundary) {
805
+ // Include comments within section boundaries
806
+ if (anchorData.docPosition >= boundary.start && anchorData.docPosition < boundary.end) {
807
+ return true;
808
+ }
809
+ // Also include "outside" comments (before first section) in the first section file
810
+ if (isFirstSection && anchorData.docPosition < firstBoundaryStart) {
811
+ return true;
812
+ }
813
+ }
814
+
815
+ return false;
816
+ });
817
+
818
+ if (process.env.DEBUG) {
819
+ console.log(`[DEBUG] ${section.file}: ${sectionComments.length} comments to place (boundary: ${boundary?.start}-${boundary?.end})`);
820
+ }
821
+
822
+ if (sectionComments.length > 0) {
823
+ // Use a more robust pattern that handles < in comment text
824
+ const commentPattern = /\{>>.*?<<\}/gs;
825
+ const beforeCount = (annotated.match(commentPattern) || []).length;
826
+ annotated = insertCommentsIntoMarkdown(annotated, sectionComments, anchors, {
827
+ quiet: !process.env.DEBUG,
828
+ sectionBoundary: boundary // Pass section boundary for position-based insertion
829
+ });
830
+ const afterCount = (annotated.match(commentPattern) || []).length;
831
+ commentsInserted = afterCount - beforeCount;
832
+
833
+ if (process.env.DEBUG) {
834
+ console.log(`[DEBUG] ${section.file}: inserted ${commentsInserted} of ${sectionComments.length} comments`);
835
+ }
836
+
837
+ if (commentsInserted > 0) {
838
+ stats.comments = (stats.comments || 0) + commentsInserted;
839
+ }
635
840
  }
636
841
  }
637
842
 
@@ -645,7 +850,7 @@ export function register(program) {
645
850
  refs: refConversions.length,
646
851
  });
647
852
 
648
- if (!options.dryRun && (stats.total > 0 || refConversions.length > 0)) {
853
+ if (!options.dryRun) {
649
854
  fs.writeFileSync(sectionPath, annotated, 'utf-8');
650
855
  }
651
856
  }
@@ -662,7 +867,7 @@ export function register(program) {
662
867
  '',
663
868
  ];
664
869
  }
665
- const s = r.stats;
870
+ const s = r.stats!;
666
871
  return [
667
872
  chalk.bold(r.file),
668
873
  r.header.length > 25 ? r.header.slice(0, 22) + '...' : r.header,
@@ -670,7 +875,7 @@ export function register(program) {
670
875
  s.deletions > 0 ? chalk.red(`-${s.deletions}`) : chalk.dim('-'),
671
876
  s.substitutions > 0 ? chalk.yellow(`~${s.substitutions}`) : chalk.dim('-'),
672
877
  s.comments > 0 ? chalk.blue(`#${s.comments}`) : chalk.dim('-'),
673
- r.refs > 0 ? chalk.magenta(`@${r.refs}`) : chalk.dim('-'),
878
+ r.refs! > 0 ? chalk.magenta(`@${r.refs}`) : chalk.dim('-'),
674
879
  ];
675
880
  });
676
881
 
@@ -703,7 +908,7 @@ export function register(program) {
703
908
  if (options.dryRun) {
704
909
  console.log(fmt.box(chalk.yellow('Dry run - no files written'), { padding: 0 }));
705
910
  } else if (totalChanges > 0 || totalRefConversions > 0 || comments.length > 0) {
706
- const summaryLines = [];
911
+ const summaryLines: string[] = [];
707
912
  summaryLines.push(`${chalk.bold(wordSections.length)} sections processed`);
708
913
  if (totalChanges > 0) summaryLines.push(`${chalk.bold(totalChanges)} annotations imported`);
709
914
  if (comments.length > 0) summaryLines.push(`${chalk.bold(comments.length)} comments placed`);
@@ -720,8 +925,9 @@ export function register(program) {
720
925
  }
721
926
  } catch (err) {
722
927
  spin.stop();
723
- console.error(fmt.status('error', err.message));
724
- if (process.env.DEBUG) console.error(err.stack);
928
+ const error = err as Error;
929
+ console.error(fmt.status('error', error.message));
930
+ if (process.env.DEBUG) console.error(error.stack);
725
931
  process.exit(1);
726
932
  }
727
933
  });
@@ -741,7 +947,7 @@ export function register(program) {
741
947
  .option('--diff-level <level>', 'Diff granularity: sentence or word (default: sentence)', 'sentence')
742
948
  .option('--dry-run', 'Show conflicts without writing')
743
949
  .option('--sections', 'Split merged output back to section files')
744
- .action(async (docxFiles, options) => {
950
+ .action(async (docxFiles: string[], options: MergeOptions) => {
745
951
  const {
746
952
  mergeThreeWay,
747
953
  formatConflict,
@@ -766,7 +972,7 @@ export function register(program) {
766
972
  if (!basePath) {
767
973
  // Try to use .rev/base.docx
768
974
  const projectDir = process.cwd();
769
- basePath = getBaseDocument(projectDir);
975
+ basePath = getBaseDocument(projectDir) ?? undefined;
770
976
 
771
977
  if (basePath) {
772
978
  baseSource = 'auto (.rev/base.docx)';
@@ -862,16 +1068,15 @@ export function register(program) {
862
1068
  console.log(formatConflict(conflict, baseText));
863
1069
  console.log();
864
1070
 
865
- const rl = await import('readline');
866
- const readline = rl.createInterface({
1071
+ const rl = readline.createInterface({
867
1072
  input: process.stdin,
868
1073
  output: process.stdout,
869
1074
  });
870
1075
 
871
- const answer = await new Promise((resolve) =>
872
- readline.question(chalk.cyan(` Choose (1-${conflict.changes.length}, s=skip): `), resolve)
1076
+ const answer = await new Promise<string>((resolve) =>
1077
+ rl.question(chalk.cyan(` Choose (1-${conflict.changes.length}, s=skip): `), resolve)
873
1078
  );
874
- readline.close();
1079
+ rl.close();
875
1080
 
876
1081
  if (answer.toLowerCase() !== 's' && !isNaN(parseInt(answer))) {
877
1082
  const choice = parseInt(answer) - 1;
@@ -923,8 +1128,9 @@ export function register(program) {
923
1128
  }
924
1129
  } catch (err) {
925
1130
  spin.stop();
926
- console.error(fmt.status('error', err.message));
927
- if (process.env.DEBUG) console.error(err.stack);
1131
+ const error = err as Error;
1132
+ console.error(fmt.status('error', error.message));
1133
+ if (process.env.DEBUG) console.error(error.stack);
928
1134
  process.exit(1);
929
1135
  }
930
1136
  });
@@ -946,7 +1152,7 @@ export function register(program) {
946
1152
  return;
947
1153
  }
948
1154
 
949
- const unresolved = data.conflicts.filter(c => c.resolved === null);
1155
+ const unresolved = data.conflicts.filter((c: any) => c.resolved === null);
950
1156
 
951
1157
  if (unresolved.length === 0) {
952
1158
  console.log(fmt.status('success', 'All conflicts resolved!'));
@@ -963,7 +1169,7 @@ export function register(program) {
963
1169
  console.log(chalk.bold(`Conflict ${conflict.id}:`));
964
1170
  // Show abbreviated info
965
1171
  console.log(chalk.dim(` Original: "${conflict.original.slice(0, 50)}${conflict.original.length > 50 ? '...' : ''}"`));
966
- console.log(chalk.dim(` Options: ${conflict.changes.map(c => c.reviewer).join(', ')}`));
1172
+ console.log(chalk.dim(` Options: ${conflict.changes.map((c: any) => c.reviewer).join(', ')}`));
967
1173
  console.log();
968
1174
  }
969
1175
 
@@ -980,7 +1186,7 @@ export function register(program) {
980
1186
  .description('Resolve merge conflicts interactively')
981
1187
  .option('--theirs', 'Accept all changes from last reviewer')
982
1188
  .option('--ours', 'Accept all changes from first reviewer')
983
- .action(async (options) => {
1189
+ .action(async (options: { theirs?: boolean; ours?: boolean }) => {
984
1190
  const { loadConflicts, saveConflicts, clearConflicts, resolveConflict, formatConflict } = await import('../merge.js');
985
1191
  const projectDir = process.cwd();
986
1192
  const data = loadConflicts(projectDir);
@@ -990,7 +1196,7 @@ export function register(program) {
990
1196
  return;
991
1197
  }
992
1198
 
993
- const unresolved = data.conflicts.filter(c => c.resolved === null);
1199
+ const unresolved = data.conflicts.filter((c: any) => c.resolved === null);
994
1200
 
995
1201
  if (unresolved.length === 0) {
996
1202
  console.log(fmt.status('success', 'All conflicts already resolved!'));
@@ -1036,16 +1242,15 @@ export function register(program) {
1036
1242
  console.log(formatConflict(conflict, baseText));
1037
1243
  console.log();
1038
1244
 
1039
- const rl = await import('readline');
1040
- const readline = rl.createInterface({
1245
+ const rl = readline.createInterface({
1041
1246
  input: process.stdin,
1042
1247
  output: process.stdout,
1043
1248
  });
1044
1249
 
1045
- const answer = await new Promise((resolve) =>
1046
- readline.question(chalk.cyan(` Choose (1-${conflict.changes.length}, s=skip, q=quit): `), resolve)
1250
+ const answer = await new Promise<string>((resolve) =>
1251
+ rl.question(chalk.cyan(` Choose (1-${conflict.changes.length}, s=skip, q=quit): `), resolve)
1047
1252
  );
1048
- readline.close();
1253
+ rl.close();
1049
1254
 
1050
1255
  if (answer.toLowerCase() === 'q') {
1051
1256
  console.log(chalk.dim('\n Saving progress...'));
@@ -1065,7 +1270,7 @@ export function register(program) {
1065
1270
 
1066
1271
  saveConflicts(projectDir, data.conflicts, data.base);
1067
1272
 
1068
- const remaining = data.conflicts.filter(c => c.resolved === null).length;
1273
+ const remaining = data.conflicts.filter((c: any) => c.resolved === null).length;
1069
1274
  if (remaining === 0) {
1070
1275
  console.log(fmt.status('success', '\nAll conflicts resolved!'));
1071
1276
  clearConflicts(projectDir);