docrev 0.8.0 → 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 +42 -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
@@ -0,0 +1,850 @@
1
+ # Implementation Plan: Table Formatting & Postprocess Scripting
2
+
3
+ ## Overview
4
+
5
+ Two features to implement:
6
+ 1. **Table formatting config** - Make `tables:` config in rev.yaml actually work
7
+ 2. **Postprocess scripting** - Allow users to run custom transformations on output
8
+
9
+ ---
10
+
11
+ ## Part 1: Table Formatting
12
+
13
+ ### Problem Statement
14
+
15
+ Pandoc's longtable with proportional `p{}` column widths forces text wrapping. Users need:
16
+ - Columns that don't wrap (e.g., `N(0, 0.5)` should stay on one line)
17
+ - Custom alignment per column
18
+ - Math notation conversion (Normal → 𝒩)
19
+
20
+ ### Why Previous Attempt Failed
21
+
22
+ The Lua filter approach failed because:
23
+ 1. Pandoc calculates column widths from markdown before the filter runs
24
+ 2. Setting `ColWidthDefault` in Lua results in `0.0000` width, not auto-width
25
+ 3. `\mbox{}` in a `p{}` column overflows instead of expanding
26
+
27
+ ### Solution: LaTeX Header Injection
28
+
29
+ Instead of a Lua filter, inject LaTeX packages/commands via `header-includes`.
30
+
31
+ #### Implementation Steps
32
+
33
+ **Step 1: Add `pdf.header-includes` config option**
34
+
35
+ File: `lib/build.js`
36
+
37
+ ```javascript
38
+ // In DEFAULT_CONFIG.pdf:
39
+ pdf: {
40
+ template: null,
41
+ documentclass: 'article',
42
+ fontsize: '12pt',
43
+ geometry: 'margin=1in',
44
+ linestretch: 1.5,
45
+ numbersections: false,
46
+ toc: false,
47
+ headerIncludes: null, // NEW: string or array of LaTeX code
48
+ },
49
+ ```
50
+
51
+ **Step 2: Pass header-includes to pandoc**
52
+
53
+ File: `lib/build.js`, in `buildPandocArgs()`:
54
+
55
+ ```javascript
56
+ if (format === 'pdf') {
57
+ // ... existing code ...
58
+
59
+ // Header includes (LaTeX preamble additions)
60
+ if (config.pdf.headerIncludes) {
61
+ const includes = Array.isArray(config.pdf.headerIncludes)
62
+ ? config.pdf.headerIncludes
63
+ : [config.pdf.headerIncludes];
64
+ for (const inc of includes) {
65
+ args.push('-V', `header-includes=${inc}`);
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ **Step 3: Create table-focused presets**
72
+
73
+ File: `lib/build.js`, new function:
74
+
75
+ ```javascript
76
+ /**
77
+ * Generate LaTeX header-includes for table configuration
78
+ * @param {object} tablesConfig
79
+ * @returns {string[]} LaTeX code lines
80
+ */
81
+ function generateTableLatex(tablesConfig) {
82
+ const lines = [];
83
+
84
+ if (!tablesConfig) return lines;
85
+
86
+ // Always include array package for column type customization
87
+ lines.push('\\usepackage{array}');
88
+
89
+ // Add nowrap column type: use with N{width} in manual tables
90
+ // This creates a column that doesn't wrap but respects minipage
91
+ if (tablesConfig.nowrap) {
92
+ lines.push('% Nowrap column type for tables');
93
+ lines.push('\\newcolumntype{N}[1]{>{\\raggedright\\arraybackslash}p{#1}}');
94
+ }
95
+
96
+ // Small tables
97
+ if (tablesConfig.small) {
98
+ lines.push('% Apply small font to longtable environment');
99
+ lines.push('\\AtBeginEnvironment{longtable}{\\small}');
100
+ lines.push('\\usepackage{etoolbox}'); // for AtBeginEnvironment
101
+ }
102
+
103
+ return lines;
104
+ }
105
+ ```
106
+
107
+ **Step 4: Integrate into build pipeline**
108
+
109
+ File: `lib/build.js`, in `buildPandocArgs()`:
110
+
111
+ ```javascript
112
+ if (format === 'pdf' || format === 'tex') {
113
+ // Generate table-specific LaTeX if tables config exists
114
+ const tableLatex = generateTableLatex(config.tables);
115
+ if (tableLatex.length > 0) {
116
+ for (const line of tableLatex) {
117
+ args.push('-V', `header-includes=${line}`);
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ **Step 5: Add markdown preprocessing for nowrap columns**
124
+
125
+ Since we can't change pandoc's column width calculation, we preprocess the markdown to wrap nowrap column content in `\mbox{}` directly.
126
+
127
+ File: `lib/build.js`, new function:
128
+
129
+ ```javascript
130
+ /**
131
+ * Process markdown tables to apply nowrap to specified columns
132
+ * Wraps cell content in \mbox{} for LaTeX output
133
+ * @param {string} content - Markdown content
134
+ * @param {object} tablesConfig - tables config from rev.yaml
135
+ * @param {string} format - output format
136
+ * @returns {string} processed content
137
+ */
138
+ function processTablesForFormat(content, tablesConfig, format) {
139
+ if (!tablesConfig?.nowrap?.length || format !== 'pdf') {
140
+ return content;
141
+ }
142
+
143
+ const nowrapPatterns = tablesConfig.nowrap.map(p => p.toLowerCase());
144
+
145
+ // Match pipe tables
146
+ const tableRegex = /(\|[^\n]+\|\n\|[-:| ]+\|\n)((?:\|[^\n]+\|\n)+)/g;
147
+
148
+ return content.replace(tableRegex, (match, header, body) => {
149
+ // Parse header to find nowrap column indices
150
+ const headerCells = header.split('|').slice(1, -1).map(c => c.trim().toLowerCase());
151
+ const nowrapCols = headerCells.map((cell, i) =>
152
+ nowrapPatterns.some(p => cell.includes(p)) ? i : -1
153
+ ).filter(i => i >= 0);
154
+
155
+ if (nowrapCols.length === 0) return match;
156
+
157
+ // Process body rows
158
+ const processedBody = body.split('\n').filter(l => l.trim()).map(row => {
159
+ const cells = row.split('|').slice(1, -1);
160
+ nowrapCols.forEach(colIdx => {
161
+ if (cells[colIdx]) {
162
+ const content = cells[colIdx].trim();
163
+ // Skip if already has LaTeX or is empty
164
+ if (content && !content.startsWith('\\') && !content.startsWith('$')) {
165
+ // Convert distribution notation to math
166
+ let processed = content
167
+ .replace(/Normal\(([^)]+)\)/g, '$\\mathcal{N}($1)$')
168
+ .replace(/Student-t\((\d+),\s*([^)]+)\)/g, '$t_{$1}($2)$')
169
+ .replace(/Gamma\(([^)]+)\)/g, '$\\text{Gamma}($1)$');
170
+ cells[colIdx] = ` ${processed} `;
171
+ }
172
+ }
173
+ });
174
+ return '|' + cells.join('|') + '|';
175
+ }).join('\n');
176
+
177
+ return header + processedBody + '\n';
178
+ });
179
+ }
180
+ ```
181
+
182
+ **Step 6: Call from prepareForFormat**
183
+
184
+ ```javascript
185
+ export function prepareForFormat(paperPath, format, config, options = {}) {
186
+ // ... existing code ...
187
+
188
+ if (format === 'pdf' || format === 'tex') {
189
+ content = stripAnnotations(content);
190
+ // NEW: Process tables for nowrap columns
191
+ content = processTablesForFormat(content, config.tables, format);
192
+ }
193
+
194
+ // ... rest of function ...
195
+ }
196
+ ```
197
+
198
+ #### Test Plan for Tables
199
+
200
+ File: `test/tables.test.js`
201
+
202
+ ```javascript
203
+ import { describe, it, beforeEach, afterEach } from 'node:test';
204
+ import assert from 'node:assert';
205
+ import * as fs from 'fs';
206
+ import * as path from 'path';
207
+ import * as os from 'os';
208
+ import { processTablesForFormat, generateTableLatex } from '../lib/build.js';
209
+
210
+ describe('Table Processing', () => {
211
+ describe('generateTableLatex', () => {
212
+ it('returns empty array with no config', () => {
213
+ assert.deepStrictEqual(generateTableLatex(null), []);
214
+ assert.deepStrictEqual(generateTableLatex({}), []);
215
+ });
216
+
217
+ it('adds array package when nowrap specified', () => {
218
+ const result = generateTableLatex({ nowrap: ['Prior'] });
219
+ assert.ok(result.includes('\\usepackage{array}'));
220
+ });
221
+
222
+ it('adds small table styling when small=true', () => {
223
+ const result = generateTableLatex({ small: true });
224
+ assert.ok(result.some(l => l.includes('\\small')));
225
+ assert.ok(result.some(l => l.includes('etoolbox')));
226
+ });
227
+ });
228
+
229
+ describe('processTablesForFormat', () => {
230
+ const sampleTable = `| Component | Prior | Justification |
231
+ |:----------|:------|:--------------|
232
+ | Intercept | Normal(1.5, 0.5) | Weak prior |
233
+ | Slope | Normal(0, 0.3) | Centered |`;
234
+
235
+ it('returns unchanged for non-pdf format', () => {
236
+ const config = { nowrap: ['Prior'] };
237
+ const result = processTablesForFormat(sampleTable, config, 'docx');
238
+ assert.strictEqual(result, sampleTable);
239
+ });
240
+
241
+ it('returns unchanged with no nowrap config', () => {
242
+ const result = processTablesForFormat(sampleTable, {}, 'pdf');
243
+ assert.strictEqual(result, sampleTable);
244
+ });
245
+
246
+ it('converts Normal() to mathcal N in nowrap columns', () => {
247
+ const config = { nowrap: ['Prior'] };
248
+ const result = processTablesForFormat(sampleTable, config, 'pdf');
249
+ assert.ok(result.includes('$\\mathcal{N}(1.5, 0.5)$'));
250
+ assert.ok(result.includes('$\\mathcal{N}(0, 0.3)$'));
251
+ });
252
+
253
+ it('converts Student-t() to subscript notation', () => {
254
+ const table = `| Param | Prior |
255
+ |-------|-------|
256
+ | SD | Student-t(3, 0, 2.5) |`;
257
+ const config = { nowrap: ['Prior'] };
258
+ const result = processTablesForFormat(table, config, 'pdf');
259
+ assert.ok(result.includes('$t_{3}(0, 2.5)$'));
260
+ });
261
+
262
+ it('does not modify columns not in nowrap list', () => {
263
+ const config = { nowrap: ['Prior'] };
264
+ const result = processTablesForFormat(sampleTable, config, 'pdf');
265
+ assert.ok(result.includes('Weak prior')); // unchanged
266
+ assert.ok(!result.includes('$Weak prior$'));
267
+ });
268
+
269
+ it('handles case-insensitive column matching', () => {
270
+ const config = { nowrap: ['PRIOR'] };
271
+ const result = processTablesForFormat(sampleTable, config, 'pdf');
272
+ assert.ok(result.includes('$\\mathcal{N}'));
273
+ });
274
+
275
+ it('skips cells that already have math', () => {
276
+ const table = `| Param | Prior |
277
+ |-------|-------|
278
+ | X | $\\mathcal{N}(0, 1)$ |`;
279
+ const config = { nowrap: ['Prior'] };
280
+ const result = processTablesForFormat(table, config, 'pdf');
281
+ // Should not double-wrap
282
+ assert.ok(!result.includes('$$'));
283
+ });
284
+ });
285
+ });
286
+ ```
287
+
288
+ #### Usage Example
289
+
290
+ ```yaml
291
+ # rev.yaml
292
+ tables:
293
+ nowrap:
294
+ - Prior
295
+ - "$\\widehat{R}$"
296
+ small: false
297
+ ```
298
+
299
+ ```markdown
300
+ | Parameter | Prior | Justification |
301
+ |:----------|:------|:--------------|
302
+ | Intercept | Normal(1.5, 0.5) | Prior P ~82% |
303
+ | Slope | Normal(0, 0.5) | Moderate |
304
+ ```
305
+
306
+ Output: Prior column cells become `$\mathcal{N}(1.5, 0.5)$` in PDF.
307
+
308
+ ---
309
+
310
+ ## Part 2: Postprocess Scripting
311
+
312
+ ### Problem Statement
313
+
314
+ Users need fine-grained control over output that pandoc/docrev can't provide:
315
+ - Custom LaTeX tweaks after generation
316
+ - Search/replace in generated files
317
+ - Format-specific post-processing (e.g., inject custom XML into DOCX)
318
+
319
+ ### Design Principles
320
+
321
+ 1. **Start simple** - Shell scripts first, DSL later if needed
322
+ 2. **Per-format** - Different postprocess for PDF vs DOCX
323
+ 3. **Safe defaults** - Scripts must be explicitly enabled
324
+ 4. **Debugging** - Clear error messages, optional verbose mode
325
+
326
+ ### Implementation Approach
327
+
328
+ #### Phase 1: Shell Script Postprocessing (MVP)
329
+
330
+ **Config Schema:**
331
+
332
+ ```yaml
333
+ # rev.yaml
334
+ postprocess:
335
+ pdf: ./scripts/fix-tables.sh # Run after PDF generated
336
+ docx: ./scripts/add-headers.ps1 # Run after DOCX generated
337
+ all: ./scripts/common.sh # Run after any format
338
+ ```
339
+
340
+ **Implementation Steps:**
341
+
342
+ **Step 1: Add postprocess to DEFAULT_CONFIG**
343
+
344
+ File: `lib/build.js`
345
+
346
+ ```javascript
347
+ export const DEFAULT_CONFIG = {
348
+ // ... existing ...
349
+ postprocess: {
350
+ pdf: null,
351
+ docx: null,
352
+ tex: null,
353
+ pptx: null,
354
+ beamer: null,
355
+ all: null,
356
+ },
357
+ };
358
+ ```
359
+
360
+ **Step 2: Add postprocess runner**
361
+
362
+ File: `lib/postprocess.js` (new file)
363
+
364
+ ```javascript
365
+ import * as fs from 'fs';
366
+ import * as path from 'path';
367
+ import { execSync, spawn } from 'child_process';
368
+
369
+ /**
370
+ * Run postprocess script for a given format
371
+ * @param {string} outputPath - Path to generated file
372
+ * @param {string} format - Output format (pdf, docx, etc.)
373
+ * @param {object} config - Full config object
374
+ * @param {object} options - { verbose: boolean }
375
+ * @returns {Promise<{success: boolean, error?: string}>}
376
+ */
377
+ export async function runPostprocess(outputPath, format, config, options = {}) {
378
+ const postprocessConfig = config.postprocess || {};
379
+
380
+ // Collect scripts to run (format-specific + all)
381
+ const scripts = [];
382
+ if (postprocessConfig[format]) {
383
+ scripts.push(postprocessConfig[format]);
384
+ }
385
+ if (postprocessConfig.all) {
386
+ scripts.push(postprocessConfig.all);
387
+ }
388
+
389
+ if (scripts.length === 0) {
390
+ return { success: true };
391
+ }
392
+
393
+ const directory = path.dirname(outputPath);
394
+ const errors = [];
395
+
396
+ for (const scriptPath of scripts) {
397
+ const absoluteScript = path.isAbsolute(scriptPath)
398
+ ? scriptPath
399
+ : path.join(directory, scriptPath);
400
+
401
+ if (!fs.existsSync(absoluteScript)) {
402
+ errors.push(`Postprocess script not found: ${scriptPath}`);
403
+ continue;
404
+ }
405
+
406
+ try {
407
+ const result = await executeScript(absoluteScript, {
408
+ OUTPUT_FILE: outputPath,
409
+ OUTPUT_FORMAT: format,
410
+ PROJECT_DIR: directory,
411
+ CONFIG_PATH: config._configPath || '',
412
+ }, options);
413
+
414
+ if (!result.success) {
415
+ errors.push(`Script ${scriptPath} failed: ${result.error}`);
416
+ }
417
+ } catch (err) {
418
+ errors.push(`Script ${scriptPath} error: ${err.message}`);
419
+ }
420
+ }
421
+
422
+ return {
423
+ success: errors.length === 0,
424
+ error: errors.join('\n'),
425
+ };
426
+ }
427
+
428
+ /**
429
+ * Execute a script with environment variables
430
+ * @param {string} scriptPath
431
+ * @param {object} env - Environment variables to set
432
+ * @param {object} options
433
+ * @returns {Promise<{success: boolean, stdout: string, stderr: string, error?: string}>}
434
+ */
435
+ async function executeScript(scriptPath, env, options = {}) {
436
+ return new Promise((resolve) => {
437
+ const ext = path.extname(scriptPath).toLowerCase();
438
+ let command, args;
439
+
440
+ // Determine how to run based on extension
441
+ if (ext === '.ps1') {
442
+ command = 'powershell';
443
+ args = ['-ExecutionPolicy', 'Bypass', '-File', scriptPath];
444
+ } else if (ext === '.py') {
445
+ command = 'python';
446
+ args = [scriptPath];
447
+ } else if (ext === '.js') {
448
+ command = 'node';
449
+ args = [scriptPath];
450
+ } else {
451
+ // Assume shell script
452
+ command = process.platform === 'win32' ? 'bash' : '/bin/bash';
453
+ args = [scriptPath];
454
+ }
455
+
456
+ const proc = spawn(command, args, {
457
+ env: { ...process.env, ...env },
458
+ cwd: path.dirname(scriptPath),
459
+ stdio: ['ignore', 'pipe', 'pipe'],
460
+ });
461
+
462
+ let stdout = '';
463
+ let stderr = '';
464
+
465
+ proc.stdout.on('data', (data) => {
466
+ stdout += data.toString();
467
+ if (options.verbose) {
468
+ process.stdout.write(data);
469
+ }
470
+ });
471
+
472
+ proc.stderr.on('data', (data) => {
473
+ stderr += data.toString();
474
+ if (options.verbose) {
475
+ process.stderr.write(data);
476
+ }
477
+ });
478
+
479
+ proc.on('error', (err) => {
480
+ resolve({ success: false, stdout, stderr, error: err.message });
481
+ });
482
+
483
+ proc.on('close', (code) => {
484
+ if (code === 0) {
485
+ resolve({ success: true, stdout, stderr });
486
+ } else {
487
+ resolve({
488
+ success: false,
489
+ stdout,
490
+ stderr,
491
+ error: `Exit code ${code}: ${stderr.trim() || 'Unknown error'}`
492
+ });
493
+ }
494
+ });
495
+ });
496
+ }
497
+
498
+ export { executeScript };
499
+ ```
500
+
501
+ **Step 3: Integrate into runPandoc**
502
+
503
+ File: `lib/build.js`
504
+
505
+ ```javascript
506
+ import { runPostprocess } from './postprocess.js';
507
+
508
+ // In runPandoc(), after pandoc completes successfully:
509
+
510
+ pandoc.on('close', async (code) => {
511
+ if (code === 0) {
512
+ // Existing PPTX post-processing...
513
+ if (format === 'pptx') {
514
+ // ...
515
+ }
516
+
517
+ // NEW: Run user postprocess scripts
518
+ const postResult = await runPostprocess(outputPath, format, config, options);
519
+ if (!postResult.success) {
520
+ console.error(`Postprocess warning: ${postResult.error}`);
521
+ }
522
+
523
+ resolve({ outputPath, success: true });
524
+ } else {
525
+ resolve({ outputPath: null, success: false, error: stderr });
526
+ }
527
+ });
528
+ ```
529
+
530
+ **Step 4: Add CLI verbose flag**
531
+
532
+ File: `lib/commands/build.js`
533
+
534
+ ```javascript
535
+ .option('--verbose', 'Show detailed output including postprocess scripts')
536
+
537
+ // Pass to build():
538
+ await build(targetDir, formats, { verbose: options.verbose });
539
+ ```
540
+
541
+ #### Phase 2: DSL for Common Operations (Future)
542
+
543
+ If shell scripts prove insufficient, add a simple declarative DSL:
544
+
545
+ ```yaml
546
+ # rev.yaml
547
+ postprocess:
548
+ pdf:
549
+ - type: replace
550
+ pattern: "\\\\begin{longtable}"
551
+ replacement: "\\\\begin{longtable}[l]"
552
+ - type: inject
553
+ after: "\\\\begin{document}"
554
+ content: "\\\\newcommand{\\\\N}{\\\\mathcal{N}}"
555
+ - type: script
556
+ path: ./scripts/final-fixes.sh
557
+ ```
558
+
559
+ This would require:
560
+ - New file: `lib/postprocess-dsl.js`
561
+ - Operation handlers for each type
562
+ - Validation of DSL syntax
563
+ - Clear error messages for invalid operations
564
+
565
+ #### Test Plan for Postprocessing
566
+
567
+ File: `test/postprocess.test.js`
568
+
569
+ ```javascript
570
+ import { describe, it, beforeEach, afterEach } from 'node:test';
571
+ import assert from 'node:assert';
572
+ import * as fs from 'fs';
573
+ import * as path from 'path';
574
+ import * as os from 'os';
575
+ import { runPostprocess, executeScript } from '../lib/postprocess.js';
576
+
577
+ describe('Postprocessing', () => {
578
+ let tempDir;
579
+
580
+ beforeEach(() => {
581
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docrev-test-'));
582
+ });
583
+
584
+ afterEach(() => {
585
+ fs.rmSync(tempDir, { recursive: true, force: true });
586
+ });
587
+
588
+ describe('executeScript', () => {
589
+ it('runs shell script with environment variables', async () => {
590
+ const scriptPath = path.join(tempDir, 'test.sh');
591
+ fs.writeFileSync(scriptPath, '#!/bin/bash\necho "$OUTPUT_FILE"', { mode: 0o755 });
592
+
593
+ const result = await executeScript(scriptPath, { OUTPUT_FILE: '/tmp/test.pdf' });
594
+ assert.ok(result.success);
595
+ assert.ok(result.stdout.includes('/tmp/test.pdf'));
596
+ });
597
+
598
+ it('returns error for non-existent script', async () => {
599
+ const result = await executeScript('/nonexistent/script.sh', {});
600
+ assert.ok(!result.success);
601
+ });
602
+
603
+ it('captures exit code on failure', async () => {
604
+ const scriptPath = path.join(tempDir, 'fail.sh');
605
+ fs.writeFileSync(scriptPath, '#!/bin/bash\nexit 1', { mode: 0o755 });
606
+
607
+ const result = await executeScript(scriptPath, {});
608
+ assert.ok(!result.success);
609
+ assert.ok(result.error.includes('Exit code 1'));
610
+ });
611
+
612
+ it('runs PowerShell scripts on Windows', async function() {
613
+ if (process.platform !== 'win32') {
614
+ this.skip();
615
+ return;
616
+ }
617
+
618
+ const scriptPath = path.join(tempDir, 'test.ps1');
619
+ fs.writeFileSync(scriptPath, 'Write-Host $env:OUTPUT_FILE');
620
+
621
+ const result = await executeScript(scriptPath, { OUTPUT_FILE: 'C:\\test.pdf' });
622
+ assert.ok(result.success);
623
+ assert.ok(result.stdout.includes('C:\\test.pdf'));
624
+ });
625
+
626
+ it('runs Python scripts', async () => {
627
+ const scriptPath = path.join(tempDir, 'test.py');
628
+ fs.writeFileSync(scriptPath, 'import os; print(os.environ["OUTPUT_FILE"])');
629
+
630
+ const result = await executeScript(scriptPath, { OUTPUT_FILE: '/tmp/test.pdf' });
631
+ assert.ok(result.success);
632
+ assert.ok(result.stdout.includes('/tmp/test.pdf'));
633
+ });
634
+
635
+ it('runs Node.js scripts', async () => {
636
+ const scriptPath = path.join(tempDir, 'test.js');
637
+ fs.writeFileSync(scriptPath, 'console.log(process.env.OUTPUT_FILE)');
638
+
639
+ const result = await executeScript(scriptPath, { OUTPUT_FILE: '/tmp/test.pdf' });
640
+ assert.ok(result.success);
641
+ assert.ok(result.stdout.includes('/tmp/test.pdf'));
642
+ });
643
+ });
644
+
645
+ describe('runPostprocess', () => {
646
+ it('returns success with no postprocess config', async () => {
647
+ const result = await runPostprocess('/tmp/test.pdf', 'pdf', {});
648
+ assert.ok(result.success);
649
+ });
650
+
651
+ it('runs format-specific script', async () => {
652
+ const scriptPath = path.join(tempDir, 'pdf-post.sh');
653
+ const markerPath = path.join(tempDir, 'marker.txt');
654
+ fs.writeFileSync(scriptPath, `#!/bin/bash\necho "ran" > "${markerPath}"`, { mode: 0o755 });
655
+
656
+ const config = {
657
+ postprocess: { pdf: scriptPath },
658
+ _configPath: path.join(tempDir, 'rev.yaml'),
659
+ };
660
+
661
+ const result = await runPostprocess(path.join(tempDir, 'out.pdf'), 'pdf', config);
662
+ assert.ok(result.success);
663
+ assert.ok(fs.existsSync(markerPath));
664
+ });
665
+
666
+ it('runs "all" script for any format', async () => {
667
+ const scriptPath = path.join(tempDir, 'all-post.sh');
668
+ const markerPath = path.join(tempDir, 'marker.txt');
669
+ fs.writeFileSync(scriptPath, `#!/bin/bash\necho "$OUTPUT_FORMAT" > "${markerPath}"`, { mode: 0o755 });
670
+
671
+ const config = {
672
+ postprocess: { all: scriptPath },
673
+ _configPath: path.join(tempDir, 'rev.yaml'),
674
+ };
675
+
676
+ await runPostprocess(path.join(tempDir, 'out.docx'), 'docx', config);
677
+ assert.ok(fs.existsSync(markerPath));
678
+ assert.strictEqual(fs.readFileSync(markerPath, 'utf-8').trim(), 'docx');
679
+ });
680
+
681
+ it('runs both format-specific and all scripts', async () => {
682
+ const pdfScript = path.join(tempDir, 'pdf.sh');
683
+ const allScript = path.join(tempDir, 'all.sh');
684
+ const pdfMarker = path.join(tempDir, 'pdf-marker.txt');
685
+ const allMarker = path.join(tempDir, 'all-marker.txt');
686
+
687
+ fs.writeFileSync(pdfScript, `#!/bin/bash\ntouch "${pdfMarker}"`, { mode: 0o755 });
688
+ fs.writeFileSync(allScript, `#!/bin/bash\ntouch "${allMarker}"`, { mode: 0o755 });
689
+
690
+ const config = {
691
+ postprocess: { pdf: pdfScript, all: allScript },
692
+ _configPath: path.join(tempDir, 'rev.yaml'),
693
+ };
694
+
695
+ await runPostprocess(path.join(tempDir, 'out.pdf'), 'pdf', config);
696
+ assert.ok(fs.existsSync(pdfMarker));
697
+ assert.ok(fs.existsSync(allMarker));
698
+ });
699
+
700
+ it('reports error for missing script', async () => {
701
+ const config = {
702
+ postprocess: { pdf: './nonexistent.sh' },
703
+ _configPath: path.join(tempDir, 'rev.yaml'),
704
+ };
705
+
706
+ const result = await runPostprocess(path.join(tempDir, 'out.pdf'), 'pdf', config);
707
+ assert.ok(!result.success);
708
+ assert.ok(result.error.includes('not found'));
709
+ });
710
+
711
+ it('reports error for failing script', async () => {
712
+ const scriptPath = path.join(tempDir, 'fail.sh');
713
+ fs.writeFileSync(scriptPath, '#!/bin/bash\nexit 42', { mode: 0o755 });
714
+
715
+ const config = {
716
+ postprocess: { pdf: scriptPath },
717
+ _configPath: path.join(tempDir, 'rev.yaml'),
718
+ };
719
+
720
+ const result = await runPostprocess(path.join(tempDir, 'out.pdf'), 'pdf', config);
721
+ assert.ok(!result.success);
722
+ assert.ok(result.error.includes('42') || result.error.includes('failed'));
723
+ });
724
+ });
725
+ });
726
+ ```
727
+
728
+ ---
729
+
730
+ ## Implementation Order
731
+
732
+ ### Sprint 1: Table Preprocessing (2-3 hours)
733
+
734
+ 1. [ ] Add `processTablesForFormat()` function to `lib/build.js`
735
+ 2. [ ] Integrate into `prepareForFormat()`
736
+ 3. [ ] Write tests in `test/tables.test.js`
737
+ 4. [ ] Test with paper 2 priors table
738
+ 5. [ ] Document in README
739
+
740
+ ### Sprint 2: Postprocess Shell Scripts (3-4 hours)
741
+
742
+ 1. [ ] Create `lib/postprocess.js` with `executeScript()` and `runPostprocess()`
743
+ 2. [ ] Add `postprocess` to `DEFAULT_CONFIG`
744
+ 3. [ ] Add config merging in `loadConfig()`
745
+ 4. [ ] Integrate into `runPandoc()` after output generation
746
+ 5. [ ] Add `--verbose` flag to CLI
747
+ 6. [ ] Write tests in `test/postprocess.test.js`
748
+ 7. [ ] Create example scripts in `examples/postprocess/`
749
+ 8. [ ] Document in README
750
+
751
+ ### Sprint 3: Header Includes (1-2 hours)
752
+
753
+ 1. [ ] Add `pdf.headerIncludes` config option
754
+ 2. [ ] Add `generateTableLatex()` helper
755
+ 3. [ ] Pass to pandoc in `buildPandocArgs()`
756
+ 4. [ ] Add tests
757
+ 5. [ ] Document
758
+
759
+ ### Future: DSL (if needed)
760
+
761
+ Only implement if shell scripts prove insufficient for common use cases.
762
+
763
+ ---
764
+
765
+ ## Files to Create/Modify
766
+
767
+ ### New Files
768
+
769
+ | File | Purpose |
770
+ |------|---------|
771
+ | `lib/postprocess.js` | Postprocess script execution |
772
+ | `test/tables.test.js` | Table processing tests |
773
+ | `test/postprocess.test.js` | Postprocess tests |
774
+ | `examples/postprocess/fix-tables.sh` | Example PDF postprocess |
775
+ | `examples/postprocess/inject-headers.ps1` | Example DOCX postprocess |
776
+
777
+ ### Modified Files
778
+
779
+ | File | Changes |
780
+ |------|---------|
781
+ | `lib/build.js` | Add `processTablesForFormat()`, `generateTableLatex()`, integrate postprocess, add configs |
782
+ | `lib/commands/build.js` | Add `--verbose` flag |
783
+
784
+ ---
785
+
786
+ ## Example Usage After Implementation
787
+
788
+ ### Table Config
789
+
790
+ ```yaml
791
+ # rev.yaml
792
+ tables:
793
+ nowrap:
794
+ - Prior
795
+ - Value
796
+ - Count
797
+ small: true
798
+ ```
799
+
800
+ ### Postprocess Scripts
801
+
802
+ ```yaml
803
+ # rev.yaml
804
+ postprocess:
805
+ pdf: ./scripts/fix-latex.sh
806
+ docx: ./scripts/add-metadata.py
807
+ all: ./scripts/notify.js
808
+ ```
809
+
810
+ Example `fix-latex.sh`:
811
+ ```bash
812
+ #!/bin/bash
813
+ # Receives: OUTPUT_FILE, OUTPUT_FORMAT, PROJECT_DIR, CONFIG_PATH
814
+
815
+ # Example: Replace longtable alignment
816
+ if [ "$OUTPUT_FORMAT" = "pdf" ]; then
817
+ echo "PDF postprocessing not needed (can't modify PDF)"
818
+ fi
819
+ ```
820
+
821
+ Example `add-metadata.py`:
822
+ ```python
823
+ #!/usr/bin/env python3
824
+ import os
825
+ from docx import Document
826
+
827
+ doc = Document(os.environ['OUTPUT_FILE'])
828
+ doc.core_properties.author = "Research Team"
829
+ doc.save(os.environ['OUTPUT_FILE'])
830
+ ```
831
+
832
+ ---
833
+
834
+ ## Risk Assessment
835
+
836
+ | Risk | Likelihood | Impact | Mitigation |
837
+ |------|------------|--------|------------|
838
+ | Table preprocessing breaks edge cases | Medium | Medium | Extensive tests, careful regex |
839
+ | Shell script security concerns | Low | High | Document that scripts run with user permissions |
840
+ | Cross-platform script compatibility | Medium | Medium | Support multiple interpreters, document requirements |
841
+ | Performance overhead from postprocess | Low | Low | Scripts are optional, run after main build |
842
+
843
+ ---
844
+
845
+ ## Success Criteria
846
+
847
+ 1. **Tables**: `Normal(0, 0.5)` in nowrap column → `$\mathcal{N}(0, 0.5)$` in PDF output
848
+ 2. **Postprocess**: User script receives correct environment variables and can modify output
849
+ 3. **Tests**: All new tests pass, existing tests unchanged
850
+ 4. **Docs**: README updated with examples for both features