docrev 0.8.1 → 0.9.0

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 (307) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.gitattributes +1 -0
  3. package/PLAN-tables-and-postprocess.md +850 -0
  4. package/README.md +33 -0
  5. package/bin/rev.js +12 -131
  6. package/bin/rev.ts +145 -0
  7. package/dist/bin/rev.d.ts +9 -0
  8. package/dist/bin/rev.d.ts.map +1 -0
  9. package/dist/bin/rev.js +118 -0
  10. package/dist/bin/rev.js.map +1 -0
  11. package/dist/lib/annotations.d.ts +91 -0
  12. package/dist/lib/annotations.d.ts.map +1 -0
  13. package/dist/lib/annotations.js +554 -0
  14. package/dist/lib/annotations.js.map +1 -0
  15. package/dist/lib/build.d.ts +171 -0
  16. package/dist/lib/build.d.ts.map +1 -0
  17. package/dist/lib/build.js +755 -0
  18. package/dist/lib/build.js.map +1 -0
  19. package/dist/lib/citations.d.ts +34 -0
  20. package/dist/lib/citations.d.ts.map +1 -0
  21. package/dist/lib/citations.js +140 -0
  22. package/dist/lib/citations.js.map +1 -0
  23. package/dist/lib/commands/build.d.ts +13 -0
  24. package/dist/lib/commands/build.d.ts.map +1 -0
  25. package/dist/lib/commands/build.js +678 -0
  26. package/dist/lib/commands/build.js.map +1 -0
  27. package/dist/lib/commands/citations.d.ts +11 -0
  28. package/dist/lib/commands/citations.d.ts.map +1 -0
  29. package/dist/lib/commands/citations.js +428 -0
  30. package/dist/lib/commands/citations.js.map +1 -0
  31. package/dist/lib/commands/comments.d.ts +11 -0
  32. package/dist/lib/commands/comments.d.ts.map +1 -0
  33. package/dist/lib/commands/comments.js +883 -0
  34. package/dist/lib/commands/comments.js.map +1 -0
  35. package/dist/lib/commands/context.d.ts +35 -0
  36. package/dist/lib/commands/context.d.ts.map +1 -0
  37. package/dist/lib/commands/context.js +59 -0
  38. package/dist/lib/commands/context.js.map +1 -0
  39. package/dist/lib/commands/core.d.ts +11 -0
  40. package/dist/lib/commands/core.d.ts.map +1 -0
  41. package/dist/lib/commands/core.js +246 -0
  42. package/dist/lib/commands/core.js.map +1 -0
  43. package/dist/lib/commands/doi.d.ts +11 -0
  44. package/dist/lib/commands/doi.d.ts.map +1 -0
  45. package/dist/lib/commands/doi.js +373 -0
  46. package/dist/lib/commands/doi.js.map +1 -0
  47. package/dist/lib/commands/history.d.ts +11 -0
  48. package/dist/lib/commands/history.d.ts.map +1 -0
  49. package/dist/lib/commands/history.js +245 -0
  50. package/dist/lib/commands/history.js.map +1 -0
  51. package/dist/lib/commands/index.d.ts +28 -0
  52. package/dist/lib/commands/index.d.ts.map +1 -0
  53. package/dist/lib/commands/index.js +35 -0
  54. package/dist/lib/commands/index.js.map +1 -0
  55. package/dist/lib/commands/init.d.ts +11 -0
  56. package/dist/lib/commands/init.d.ts.map +1 -0
  57. package/dist/lib/commands/init.js +209 -0
  58. package/dist/lib/commands/init.js.map +1 -0
  59. package/dist/lib/commands/response.d.ts +11 -0
  60. package/dist/lib/commands/response.d.ts.map +1 -0
  61. package/dist/lib/commands/response.js +317 -0
  62. package/dist/lib/commands/response.js.map +1 -0
  63. package/dist/lib/commands/sections.d.ts +11 -0
  64. package/dist/lib/commands/sections.d.ts.map +1 -0
  65. package/dist/lib/commands/sections.js +1071 -0
  66. package/dist/lib/commands/sections.js.map +1 -0
  67. package/dist/lib/commands/utilities.d.ts +19 -0
  68. package/dist/lib/commands/utilities.d.ts.map +1 -0
  69. package/dist/lib/commands/utilities.js +2009 -0
  70. package/dist/lib/commands/utilities.js.map +1 -0
  71. package/dist/lib/comment-realign.d.ts +50 -0
  72. package/dist/lib/comment-realign.d.ts.map +1 -0
  73. package/dist/lib/comment-realign.js +372 -0
  74. package/dist/lib/comment-realign.js.map +1 -0
  75. package/dist/lib/config.d.ts +41 -0
  76. package/dist/lib/config.d.ts.map +1 -0
  77. package/dist/lib/config.js +76 -0
  78. package/dist/lib/config.js.map +1 -0
  79. package/dist/lib/crossref.d.ts +108 -0
  80. package/dist/lib/crossref.d.ts.map +1 -0
  81. package/dist/lib/crossref.js +597 -0
  82. package/dist/lib/crossref.js.map +1 -0
  83. package/dist/lib/dependencies.d.ts +30 -0
  84. package/dist/lib/dependencies.d.ts.map +1 -0
  85. package/dist/lib/dependencies.js +95 -0
  86. package/dist/lib/dependencies.js.map +1 -0
  87. package/dist/lib/doi-cache.d.ts +29 -0
  88. package/dist/lib/doi-cache.d.ts.map +1 -0
  89. package/dist/lib/doi-cache.js +104 -0
  90. package/dist/lib/doi-cache.js.map +1 -0
  91. package/dist/lib/doi.d.ts +65 -0
  92. package/dist/lib/doi.d.ts.map +1 -0
  93. package/dist/lib/doi.js +710 -0
  94. package/dist/lib/doi.js.map +1 -0
  95. package/dist/lib/equations.d.ts +61 -0
  96. package/dist/lib/equations.d.ts.map +1 -0
  97. package/dist/lib/equations.js +445 -0
  98. package/dist/lib/equations.js.map +1 -0
  99. package/dist/lib/errors.d.ts +60 -0
  100. package/dist/lib/errors.d.ts.map +1 -0
  101. package/dist/lib/errors.js +303 -0
  102. package/dist/lib/errors.js.map +1 -0
  103. package/dist/lib/format.d.ts +104 -0
  104. package/dist/lib/format.d.ts.map +1 -0
  105. package/dist/lib/format.js +416 -0
  106. package/dist/lib/format.js.map +1 -0
  107. package/dist/lib/git.d.ts +88 -0
  108. package/dist/lib/git.d.ts.map +1 -0
  109. package/dist/lib/git.js +304 -0
  110. package/dist/lib/git.js.map +1 -0
  111. package/dist/lib/grammar.d.ts +62 -0
  112. package/dist/lib/grammar.d.ts.map +1 -0
  113. package/dist/lib/grammar.js +244 -0
  114. package/dist/lib/grammar.js.map +1 -0
  115. package/dist/lib/image-registry.d.ts +68 -0
  116. package/dist/lib/image-registry.d.ts.map +1 -0
  117. package/dist/lib/image-registry.js +112 -0
  118. package/dist/lib/image-registry.js.map +1 -0
  119. package/dist/lib/import.d.ts +184 -0
  120. package/dist/lib/import.d.ts.map +1 -0
  121. package/dist/lib/import.js +1581 -0
  122. package/dist/lib/import.js.map +1 -0
  123. package/dist/lib/journals.d.ts +55 -0
  124. package/dist/lib/journals.d.ts.map +1 -0
  125. package/dist/lib/journals.js +417 -0
  126. package/dist/lib/journals.js.map +1 -0
  127. package/dist/lib/merge.d.ts +138 -0
  128. package/dist/lib/merge.d.ts.map +1 -0
  129. package/dist/lib/merge.js +603 -0
  130. package/dist/lib/merge.js.map +1 -0
  131. package/dist/lib/orcid.d.ts +36 -0
  132. package/dist/lib/orcid.d.ts.map +1 -0
  133. package/dist/lib/orcid.js +117 -0
  134. package/dist/lib/orcid.js.map +1 -0
  135. package/dist/lib/pdf-comments.d.ts +95 -0
  136. package/dist/lib/pdf-comments.d.ts.map +1 -0
  137. package/dist/lib/pdf-comments.js +192 -0
  138. package/dist/lib/pdf-comments.js.map +1 -0
  139. package/dist/lib/pdf-import.d.ts +118 -0
  140. package/dist/lib/pdf-import.d.ts.map +1 -0
  141. package/dist/lib/pdf-import.js +397 -0
  142. package/dist/lib/pdf-import.js.map +1 -0
  143. package/dist/lib/plugins.d.ts +76 -0
  144. package/dist/lib/plugins.d.ts.map +1 -0
  145. package/dist/lib/plugins.js +235 -0
  146. package/dist/lib/plugins.js.map +1 -0
  147. package/dist/lib/postprocess.d.ts +42 -0
  148. package/dist/lib/postprocess.d.ts.map +1 -0
  149. package/dist/lib/postprocess.js +138 -0
  150. package/dist/lib/postprocess.js.map +1 -0
  151. package/dist/lib/pptx-template.d.ts +59 -0
  152. package/dist/lib/pptx-template.d.ts.map +1 -0
  153. package/dist/lib/pptx-template.js +613 -0
  154. package/dist/lib/pptx-template.js.map +1 -0
  155. package/dist/lib/pptx-themes.d.ts +80 -0
  156. package/dist/lib/pptx-themes.d.ts.map +1 -0
  157. package/dist/lib/pptx-themes.js +818 -0
  158. package/dist/lib/pptx-themes.js.map +1 -0
  159. package/dist/lib/protect-restore.d.ts +137 -0
  160. package/dist/lib/protect-restore.d.ts.map +1 -0
  161. package/dist/lib/protect-restore.js +394 -0
  162. package/dist/lib/protect-restore.js.map +1 -0
  163. package/dist/lib/rate-limiter.d.ts +27 -0
  164. package/dist/lib/rate-limiter.d.ts.map +1 -0
  165. package/dist/lib/rate-limiter.js +79 -0
  166. package/dist/lib/rate-limiter.js.map +1 -0
  167. package/dist/lib/response.d.ts +41 -0
  168. package/dist/lib/response.d.ts.map +1 -0
  169. package/dist/lib/response.js +150 -0
  170. package/dist/lib/response.js.map +1 -0
  171. package/dist/lib/review.d.ts +35 -0
  172. package/dist/lib/review.d.ts.map +1 -0
  173. package/dist/lib/review.js +263 -0
  174. package/dist/lib/review.js.map +1 -0
  175. package/dist/lib/schema.d.ts +66 -0
  176. package/dist/lib/schema.d.ts.map +1 -0
  177. package/dist/lib/schema.js +339 -0
  178. package/dist/lib/schema.js.map +1 -0
  179. package/dist/lib/scientific-words.d.ts +6 -0
  180. package/dist/lib/scientific-words.d.ts.map +1 -0
  181. package/dist/lib/scientific-words.js +66 -0
  182. package/dist/lib/scientific-words.js.map +1 -0
  183. package/dist/lib/sections.d.ts +40 -0
  184. package/dist/lib/sections.d.ts.map +1 -0
  185. package/dist/lib/sections.js +288 -0
  186. package/dist/lib/sections.js.map +1 -0
  187. package/dist/lib/slides.d.ts +86 -0
  188. package/dist/lib/slides.d.ts.map +1 -0
  189. package/dist/lib/slides.js +676 -0
  190. package/dist/lib/slides.js.map +1 -0
  191. package/dist/lib/spelling.d.ts +76 -0
  192. package/dist/lib/spelling.d.ts.map +1 -0
  193. package/dist/lib/spelling.js +272 -0
  194. package/dist/lib/spelling.js.map +1 -0
  195. package/dist/lib/templates.d.ts +30 -0
  196. package/dist/lib/templates.d.ts.map +1 -0
  197. package/dist/lib/templates.js +504 -0
  198. package/dist/lib/templates.js.map +1 -0
  199. package/dist/lib/themes.d.ts +85 -0
  200. package/dist/lib/themes.d.ts.map +1 -0
  201. package/dist/lib/themes.js +652 -0
  202. package/dist/lib/themes.js.map +1 -0
  203. package/dist/lib/trackchanges.d.ts +51 -0
  204. package/dist/lib/trackchanges.d.ts.map +1 -0
  205. package/dist/lib/trackchanges.js +202 -0
  206. package/dist/lib/trackchanges.js.map +1 -0
  207. package/dist/lib/tui.d.ts +76 -0
  208. package/dist/lib/tui.d.ts.map +1 -0
  209. package/dist/lib/tui.js +377 -0
  210. package/dist/lib/tui.js.map +1 -0
  211. package/dist/lib/types.d.ts +447 -0
  212. package/dist/lib/types.d.ts.map +1 -0
  213. package/dist/lib/types.js +6 -0
  214. package/dist/lib/types.js.map +1 -0
  215. package/dist/lib/undo.d.ts +57 -0
  216. package/dist/lib/undo.d.ts.map +1 -0
  217. package/dist/lib/undo.js +185 -0
  218. package/dist/lib/undo.js.map +1 -0
  219. package/dist/lib/utils.d.ts +16 -0
  220. package/dist/lib/utils.d.ts.map +1 -0
  221. package/dist/lib/utils.js +40 -0
  222. package/dist/lib/utils.js.map +1 -0
  223. package/dist/lib/variables.d.ts +42 -0
  224. package/dist/lib/variables.d.ts.map +1 -0
  225. package/dist/lib/variables.js +141 -0
  226. package/dist/lib/variables.js.map +1 -0
  227. package/dist/lib/word.d.ts +80 -0
  228. package/dist/lib/word.d.ts.map +1 -0
  229. package/dist/lib/word.js +360 -0
  230. package/dist/lib/word.js.map +1 -0
  231. package/dist/lib/wordcomments.d.ts +51 -0
  232. package/dist/lib/wordcomments.d.ts.map +1 -0
  233. package/dist/lib/wordcomments.js +587 -0
  234. package/dist/lib/wordcomments.js.map +1 -0
  235. package/eslint.config.js +27 -0
  236. package/lib/annotations.ts +622 -0
  237. package/lib/apply-buildup-colors.py +88 -0
  238. package/lib/build.ts +1013 -0
  239. package/lib/{citations.js → citations.ts} +38 -27
  240. package/lib/commands/{build.js → build.ts} +80 -27
  241. package/lib/commands/{citations.js → citations.ts} +36 -18
  242. package/lib/commands/{comments.js → comments.ts} +187 -54
  243. package/lib/commands/{context.js → context.ts} +18 -8
  244. package/lib/commands/{core.js → core.ts} +34 -20
  245. package/lib/commands/{doi.js → doi.ts} +32 -16
  246. package/lib/commands/{history.js → history.ts} +25 -12
  247. package/lib/commands/{index.js → index.ts} +9 -5
  248. package/lib/commands/{init.js → init.ts} +20 -8
  249. package/lib/commands/{response.js → response.ts} +47 -20
  250. package/lib/commands/{sections.js → sections.ts} +273 -68
  251. package/lib/commands/{utilities.js → utilities.ts} +338 -158
  252. package/lib/{comment-realign.js → comment-realign.ts} +117 -45
  253. package/lib/config.ts +84 -0
  254. package/lib/{crossref.js → crossref.ts} +213 -138
  255. package/lib/dependencies.ts +106 -0
  256. package/lib/doi-cache.ts +115 -0
  257. package/lib/{doi.js → doi.ts} +115 -281
  258. package/lib/{equations.js → equations.ts} +60 -64
  259. package/lib/{errors.js → errors.ts} +56 -48
  260. package/lib/{format.js → format.ts} +137 -63
  261. package/lib/{git.js → git.ts} +66 -63
  262. package/lib/{grammar.js → grammar.ts} +45 -32
  263. package/lib/image-registry.ts +180 -0
  264. package/lib/import.ts +2060 -0
  265. package/lib/journals.ts +505 -0
  266. package/lib/{merge.js → merge.ts} +185 -135
  267. package/lib/{orcid.js → orcid.ts} +17 -22
  268. package/lib/{pdf-comments.js → pdf-comments.ts} +76 -18
  269. package/lib/{pdf-import.js → pdf-import.ts} +148 -70
  270. package/lib/{plugins.js → plugins.ts} +82 -39
  271. package/lib/postprocess.ts +188 -0
  272. package/lib/pptx-color-filter.lua +37 -0
  273. package/lib/pptx-template.ts +625 -0
  274. package/lib/pptx-themes/academic.pptx +0 -0
  275. package/lib/pptx-themes/corporate.pptx +0 -0
  276. package/lib/pptx-themes/dark.pptx +0 -0
  277. package/lib/pptx-themes/default.pptx +0 -0
  278. package/lib/pptx-themes/minimal.pptx +0 -0
  279. package/lib/pptx-themes/plant.pptx +0 -0
  280. package/lib/pptx-themes.ts +896 -0
  281. package/lib/protect-restore.ts +516 -0
  282. package/lib/rate-limiter.ts +94 -0
  283. package/lib/{response.js → response.ts} +36 -21
  284. package/lib/{review.js → review.ts} +53 -43
  285. package/lib/{schema.js → schema.ts} +70 -25
  286. package/lib/{sections.js → sections.ts} +71 -76
  287. package/lib/slides.ts +793 -0
  288. package/lib/{spelling.js → spelling.ts} +43 -59
  289. package/lib/{templates.js → templates.ts} +20 -17
  290. package/lib/themes.ts +742 -0
  291. package/lib/{trackchanges.js → trackchanges.ts} +52 -23
  292. package/lib/types.ts +509 -0
  293. package/lib/{undo.js → undo.ts} +75 -52
  294. package/lib/utils.ts +41 -0
  295. package/lib/{variables.js → variables.ts} +60 -54
  296. package/lib/word.ts +428 -0
  297. package/lib/{wordcomments.js → wordcomments.ts} +94 -40
  298. package/package.json +15 -5
  299. package/skill/REFERENCE.md +67 -0
  300. package/tsconfig.json +26 -0
  301. package/lib/annotations.js +0 -414
  302. package/lib/build.js +0 -639
  303. package/lib/config.js +0 -79
  304. package/lib/import.js +0 -1145
  305. package/lib/journals.js +0 -629
  306. package/lib/word.js +0 -225
  307. /package/lib/{scientific-words.js → scientific-words.ts} +0 -0
@@ -17,7 +17,45 @@ const MARKER_START_PREFIX = '⟦CMS:';
17
17
  const MARKER_END_PREFIX = '⟦CME:';
18
18
  const MARKER_SUFFIX = '⟧';
19
19
 
20
- function escapeXml(str) {
20
+ interface ParsedComment {
21
+ author: string;
22
+ text: string;
23
+ anchor: string | null;
24
+ start: number;
25
+ end: number;
26
+ fullMatch: string;
27
+ }
28
+
29
+ interface PreparedComment extends ParsedComment {
30
+ isReply: boolean;
31
+ parentIdx: number | null;
32
+ commentIdx: number;
33
+ anchorFromReply?: boolean;
34
+ placesParentMarkers?: boolean;
35
+ }
36
+
37
+ interface PrepareResult {
38
+ markedMarkdown: string;
39
+ comments: PreparedComment[];
40
+ }
41
+
42
+ interface CommentWithIds extends PreparedComment {
43
+ id: string;
44
+ paraId: string;
45
+ paraId2: string;
46
+ durableId: string;
47
+ parentParaId?: string;
48
+ }
49
+
50
+ interface InjectionResult {
51
+ success: boolean;
52
+ commentCount: number;
53
+ replyCount?: number;
54
+ skippedComments: number;
55
+ error?: string;
56
+ }
57
+
58
+ function escapeXml(str: string): string {
21
59
  return str
22
60
  .replace(/&/g, '&')
23
61
  .replace(/</g, '&lt;')
@@ -26,7 +64,7 @@ function escapeXml(str) {
26
64
  .replace(/'/g, '&apos;');
27
65
  }
28
66
 
29
- function generateParaId(commentIdx, paraNum) {
67
+ function generateParaId(commentIdx: number, paraNum: number): string {
30
68
  // Generate 8-character uppercase hex ID matching Word format
31
69
  // Word uses IDs like "3F25BC58", "0331C187"
32
70
  // Must be deterministic - same inputs always produce same output
@@ -41,14 +79,14 @@ function generateParaId(commentIdx, paraNum) {
41
79
  * - markedMarkdown: markdown with markers for parent comments only
42
80
  * - comments: array with author, text, isReply, parentIdx
43
81
  */
44
- export function prepareMarkdownWithMarkers(markdown) {
82
+ export function prepareMarkdownWithMarkers(markdown: string): PrepareResult {
45
83
  // Match all comments with optional anchor
46
84
  const commentPattern = /\{>>(.+?)<<\}(?:\s*\[([^\]]+)\]\{\.mark\})?/g;
47
85
 
48
- const rawMatches = [];
49
- let match;
86
+ const rawMatches: ParsedComment[] = [];
87
+ let match: RegExpExecArray | null;
50
88
  while ((match = commentPattern.exec(markdown)) !== null) {
51
- const content = match[1];
89
+ const content = match[1] ?? '';
52
90
  let author = 'Unknown';
53
91
  let text = content;
54
92
  const colonIdx = content.indexOf(':');
@@ -75,12 +113,13 @@ export function prepareMarkdownWithMarkers(markdown) {
75
113
  // First comment in a cluster = parent, all subsequent = replies to that parent
76
114
  // Comments are "adjacent" if there's minimal text between them (< 10 chars)
77
115
  const ADJACENT_THRESHOLD = 10;
78
- const comments = [];
116
+ const comments: PreparedComment[] = [];
79
117
  let clusterParentIdx = -1; // Index of first comment in current cluster
80
118
  let lastCommentEnd = -1;
81
119
 
82
120
  for (let i = 0; i < rawMatches.length; i++) {
83
121
  const m = rawMatches[i];
122
+ if (!m) continue;
84
123
 
85
124
  // Check if this comment is adjacent to the previous one
86
125
  const gap = lastCommentEnd >= 0 ? m.start - lastCommentEnd : Infinity;
@@ -94,7 +133,12 @@ export function prepareMarkdownWithMarkers(markdown) {
94
133
  if (clusterParentIdx === -1) {
95
134
  // First comment in cluster = parent (regardless of author)
96
135
  comments.push({
97
- ...m,
136
+ author: m.author,
137
+ text: m.text,
138
+ anchor: m.anchor,
139
+ start: m.start,
140
+ end: m.end,
141
+ fullMatch: m.fullMatch,
98
142
  isReply: false,
99
143
  parentIdx: null,
100
144
  commentIdx: comments.length
@@ -103,7 +147,12 @@ export function prepareMarkdownWithMarkers(markdown) {
103
147
  } else {
104
148
  // Subsequent comment in cluster = reply to first comment
105
149
  comments.push({
106
- ...m,
150
+ author: m.author,
151
+ text: m.text,
152
+ anchor: m.anchor,
153
+ start: m.start,
154
+ end: m.end,
155
+ fullMatch: m.fullMatch,
107
156
  isReply: true,
108
157
  parentIdx: clusterParentIdx,
109
158
  commentIdx: comments.length
@@ -119,7 +168,7 @@ export function prepareMarkdownWithMarkers(markdown) {
119
168
  for (const c of comments) {
120
169
  if (c.isReply && c.anchor && c.parentIdx !== null) {
121
170
  const parent = comments[c.parentIdx];
122
- if (!parent.anchor) {
171
+ if (parent && !parent.anchor) {
123
172
  parent.anchor = c.anchor;
124
173
  parent.anchorFromReply = true; // Parent's anchor came from a reply (markers placed by reply)
125
174
  c.placesParentMarkers = true; // This reply should place the parent's markers
@@ -134,12 +183,14 @@ export function prepareMarkdownWithMarkers(markdown) {
134
183
 
135
184
  for (let i = comments.length - 1; i >= 0; i--) {
136
185
  const c = comments[i];
186
+ if (!c) continue;
137
187
 
138
188
  if (c.isReply) {
139
189
  // Reply: remove from document entirely (will be in comments.xml only)
140
190
  // Also consume leading whitespace to avoid double spaces
141
191
  let removeStart = c.start;
142
- while (removeStart > 0 && /\s/.test(markedMarkdown[removeStart - 1])) {
192
+ const charBefore = markedMarkdown[removeStart - 1];
193
+ while (removeStart > 0 && charBefore && /\s/.test(charBefore)) {
143
194
  removeStart--;
144
195
  }
145
196
 
@@ -148,7 +199,7 @@ export function prepareMarkdownWithMarkers(markdown) {
148
199
  // Extract anchor text from the original match
149
200
  const anchorMatch = c.fullMatch.match(/\[([^\]]+)\]\{\.mark\}$/);
150
201
  if (anchorMatch) {
151
- const anchorText = anchorMatch[1];
202
+ const anchorText = anchorMatch[1] ?? '';
152
203
  // Output markers with PARENT's index around the anchor text
153
204
  const parentIdx = c.parentIdx;
154
205
  const replacement = `${MARKER_START_PREFIX}${parentIdx}${MARKER_SUFFIX}${anchorText}${MARKER_END_PREFIX}${parentIdx}${MARKER_SUFFIX}`;
@@ -164,7 +215,8 @@ export function prepareMarkdownWithMarkers(markdown) {
164
215
  if (c.anchorFromReply) {
165
216
  // Anchor markers are placed by the reply, just remove this comment
166
217
  let removeStart = c.start;
167
- while (removeStart > 0 && /\s/.test(markedMarkdown[removeStart - 1])) {
218
+ const charBefore = markedMarkdown[removeStart - 1];
219
+ while (removeStart > 0 && charBefore && /\s/.test(charBefore)) {
168
220
  removeStart--;
169
221
  }
170
222
  markedMarkdown = markedMarkdown.slice(0, removeStart) + markedMarkdown.slice(c.end);
@@ -180,7 +232,7 @@ export function prepareMarkdownWithMarkers(markdown) {
180
232
  return { markedMarkdown, comments };
181
233
  }
182
234
 
183
- function createCommentsXml(comments) {
235
+ function createCommentsXml(comments: CommentWithIds[]): string {
184
236
  // Word expects date without milliseconds: 2025-12-30T08:33:00Z
185
237
  const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
186
238
 
@@ -209,7 +261,7 @@ function createCommentsXml(comments) {
209
261
  return xml;
210
262
  }
211
263
 
212
- function createCommentsExtendedXml(comments) {
264
+ function createCommentsExtendedXml(comments: CommentWithIds[]): string {
213
265
  let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
214
266
  // Minimal namespaces matching golden file structure
215
267
  xml += '<w15:commentsEx xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" mc:Ignorable="w14 w15">';
@@ -228,7 +280,7 @@ function createCommentsExtendedXml(comments) {
228
280
  return xml;
229
281
  }
230
282
 
231
- function generateDurableId(index) {
283
+ function generateDurableId(index: number): string {
232
284
  // Generate unique 8-char hex ID for durableId
233
285
  // CRITICAL: Must stay within signed 32-bit range (< 0x7FFFFFFF = 2147483647)
234
286
  // Word interprets durableIds as signed 32-bit integers
@@ -237,7 +289,7 @@ function generateDurableId(index) {
237
289
  return id.toString(16).toUpperCase().padStart(8, '0');
238
290
  }
239
291
 
240
- function createCommentsIdsXml(comments) {
292
+ function createCommentsIdsXml(comments: CommentWithIds[]): string {
241
293
  let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
242
294
  // Minimal namespaces matching golden file structure
243
295
  xml += '<w16cid:commentsIds ';
@@ -257,7 +309,7 @@ function createCommentsIdsXml(comments) {
257
309
  return xml;
258
310
  }
259
311
 
260
- function createCommentsExtensibleXml(comments) {
312
+ function createCommentsExtensibleXml(comments: CommentWithIds[]): string {
261
313
  const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
262
314
 
263
315
  let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
@@ -278,7 +330,7 @@ function createCommentsExtensibleXml(comments) {
278
330
 
279
331
  // Generate deterministic user IDs for authors (no hardcoded personal data)
280
332
 
281
- function createPeopleXml(comments) {
333
+ function createPeopleXml(comments: CommentWithIds[]): string {
282
334
  // Extract unique authors
283
335
  const authors = [...new Set(comments.map(c => c.author))];
284
336
 
@@ -308,7 +360,7 @@ function createPeopleXml(comments) {
308
360
  return xml;
309
361
  }
310
362
 
311
- function generateUserId(author) {
363
+ function generateUserId(author: string): string {
312
364
  // Generate a deterministic 16-char hex ID from author name
313
365
  let hash = 0;
314
366
  for (let i = 0; i < author.length; i++) {
@@ -321,7 +373,11 @@ function generateUserId(author) {
321
373
  /**
322
374
  * Inject comments at marker positions
323
375
  */
324
- export async function injectCommentsAtMarkers(docxPath, comments, outputPath) {
376
+ export async function injectCommentsAtMarkers(
377
+ docxPath: string,
378
+ comments: PreparedComment[],
379
+ outputPath: string
380
+ ): Promise<InjectionResult> {
325
381
  try {
326
382
  if (!fs.existsSync(docxPath)) {
327
383
  return { success: false, commentCount: 0, skippedComments: 0, error: `File not found: ${docxPath}` };
@@ -341,7 +397,7 @@ export async function injectCommentsAtMarkers(docxPath, comments, outputPath) {
341
397
  let documentXml = zip.readAsText(documentEntry);
342
398
 
343
399
  // Assign IDs and paraIds (IDs start at 1, not 0 - Word convention)
344
- const commentsWithIds = comments.map((c, idx) => ({
400
+ const commentsWithIds: CommentWithIds[] = comments.map((c, idx) => ({
345
401
  ...c,
346
402
  id: String(idx + 1),
347
403
  paraId: generateParaId(idx, 1), // First paragraph (e.g., 10000001)
@@ -352,17 +408,21 @@ export async function injectCommentsAtMarkers(docxPath, comments, outputPath) {
352
408
  // Link replies to parent paraIds
353
409
  for (const c of commentsWithIds) {
354
410
  if (c.isReply && c.parentIdx !== null) {
355
- c.parentParaId = commentsWithIds[c.parentIdx].paraId;
411
+ const parent = commentsWithIds[c.parentIdx];
412
+ if (parent) {
413
+ c.parentParaId = parent.paraId;
414
+ }
356
415
  }
357
416
  }
358
417
 
359
- const injectedIds = new Set();
418
+ const injectedIds = new Set<string>();
360
419
 
361
420
  // Process only parent comments (non-replies) for document ranges
362
421
  const parentComments = commentsWithIds.filter(c => !c.isReply);
363
422
 
364
423
  for (let i = parentComments.length - 1; i >= 0; i--) {
365
424
  const comment = parentComments[i];
425
+ if (!comment) continue;
366
426
  const idx = comment.commentIdx;
367
427
 
368
428
  const startMarker = `${MARKER_START_PREFIX}${idx}${MARKER_SUFFIX}`;
@@ -392,8 +452,10 @@ export async function injectCommentsAtMarkers(docxPath, comments, outputPath) {
392
452
  const textMatch = runContent.match(/<w:t[^>]*>([\s\S]*?)<\/w:t>/);
393
453
  if (!textMatch) continue;
394
454
 
395
- const fullText = textMatch[1];
396
- const tElement = textMatch[0].match(/<w:t[^>]*>/)[0];
455
+ const fullText = textMatch[1] ?? '';
456
+ const tElementMatch = textMatch[0].match(/<w:t[^>]*>/);
457
+ if (!tElementMatch) continue;
458
+ const tElement = tElementMatch[0];
397
459
 
398
460
  const startInText = fullText.indexOf(startMarker);
399
461
  const endInText = fullText.indexOf(endMarker);
@@ -407,7 +469,7 @@ export async function injectCommentsAtMarkers(docxPath, comments, outputPath) {
407
469
  if (!anchorText && textAfter) {
408
470
  const wordMatch = textAfter.match(/^\s*(\S+)/);
409
471
  if (wordMatch) {
410
- anchorText = wordMatch[1];
472
+ anchorText = wordMatch[1] ?? '';
411
473
  textAfter = textAfter.slice(wordMatch[0].length);
412
474
  }
413
475
  }
@@ -425,7 +487,7 @@ export async function injectCommentsAtMarkers(docxPath, comments, outputPath) {
425
487
  }
426
488
 
427
489
  // Find replies to this comment
428
- const replies = commentsWithIds.filter(c => c.isReply && c.parentIdx === comment.commentIdx);
490
+ const replies = commentsWithIds.filter(c => c.isReply && c.parentIdx === comment?.commentIdx);
429
491
 
430
492
  // Start ranges for parent AND all replies (nested)
431
493
  replacement += `<w:commentRangeStart w:id="${comment.id}"/>`;
@@ -458,7 +520,7 @@ export async function injectCommentsAtMarkers(docxPath, comments, outputPath) {
458
520
  }
459
521
 
460
522
  // Add required namespaces to document.xml for comment threading
461
- const requiredNs = {
523
+ const requiredNs: Record<string, string> = {
462
524
  'xmlns:w14': 'http://schemas.microsoft.com/office/word/2010/wordml',
463
525
  'xmlns:w15': 'http://schemas.microsoft.com/office/word/2012/wordml',
464
526
  'xmlns:w16cid': 'http://schemas.microsoft.com/office/word/2016/wordml/cid',
@@ -494,7 +556,8 @@ export async function injectCommentsAtMarkers(docxPath, comments, outputPath) {
494
556
  return injectedIds.has(c.id);
495
557
  } else {
496
558
  // Include reply if its parent was injected
497
- return c.parentIdx !== null && injectedIds.has(commentsWithIds[c.parentIdx].id);
559
+ const parent = c.parentIdx !== null ? commentsWithIds[c.parentIdx] : undefined;
560
+ return parent && injectedIds.has(parent.id);
498
561
  }
499
562
  });
500
563
 
@@ -639,16 +702,7 @@ export async function injectCommentsAtMarkers(docxPath, comments, outputPath) {
639
702
  skippedComments: comments.length - includedComments.length,
640
703
  };
641
704
 
642
- } catch (err) {
705
+ } catch (err: any) {
643
706
  return { success: false, commentCount: 0, skippedComments: 0, error: err.message };
644
707
  }
645
708
  }
646
-
647
- export async function injectComments(docxPath, markdown, outputPath) {
648
- console.warn('Warning: Use prepareMarkdownWithMarkers + injectCommentsAtMarkers instead');
649
- return { success: false, commentCount: 0, skippedComments: 0, error: 'Use marker-based flow' };
650
- }
651
-
652
- export async function buildWithComments(cleanDocxPath, comments, outputPath) {
653
- return injectCommentsAtMarkers(cleanDocxPath, comments, outputPath);
654
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docrev",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Academic paper revision workflow: Word ↔ Markdown round-trips, DOI validation, reviewer comments",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -81,10 +81,15 @@
81
81
  "rev": "bin/rev.js"
82
82
  },
83
83
  "scripts": {
84
- "build": "echo 'No build needed'",
85
- "test": "node --test test/*.test.js",
84
+ "build": "tsc",
85
+ "build:watch": "tsc --watch",
86
+ "dev": "tsx bin/rev.ts",
87
+ "test": "tsx --test test/*.test.js",
88
+ "test:ts": "tsx --test test/*.test.ts",
86
89
  "test:watch": "node --test --watch test/*.test.js",
87
- "test:coverage": "c8 --reporter=text --reporter=lcov node --test test/*.test.js"
90
+ "test:coverage": "c8 --reporter=text --reporter=lcov node --test test/*.test.js",
91
+ "typecheck": "tsc --noEmit",
92
+ "prepublishOnly": "npm run build"
88
93
  },
89
94
  "repository": {
90
95
  "type": "git",
@@ -119,10 +124,15 @@
119
124
  "nspell": "^2.1.5",
120
125
  "pdf-lib": "^1.17.1",
121
126
  "pdfjs-dist": "^5.4.530",
127
+ "tsx": "^4.21.0",
122
128
  "xml2js": "^0.6.2",
123
129
  "yaml": "^2.8.2"
124
130
  },
125
131
  "devDependencies": {
126
- "c8": "^10.1.2"
132
+ "@types/adm-zip": "^0.5.7",
133
+ "@types/node": "^25.2.0",
134
+ "@types/xml2js": "^0.4.14",
135
+ "c8": "^10.1.2",
136
+ "typescript": "^5.9.3"
127
137
  }
128
138
  }
@@ -313,6 +313,73 @@ rev config user "Your Name" # Set author name for replies
313
313
  rev config # Show current config
314
314
  ```
315
315
 
316
+ ## rev.yaml Settings
317
+
318
+ ### Tables
319
+
320
+ Configure table formatting for PDF output:
321
+
322
+ ```yaml
323
+ tables:
324
+ nowrap:
325
+ - Prior # Column headers to keep on one line
326
+ - "$\\widehat{R}$"
327
+ ```
328
+
329
+ In `nowrap` columns, distribution notation is auto-converted to LaTeX math:
330
+ - `Normal(0, 0.5)` → `$\mathcal{N}(0, 0.5)$`
331
+ - `Student-t(3, 0, 1)` → `$t_3(0, 1)$`
332
+ - `Gamma(2, 0.5)` → `$\text{Gamma}(2, 0.5)$`
333
+ - `Half-Normal(0, 1)` → `$\text{Half-Normal}(0, 1)$`
334
+ - `Exponential(1)` → `$\text{Exp}(1)$`
335
+
336
+ **Tip:** For complex tables, use simple tables (space-aligned with dashes) instead of pipe tables to avoid column width issues:
337
+
338
+ ```markdown
339
+ Parameter Prior Description
340
+ ---------- ------------------------- ------------------
341
+ alpha $\mathcal{N}(0, 0.5)$ Intercept prior
342
+ beta $t_3(0, 2.5)$ Slope prior
343
+ ```
344
+
345
+ ### Postprocess Scripts
346
+
347
+ Run custom scripts after output generation:
348
+
349
+ ```yaml
350
+ postprocess:
351
+ pdf: ./scripts/fix-pdf.py # Runs after PDF
352
+ docx: ./scripts/add-meta.js # Runs after DOCX
353
+ tex: ./scripts/tweak-latex.sh # Runs after TEX
354
+ pptx: ./scripts/fix-slides.ps1 # Runs after PPTX
355
+ all: ./scripts/notify.js # Runs after ANY format
356
+ ```
357
+
358
+ **Environment variables** available to scripts:
359
+
360
+ | Variable | Description |
361
+ |----------|-------------|
362
+ | `OUTPUT_FILE` | Full path to generated file |
363
+ | `OUTPUT_FORMAT` | Format: `pdf`, `docx`, `tex`, `pptx`, `beamer` |
364
+ | `PROJECT_DIR` | Directory containing rev.yaml |
365
+ | `CONFIG_PATH` | Full path to rev.yaml |
366
+
367
+ **Supported script types:** `.js`, `.mjs` (Node), `.py` (Python), `.ps1` (PowerShell), `.sh` (Bash)
368
+
369
+ **Example Python script** (add DOCX metadata):
370
+ ```python
371
+ import os
372
+ from docx import Document
373
+ doc = Document(os.environ['OUTPUT_FILE'])
374
+ doc.core_properties.author = "Research Team"
375
+ doc.save(os.environ['OUTPUT_FILE'])
376
+ ```
377
+
378
+ Use `--verbose` flag to see script output:
379
+ ```bash
380
+ rev build pdf --verbose
381
+ ```
382
+
316
383
  ## Shell Completions
317
384
 
318
385
  ### rev completions
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": ".",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "noImplicitAny": true,
14
+ "strictNullChecks": true,
15
+ "noImplicitReturns": true,
16
+ "noUncheckedIndexedAccess": false,
17
+ "esModuleInterop": true,
18
+ "skipLibCheck": true,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "resolveJsonModule": true,
21
+ "allowJs": true,
22
+ "checkJs": false
23
+ },
24
+ "include": ["lib/**/*", "bin/**/*"],
25
+ "exclude": ["node_modules", "dist", "test"]
26
+ }