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
@@ -7,12 +7,12 @@ import { execSync } from 'child_process';
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import { diffWords } from 'diff';
10
+ import type { FileChange, CommitInfo, ChangedFile, BlameEntry, AuthorStats, ContributorStats } from './types.js';
10
11
 
11
12
  /**
12
13
  * Check if current directory is a git repository
13
- * @returns {boolean}
14
14
  */
15
- export function isGitRepo() {
15
+ export function isGitRepo(): boolean {
16
16
  try {
17
17
  execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' });
18
18
  return true;
@@ -23,9 +23,8 @@ export function isGitRepo() {
23
23
 
24
24
  /**
25
25
  * Get the current git branch
26
- * @returns {string|null}
27
26
  */
28
- export function getCurrentBranch() {
27
+ export function getCurrentBranch(): string | null {
29
28
  try {
30
29
  return execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' })
31
30
  .toString()
@@ -37,15 +36,14 @@ export function getCurrentBranch() {
37
36
 
38
37
  /**
39
38
  * Get the default branch (main or master)
40
- * @returns {string}
41
39
  */
42
- export function getDefaultBranch() {
40
+ export function getDefaultBranch(): string {
43
41
  try {
44
42
  // Try to get the remote default branch
45
43
  const remote = execSync('git remote show origin', { stdio: 'pipe' })
46
44
  .toString();
47
45
  const match = remote.match(/HEAD branch:\s*(\S+)/);
48
- if (match) return match[1];
46
+ if (match?.[1]) return match[1];
49
47
  } catch {
50
48
  // Fall through
51
49
  }
@@ -66,11 +64,10 @@ export function getDefaultBranch() {
66
64
 
67
65
  /**
68
66
  * Get file content from a specific git ref
69
- * @param {string} filePath
70
- * @param {string} ref - Git reference (branch, tag, commit)
71
- * @returns {string|null}
67
+ * @param filePath - Path to file
68
+ * @param ref - Git reference (branch, tag, commit)
72
69
  */
73
- export function getFileAtRef(filePath, ref) {
70
+ export function getFileAtRef(filePath: string, ref: string): string | null {
74
71
  try {
75
72
  return execSync(`git show ${ref}:${filePath}`, { stdio: 'pipe' }).toString();
76
73
  } catch {
@@ -80,11 +77,10 @@ export function getFileAtRef(filePath, ref) {
80
77
 
81
78
  /**
82
79
  * Get list of changed files between refs
83
- * @param {string} fromRef
84
- * @param {string} toRef - Default: HEAD
85
- * @returns {Array<{file: string, status: string}>}
80
+ * @param fromRef - Starting reference
81
+ * @param toRef - Ending reference (default: HEAD)
86
82
  */
87
- export function getChangedFiles(fromRef, toRef = 'HEAD') {
83
+ export function getChangedFiles(fromRef: string, toRef: string = 'HEAD'): ChangedFile[] {
88
84
  try {
89
85
  const output = execSync(`git diff --name-status ${fromRef}..${toRef}`, { stdio: 'pipe' })
90
86
  .toString()
@@ -93,10 +89,12 @@ export function getChangedFiles(fromRef, toRef = 'HEAD') {
93
89
  if (!output) return [];
94
90
 
95
91
  return output.split('\n').map(line => {
96
- const [status, file] = line.split('\t');
92
+ const parts = line.split('\t');
93
+ const status = parts[0];
94
+ const file = parts[1] ?? '';
97
95
  return {
98
96
  file,
99
- status: status === 'A' ? 'added' : status === 'D' ? 'deleted' : 'modified',
97
+ status: (status === 'A' ? 'added' : status === 'D' ? 'deleted' : 'modified') as 'added' | 'deleted' | 'modified',
100
98
  };
101
99
  });
102
100
  } catch {
@@ -106,11 +104,10 @@ export function getChangedFiles(fromRef, toRef = 'HEAD') {
106
104
 
107
105
  /**
108
106
  * Get commit history for a file
109
- * @param {string} filePath
110
- * @param {number} limit
111
- * @returns {Array<{hash: string, date: string, message: string}>}
107
+ * @param filePath - Path to file
108
+ * @param limit - Maximum number of commits to return
112
109
  */
113
- export function getFileHistory(filePath, limit = 10) {
110
+ export function getFileHistory(filePath: string, limit: number = 10): CommitInfo[] {
114
111
  try {
115
112
  const output = execSync(
116
113
  `git log --format="%h|%ci|%s" -n ${limit} -- "${filePath}"`,
@@ -120,8 +117,13 @@ export function getFileHistory(filePath, limit = 10) {
120
117
  if (!output) return [];
121
118
 
122
119
  return output.split('\n').map(line => {
123
- const [hash, date, message] = line.split('|');
124
- return { hash, date, message };
120
+ const parts = line.split('|');
121
+ return {
122
+ hash: parts[0] ?? '',
123
+ date: parts[1] ?? '',
124
+ message: parts[2] ?? '',
125
+ author: ''
126
+ };
125
127
  });
126
128
  } catch {
127
129
  return [];
@@ -130,12 +132,11 @@ export function getFileHistory(filePath, limit = 10) {
130
132
 
131
133
  /**
132
134
  * Compare file content between two refs
133
- * @param {string} filePath
134
- * @param {string} fromRef
135
- * @param {string} toRef
136
- * @returns {{added: number, removed: number, changes: Array}}
135
+ * @param filePath - Path to file
136
+ * @param fromRef - Starting reference
137
+ * @param toRef - Ending reference (default: HEAD)
137
138
  */
138
- export function compareFileVersions(filePath, fromRef, toRef = 'HEAD') {
139
+ export function compareFileVersions(filePath: string, fromRef: string, toRef: string = 'HEAD'): FileChange {
139
140
  const oldContent = getFileAtRef(filePath, fromRef) || '';
140
141
  const newContent = toRef === 'HEAD'
141
142
  ? fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : ''
@@ -145,15 +146,15 @@ export function compareFileVersions(filePath, fromRef, toRef = 'HEAD') {
145
146
 
146
147
  let added = 0;
147
148
  let removed = 0;
148
- const changes = [];
149
+ const changes: Array<{ added?: boolean; removed?: boolean; value: string }> = [];
149
150
 
150
151
  for (const part of diffs) {
151
152
  if (part.added) {
152
153
  added += part.value.split(/\s+/).filter(w => w).length;
153
- changes.push({ type: 'add', text: part.value });
154
+ changes.push({ added: true, value: part.value });
154
155
  } else if (part.removed) {
155
156
  removed += part.value.split(/\s+/).filter(w => w).length;
156
- changes.push({ type: 'remove', text: part.value });
157
+ changes.push({ removed: true, value: part.value });
157
158
  }
158
159
  }
159
160
 
@@ -162,15 +163,18 @@ export function compareFileVersions(filePath, fromRef, toRef = 'HEAD') {
162
163
 
163
164
  /**
164
165
  * Get word count difference between refs
165
- * @param {string[]} files
166
- * @param {string} fromRef
167
- * @param {string} toRef
168
- * @returns {{total: {added: number, removed: number}, byFile: Object}}
166
+ * @param files - Array of file paths
167
+ * @param fromRef - Starting reference
168
+ * @param toRef - Ending reference (default: HEAD)
169
169
  */
170
- export function getWordCountDiff(files, fromRef, toRef = 'HEAD') {
170
+ export function getWordCountDiff(
171
+ files: string[],
172
+ fromRef: string,
173
+ toRef: string = 'HEAD'
174
+ ): { total: { added: number; removed: number }; byFile: Record<string, { added: number; removed: number }> } {
171
175
  let totalAdded = 0;
172
176
  let totalRemoved = 0;
173
- const byFile = {};
177
+ const byFile: Record<string, { added: number; removed: number }> = {};
174
178
 
175
179
  for (const file of files) {
176
180
  const { added, removed } = compareFileVersions(file, fromRef, toRef);
@@ -187,10 +191,9 @@ export function getWordCountDiff(files, fromRef, toRef = 'HEAD') {
187
191
 
188
192
  /**
189
193
  * Get recent commits
190
- * @param {number} limit
191
- * @returns {Array<{hash: string, date: string, message: string, author: string}>}
194
+ * @param limit - Maximum number of commits to return
192
195
  */
193
- export function getRecentCommits(limit = 10) {
196
+ export function getRecentCommits(limit: number = 10): CommitInfo[] {
194
197
  try {
195
198
  const output = execSync(
196
199
  `git log --format="%h|%ci|%an|%s" -n ${limit}`,
@@ -200,8 +203,13 @@ export function getRecentCommits(limit = 10) {
200
203
  if (!output) return [];
201
204
 
202
205
  return output.split('\n').map(line => {
203
- const [hash, date, author, message] = line.split('|');
204
- return { hash, date, author, message };
206
+ const parts = line.split('|');
207
+ return {
208
+ hash: parts[0] ?? '',
209
+ date: parts[1] ?? '',
210
+ author: parts[2] ?? '',
211
+ message: parts[3] ?? ''
212
+ };
205
213
  });
206
214
  } catch {
207
215
  return [];
@@ -210,9 +218,8 @@ export function getRecentCommits(limit = 10) {
210
218
 
211
219
  /**
212
220
  * Check if there are uncommitted changes
213
- * @returns {boolean}
214
221
  */
215
- export function hasUncommittedChanges() {
222
+ export function hasUncommittedChanges(): boolean {
216
223
  try {
217
224
  const output = execSync('git status --porcelain', { stdio: 'pipe' }).toString();
218
225
  return output.trim().length > 0;
@@ -223,9 +230,8 @@ export function hasUncommittedChanges() {
223
230
 
224
231
  /**
225
232
  * Get tags
226
- * @returns {string[]}
227
233
  */
228
- export function getTags() {
234
+ export function getTags(): string[] {
229
235
  try {
230
236
  return execSync('git tag --sort=-creatordate', { stdio: 'pipe' })
231
237
  .toString()
@@ -240,10 +246,9 @@ export function getTags() {
240
246
  /**
241
247
  * Get blame information for a file
242
248
  * Returns author and commit info for each line
243
- * @param {string} filePath
244
- * @returns {Array<{line: number, author: string, date: string, hash: string, content: string}>}
249
+ * @param filePath - Path to file
245
250
  */
246
- export function getFileBlame(filePath) {
251
+ export function getFileBlame(filePath: string): BlameEntry[] {
247
252
  try {
248
253
  const output = execSync(
249
254
  `git blame --line-porcelain "${filePath}"`,
@@ -251,16 +256,16 @@ export function getFileBlame(filePath) {
251
256
  ).toString();
252
257
 
253
258
  const lines = output.split('\n');
254
- const result = [];
255
- let current = {};
259
+ const result: BlameEntry[] = [];
260
+ let current: Partial<BlameEntry> = {};
256
261
  let lineNumber = 0;
257
262
 
258
263
  for (const line of lines) {
259
264
  if (/^[0-9a-f]{40}/.test(line)) {
260
265
  // New blame entry: hash original-line final-line [count]
261
266
  const parts = line.split(' ');
262
- current.hash = parts[0].slice(0, 7);
263
- lineNumber = parseInt(parts[2], 10);
267
+ current.hash = parts[0]?.slice(0, 7) ?? '';
268
+ lineNumber = parseInt(parts[2] ?? '0', 10);
264
269
  } else if (line.startsWith('author ')) {
265
270
  current.author = line.slice(7);
266
271
  } else if (line.startsWith('author-time ')) {
@@ -270,7 +275,7 @@ export function getFileBlame(filePath) {
270
275
  // Actual content line (prefixed with tab)
271
276
  current.content = line.slice(1);
272
277
  current.line = lineNumber;
273
- result.push({ ...current });
278
+ result.push(current as BlameEntry);
274
279
  current = {};
275
280
  }
276
281
  }
@@ -283,20 +288,19 @@ export function getFileBlame(filePath) {
283
288
 
284
289
  /**
285
290
  * Get author statistics for a file
286
- * @param {string} filePath
287
- * @returns {Object<string, {lines: number, percentage: number}>}
291
+ * @param filePath - Path to file
288
292
  */
289
- export function getAuthorStats(filePath) {
293
+ export function getAuthorStats(filePath: string): Record<string, AuthorStats> {
290
294
  const blame = getFileBlame(filePath);
291
295
  if (blame.length === 0) return {};
292
296
 
293
- const counts = {};
297
+ const counts: Record<string, number> = {};
294
298
  for (const entry of blame) {
295
299
  counts[entry.author] = (counts[entry.author] || 0) + 1;
296
300
  }
297
301
 
298
302
  const total = blame.length;
299
- const stats = {};
303
+ const stats: Record<string, AuthorStats> = {};
300
304
  for (const [author, lines] of Object.entries(counts)) {
301
305
  stats[author] = {
302
306
  lines,
@@ -309,11 +313,10 @@ export function getAuthorStats(filePath) {
309
313
 
310
314
  /**
311
315
  * Get contributors across multiple files
312
- * @param {string[]} files
313
- * @returns {Object<string, {lines: number, files: number}>}
316
+ * @param files - Array of file paths
314
317
  */
315
- export function getContributors(files) {
316
- const contributors = {};
318
+ export function getContributors(files: string[]): Record<string, ContributorStats> {
319
+ const contributors: Record<string, ContributorStats> = {};
317
320
 
318
321
  for (const file of files) {
319
322
  const stats = getAuthorStats(file);
@@ -13,10 +13,41 @@ import * as path from 'path';
13
13
  // Default dictionary location
14
14
  const DEFAULT_DICT_NAME = '.rev-dictionary';
15
15
 
16
+ interface GrammarRule {
17
+ id: string;
18
+ pattern: RegExp;
19
+ message: string;
20
+ severity: 'error' | 'warning' | 'info';
21
+ check?: (match: string) => boolean;
22
+ }
23
+
24
+ interface GrammarIssue {
25
+ rule: string;
26
+ message: string;
27
+ severity: 'error' | 'warning' | 'info';
28
+ line: number;
29
+ column: number;
30
+ match: string;
31
+ context: string;
32
+ }
33
+
34
+ interface GrammarSummary {
35
+ total: number;
36
+ errors: number;
37
+ warnings: number;
38
+ info: number;
39
+ byRule: Record<string, number>;
40
+ }
41
+
42
+ interface CheckGrammarOptions {
43
+ scientific?: boolean;
44
+ directory?: string;
45
+ }
46
+
16
47
  /**
17
48
  * Common grammar/style rules
18
49
  */
19
- const GRAMMAR_RULES = [
50
+ const GRAMMAR_RULES: GrammarRule[] = [
20
51
  {
21
52
  id: 'passive-voice',
22
53
  pattern: /\b(is|are|was|were|be|been|being)\s+(being\s+)?\w+ed\b/gi,
@@ -50,7 +81,7 @@ const GRAMMAR_RULES = [
50
81
  {
51
82
  id: 'sentence-length',
52
83
  pattern: /[^.!?]*[.!?]/g,
53
- check: (match) => {
84
+ check: (match: string): boolean => {
54
85
  const words = match.trim().split(/\s+/).length;
55
86
  return words > 40;
56
87
  },
@@ -80,7 +111,7 @@ const GRAMMAR_RULES = [
80
111
  /**
81
112
  * Scientific writing specific rules
82
113
  */
83
- const SCIENTIFIC_RULES = [
114
+ const SCIENTIFIC_RULES: GrammarRule[] = [
84
115
  {
85
116
  id: 'first-person',
86
117
  pattern: /\b(I|we|my|our)\b/gi,
@@ -109,12 +140,10 @@ const SCIENTIFIC_RULES = [
109
140
 
110
141
  /**
111
142
  * Load custom dictionary from file
112
- * @param {string} directory - Directory to search for dictionary
113
- * @returns {Set<string>} Set of custom words
114
143
  */
115
- export function loadDictionary(directory = '.') {
144
+ export function loadDictionary(directory: string = '.'): Set<string> {
116
145
  const dictPath = path.join(directory, DEFAULT_DICT_NAME);
117
- const words = new Set();
146
+ const words = new Set<string>();
118
147
 
119
148
  if (fs.existsSync(dictPath)) {
120
149
  const content = fs.readFileSync(dictPath, 'utf-8');
@@ -133,10 +162,8 @@ export function loadDictionary(directory = '.') {
133
162
 
134
163
  /**
135
164
  * Save custom dictionary to file
136
- * @param {Set<string>} words - Set of words
137
- * @param {string} directory - Directory to save dictionary
138
165
  */
139
- export function saveDictionary(words, directory = '.') {
166
+ export function saveDictionary(words: Set<string>, directory: string = '.'): void {
140
167
  const dictPath = path.join(directory, DEFAULT_DICT_NAME);
141
168
 
142
169
  const header = `# Custom dictionary for docrev
@@ -150,11 +177,8 @@ export function saveDictionary(words, directory = '.') {
150
177
 
151
178
  /**
152
179
  * Add word to custom dictionary
153
- * @param {string} word - Word to add
154
- * @param {string} directory - Directory containing dictionary
155
- * @returns {boolean} True if word was added (not already present)
156
180
  */
157
- export function addToDictionary(word, directory = '.') {
181
+ export function addToDictionary(word: string, directory: string = '.'): boolean {
158
182
  const words = loadDictionary(directory);
159
183
  const normalizedWord = word.trim().toLowerCase();
160
184
 
@@ -169,11 +193,8 @@ export function addToDictionary(word, directory = '.') {
169
193
 
170
194
  /**
171
195
  * Remove word from custom dictionary
172
- * @param {string} word - Word to remove
173
- * @param {string} directory - Directory containing dictionary
174
- * @returns {boolean} True if word was removed
175
196
  */
176
- export function removeFromDictionary(word, directory = '.') {
197
+ export function removeFromDictionary(word: string, directory: string = '.'): boolean {
177
198
  const words = loadDictionary(directory);
178
199
  const normalizedWord = word.trim().toLowerCase();
179
200
 
@@ -188,16 +209,11 @@ export function removeFromDictionary(word, directory = '.') {
188
209
 
189
210
  /**
190
211
  * Check text for grammar/style issues
191
- * @param {string} text - Text to check
192
- * @param {object} options - Options
193
- * @param {boolean} options.scientific - Include scientific writing rules
194
- * @param {string} options.directory - Directory for custom dictionary
195
- * @returns {Array<{rule: string, message: string, severity: string, line: number, match: string}>}
196
212
  */
197
- export function checkGrammar(text, options = {}) {
213
+ export function checkGrammar(text: string, options: CheckGrammarOptions = {}): GrammarIssue[] {
198
214
  const { scientific = true, directory = '.' } = options;
199
215
  const customDict = loadDictionary(directory);
200
- const issues = [];
216
+ const issues: GrammarIssue[] = [];
201
217
 
202
218
  // Get all rules
203
219
  const rules = scientific ? [...GRAMMAR_RULES, ...SCIENTIFIC_RULES] : GRAMMAR_RULES;
@@ -207,6 +223,7 @@ export function checkGrammar(text, options = {}) {
207
223
 
208
224
  for (let lineNum = 0; lineNum < lines.length; lineNum++) {
209
225
  const line = lines[lineNum];
226
+ if (!line) continue;
210
227
 
211
228
  // Skip code blocks and YAML frontmatter
212
229
  if (line.trim().startsWith('```') || line.trim().startsWith('---')) {
@@ -252,11 +269,9 @@ export function checkGrammar(text, options = {}) {
252
269
 
253
270
  /**
254
271
  * Get grammar check summary
255
- * @param {Array} issues - List of issues from checkGrammar
256
- * @returns {object} Summary stats
257
272
  */
258
- export function getGrammarSummary(issues) {
259
- const summary = {
273
+ export function getGrammarSummary(issues: GrammarIssue[]): GrammarSummary {
274
+ const summary: GrammarSummary = {
260
275
  total: issues.length,
261
276
  errors: 0,
262
277
  warnings: 0,
@@ -277,10 +292,8 @@ export function getGrammarSummary(issues) {
277
292
 
278
293
  /**
279
294
  * List available grammar rules
280
- * @param {boolean} scientific - Include scientific rules
281
- * @returns {Array<{id: string, message: string, severity: string}>}
282
295
  */
283
- export function listRules(scientific = true) {
296
+ export function listRules(scientific: boolean = true): Array<{ id: string; message: string; severity: string }> {
284
297
  const rules = scientific ? [...GRAMMAR_RULES, ...SCIENTIFIC_RULES] : GRAMMAR_RULES;
285
298
  return rules.map(r => ({
286
299
  id: r.id,
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Image registry utilities for tracking figures and tables in markdown documents
3
+ *
4
+ * The registry maps figure/table labels and display numbers to source paths,
5
+ * enabling Word import to match rendered figures back to original sources.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+
11
+ /**
12
+ * Image registry entry
13
+ */
14
+ interface ImageEntry {
15
+ caption: string;
16
+ path: string;
17
+ label: string | null;
18
+ type: 'fig' | 'tbl';
19
+ number?: string;
20
+ }
21
+
22
+ /**
23
+ * Image registry
24
+ */
25
+ interface ImageRegistry {
26
+ figures: ImageEntry[];
27
+ byLabel: Map<string, ImageEntry>;
28
+ byNumber: Map<string, ImageEntry>;
29
+ byCaption: Map<string, ImageEntry>;
30
+ }
31
+
32
+ /**
33
+ * Crossref registry info
34
+ */
35
+ interface CrossrefInfo {
36
+ num: number;
37
+ isSupp: boolean;
38
+ }
39
+
40
+ /**
41
+ * Crossref registry
42
+ */
43
+ interface CrossrefRegistry {
44
+ figures: Map<string, CrossrefInfo>;
45
+ tables: Map<string, CrossrefInfo>;
46
+ }
47
+
48
+ /**
49
+ * Saved registry data
50
+ */
51
+ interface RegistryData {
52
+ version: number;
53
+ created: string;
54
+ figures: ImageEntry[];
55
+ }
56
+
57
+ /**
58
+ * Pattern to extract markdown images with optional pandoc-crossref anchors
59
+ * Captures: ![caption](path){#fig:label} or ![caption](path)
60
+ * Groups: [1] = caption, [2] = path, [3] = label type (fig/tbl), [4] = label
61
+ */
62
+ export const IMAGE_PATTERN = /!\[([^\]]*)\]\(([^)]+)\)(?:\{#(fig|tbl):([^}]+)\})?/g;
63
+
64
+ /**
65
+ * Build image registry from markdown content
66
+ * Maps figure labels and display numbers to source paths
67
+ */
68
+ export function buildImageRegistry(
69
+ content: string,
70
+ crossrefRegistry: CrossrefRegistry | null = null
71
+ ): ImageRegistry {
72
+ const figures: ImageEntry[] = [];
73
+ const byLabel = new Map<string, ImageEntry>();
74
+ const byNumber = new Map<string, ImageEntry>();
75
+ const byCaption = new Map<string, ImageEntry>();
76
+
77
+ IMAGE_PATTERN.lastIndex = 0;
78
+ let match: RegExpExecArray | null;
79
+
80
+ while ((match = IMAGE_PATTERN.exec(content)) !== null) {
81
+ const caption = match[1];
82
+ const imagePath = match[2];
83
+ const labelType = match[3] as 'fig' | 'tbl' | undefined; // 'fig' or 'tbl' or undefined
84
+ const label = match[4]; // label without prefix
85
+
86
+ const entry: ImageEntry = {
87
+ caption,
88
+ path: imagePath,
89
+ label: label || null,
90
+ type: labelType || 'fig',
91
+ };
92
+
93
+ // Add display number if we have a crossref registry
94
+ if (label && crossrefRegistry) {
95
+ const info = crossrefRegistry.figures.get(label) || crossrefRegistry.tables.get(label);
96
+ if (info) {
97
+ entry.number = info.isSupp ? `S${info.num}` : `${info.num}`;
98
+ byNumber.set(`${entry.type}:${entry.number}`, entry);
99
+ }
100
+ }
101
+
102
+ figures.push(entry);
103
+
104
+ if (label) {
105
+ byLabel.set(`${labelType || 'fig'}:${label}`, entry);
106
+ }
107
+
108
+ // Index by first 50 chars of caption for fuzzy matching
109
+ if (caption) {
110
+ const captionKey = caption.slice(0, 50).toLowerCase().trim();
111
+ byCaption.set(captionKey, entry);
112
+ }
113
+ }
114
+
115
+ return { figures, byLabel, byNumber, byCaption };
116
+ }
117
+
118
+ /**
119
+ * Write image registry to .rev directory
120
+ */
121
+ export function writeImageRegistry(directory: string, registry: ImageRegistry): string {
122
+ const revDir = path.join(directory, '.rev');
123
+ if (!fs.existsSync(revDir)) {
124
+ fs.mkdirSync(revDir, { recursive: true });
125
+ }
126
+
127
+ // Convert Maps to objects for JSON serialization
128
+ const data: RegistryData = {
129
+ version: 1,
130
+ created: new Date().toISOString(),
131
+ figures: registry.figures,
132
+ };
133
+
134
+ const registryPath = path.join(revDir, 'image-registry.json');
135
+ fs.writeFileSync(registryPath, JSON.stringify(data, null, 2), 'utf-8');
136
+
137
+ return registryPath;
138
+ }
139
+
140
+ /**
141
+ * Read image registry from .rev directory
142
+ */
143
+ export function readImageRegistry(directory: string): (ImageRegistry & RegistryData) | null {
144
+ const registryPath = path.join(directory, '.rev', 'image-registry.json');
145
+
146
+ if (!fs.existsSync(registryPath)) {
147
+ return null;
148
+ }
149
+
150
+ try {
151
+ const data = JSON.parse(fs.readFileSync(registryPath, 'utf-8')) as RegistryData;
152
+
153
+ // Rebuild lookup maps from figures array
154
+ const byLabel = new Map<string, ImageEntry>();
155
+ const byNumber = new Map<string, ImageEntry>();
156
+ const byCaption = new Map<string, ImageEntry>();
157
+
158
+ for (const entry of data.figures || []) {
159
+ if (entry.label) {
160
+ byLabel.set(`${entry.type || 'fig'}:${entry.label}`, entry);
161
+ }
162
+ if (entry.number) {
163
+ byNumber.set(`${entry.type || 'fig'}:${entry.number}`, entry);
164
+ }
165
+ if (entry.caption) {
166
+ const captionKey = entry.caption.slice(0, 50).toLowerCase().trim();
167
+ byCaption.set(captionKey, entry);
168
+ }
169
+ }
170
+
171
+ return {
172
+ ...data,
173
+ byLabel,
174
+ byNumber,
175
+ byCaption,
176
+ };
177
+ } catch (err) {
178
+ return null;
179
+ }
180
+ }