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
@@ -0,0 +1,625 @@
1
+ /**
2
+ * PPTX post-processing
3
+ *
4
+ * Injects logos into each slide of a generated PPTX to match ref.pptx styling.
5
+ * Uses ref.pptx as-is for --reference-doc, then post-processes to add logos.
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'node:fs';
9
+ import { join, basename, extname, dirname } from 'node:path';
10
+ import { execSync } from 'node:child_process';
11
+
12
+ interface ThemeFonts {
13
+ major?: string;
14
+ minor?: string;
15
+ }
16
+
17
+ interface Theme {
18
+ fonts?: ThemeFonts;
19
+ }
20
+
21
+ interface TemplateOptions {
22
+ baseTemplate: string;
23
+ outputPath: string;
24
+ }
25
+
26
+ interface BuildupConfig {
27
+ default?: string;
28
+ title?: string;
29
+ grey?: string;
30
+ accent?: string;
31
+ enabled?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Extract PPTX to directory
36
+ */
37
+ async function extractPptx(pptxPath: string, destDir: string): Promise<void> {
38
+ if (process.platform === 'win32') {
39
+ const zipPath = pptxPath.replace(/\.pptx$/i, '.zip');
40
+ const content = readFileSync(pptxPath);
41
+ writeFileSync(zipPath, content);
42
+ try {
43
+ execSync(`powershell -Command "Expand-Archive -LiteralPath '${zipPath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'pipe' });
44
+ } finally {
45
+ try { unlinkSync(zipPath); } catch { /* ignore */ }
46
+ }
47
+ } else {
48
+ execSync(`unzip -q "${pptxPath}" -d "${destDir}"`, { stdio: 'pipe' });
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Create PPTX from directory
54
+ * Uses same compression settings as the original file
55
+ */
56
+ async function createPptx(srcDir: string, pptxPath: string): Promise<void> {
57
+ const scriptPath = join(dirname(pptxPath), '.zip-create.py');
58
+ const script = `import zipfile, os, sys
59
+
60
+ src, dst = sys.argv[1], sys.argv[2]
61
+
62
+ # Collect all files
63
+ files_to_add = []
64
+ for root, dirs, files in os.walk(src):
65
+ for f in files:
66
+ fp = os.path.join(root, f)
67
+ arcname = os.path.relpath(fp, src).replace(os.sep, '/')
68
+ files_to_add.append((fp, arcname))
69
+
70
+ # Write ZIP with DEFLATED compression
71
+ with zipfile.ZipFile(dst, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
72
+ for fp, arcname in files_to_add:
73
+ zf.write(fp, arcname)
74
+ `;
75
+
76
+ writeFileSync(scriptPath, script);
77
+ try {
78
+ const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
79
+ execSync(`${pythonCmd} "${scriptPath}" "${srcDir}" "${pptxPath}"`, { stdio: 'pipe' });
80
+ } finally {
81
+ try { unlinkSync(scriptPath); } catch { /* ignore */ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Recursively remove directory
87
+ */
88
+ function rmSync(path: string, options?: { recursive?: boolean }): void {
89
+ const fs = require('node:fs');
90
+ if (fs.rmSync) {
91
+ fs.rmSync(path, options);
92
+ } else {
93
+ const items = fs.readdirSync(path);
94
+ for (const item of items) {
95
+ const itemPath = join(path, item);
96
+ if (fs.statSync(itemPath).isDirectory()) {
97
+ rmSync(itemPath, options);
98
+ } else {
99
+ fs.unlinkSync(itemPath);
100
+ }
101
+ }
102
+ fs.rmdirSync(path);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Inject slide numbers into each slide of a PPTX
108
+ * Only adds slide numbers to slides that have a footer (i.e., slides with the green banner).
109
+ * Title slides, section slides, cover slides don't have the banner so they don't get numbers.
110
+ * Uses in-place ZIP modification to preserve file structure.
111
+ */
112
+ export async function injectSlideNumbers(pptxPath: string): Promise<void> {
113
+ if (!existsSync(pptxPath)) return;
114
+
115
+ const scriptPath = join(dirname(pptxPath), '.inject-slidenum.py');
116
+ const script = `import zipfile, sys, re, os
117
+
118
+ pptx_path = sys.argv[1]
119
+ temp_path = pptx_path + '.tmp'
120
+
121
+ # Slide number XML template with manual number (white text, 16pt)
122
+ def get_slidenum_xml(max_id, num):
123
+ return f'<p:sp><p:nvSpPr><p:cNvPr id="{max_id}" name="Slide Number Placeholder {max_id}"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldNum" sz="quarter" idx="12"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="8610600" y="6581838"/><a:ext cx="2743200" cy="319024"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:txBody><a:bodyPr/><a:lstStyle><a:lvl1pPr><a:defRPr sz="1600"><a:solidFill><a:srgbClr val="FFFFFF"/></a:solidFill></a:defRPr></a:lvl1pPr></a:lstStyle><a:p><a:r><a:rPr lang="en-GB" sz="1600" dirty="0"><a:solidFill><a:srgbClr val="FFFFFF"/></a:solidFill></a:rPr><a:t>{num}</a:t></a:r></a:p></p:txBody></p:sp>'
124
+
125
+ def is_content_slide(text):
126
+ """Check if slide is a content slide (has footer AND body placeholder)"""
127
+ has_footer = 'type="ftr"' in text
128
+ has_body = 'idx="1"' in text or 'type="body"' in text
129
+ return has_footer and has_body
130
+
131
+ # First pass: identify content slides and assign sequential numbers
132
+ with zipfile.ZipFile(pptx_path, 'r') as zin:
133
+ slide_numbers = {} # filename -> sequential number
134
+ content_num = 1
135
+
136
+ # Get all slide files sorted by number
137
+ slide_files = sorted([f for f in zin.namelist()
138
+ if f.startswith('ppt/slides/slide') and f.endswith('.xml')],
139
+ key=lambda x: int(re.search(r'slide(\\d+)', x).group(1)))
140
+
141
+ for fname in slide_files:
142
+ text = zin.read(fname).decode('utf-8')
143
+ if is_content_slide(text) and 'type="sldNum"' not in text:
144
+ slide_numbers[fname] = content_num
145
+ content_num += 1
146
+
147
+ # Second pass: inject numbers
148
+ with zipfile.ZipFile(pptx_path, 'r') as zin:
149
+ with zipfile.ZipFile(temp_path, 'w') as zout:
150
+ for item in zin.infolist():
151
+ content = zin.read(item.filename)
152
+
153
+ if item.filename in slide_numbers:
154
+ text = content.decode('utf-8')
155
+ # Find max id
156
+ ids = [int(m) for m in re.findall(r'id="(\\d+)"', text)]
157
+ max_id = max(ids) + 1 if ids else 100
158
+
159
+ # Insert slide number with sequential count
160
+ slidenum_xml = get_slidenum_xml(max_id, slide_numbers[item.filename])
161
+ text = text.replace('</p:spTree>', slidenum_xml + '</p:spTree>')
162
+ content = text.encode('utf-8')
163
+
164
+ zout.writestr(item, content)
165
+
166
+ os.replace(temp_path, pptx_path)
167
+ `;
168
+
169
+ writeFileSync(scriptPath, script);
170
+ try {
171
+ const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
172
+ execSync(`${pythonCmd} "${scriptPath}" "${pptxPath}"`, { stdio: 'pipe' });
173
+ } finally {
174
+ try { unlinkSync(scriptPath); } catch { /* ignore */ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Inject logos into cover slide of a PPTX (matching ref.pptx style)
180
+ * Uses in-place ZIP modification to preserve file structure.
181
+ */
182
+ export async function injectLogosIntoSlides(pptxPath: string, mediaDir: string | null): Promise<void> {
183
+ if (!mediaDir || !existsSync(mediaDir) || !existsSync(pptxPath)) return;
184
+
185
+ // Check for logo files
186
+ const logoLeft = join(mediaDir, 'logo-left.png');
187
+ const logoRight = join(mediaDir, 'logo-right.png');
188
+
189
+ const hasLeft = existsSync(logoLeft);
190
+ const hasRight = existsSync(logoRight);
191
+
192
+ if (!hasLeft && !hasRight) return;
193
+
194
+ // Read logo files as base64
195
+ const logoLeftData = hasLeft ? readFileSync(logoLeft).toString('base64') : '';
196
+ const logoRightData = hasRight ? readFileSync(logoRight).toString('base64') : '';
197
+
198
+ const scriptPath = join(dirname(pptxPath), '.inject-logos.py');
199
+ const script = `import zipfile, sys, re, os, base64
200
+
201
+ pptx_path = sys.argv[1]
202
+ has_left = ${hasLeft ? 'True' : 'False'}
203
+ has_right = ${hasRight ? 'True' : 'False'}
204
+ logo_left_b64 = """${logoLeftData}"""
205
+ logo_right_b64 = """${logoRightData}"""
206
+
207
+ temp_path = pptx_path + '.tmp'
208
+
209
+ # Find next available image number
210
+ def get_next_image_num(zf):
211
+ max_num = 0
212
+ for name in zf.namelist():
213
+ m = re.match(r'ppt/media/image(\\d+)\\.', name)
214
+ if m:
215
+ max_num = max(max_num, int(m.group(1)))
216
+ return max_num + 1
217
+
218
+ with zipfile.ZipFile(pptx_path, 'r') as zin:
219
+ next_img = get_next_image_num(zin)
220
+ right_img_name = f'ppt/media/image{next_img}.png' if has_right else None
221
+ left_img_name = f'ppt/media/image{next_img + 1}.png' if has_left else None
222
+
223
+ with zipfile.ZipFile(temp_path, 'w') as zout:
224
+ for item in zin.infolist():
225
+ content = zin.read(item.filename)
226
+
227
+ # Update [Content_Types].xml to include png if needed
228
+ if item.filename == '[Content_Types].xml':
229
+ text = content.decode('utf-8')
230
+ if 'Extension="png"' not in text:
231
+ text = text.replace('</Types>', '<Default Extension="png" ContentType="image/png"/></Types>')
232
+ content = text.encode('utf-8')
233
+
234
+ # Update slide1.xml.rels to add image relationships
235
+ if item.filename == 'ppt/slides/_rels/slide1.xml.rels':
236
+ text = content.decode('utf-8')
237
+ # Find max rId
238
+ rids = [int(m) for m in re.findall(r'Id="rId(\\d+)"', text)]
239
+ max_rid = max(rids) if rids else 0
240
+
241
+ new_rels = []
242
+ right_rid = None
243
+ left_rid = None
244
+
245
+ if has_right:
246
+ max_rid += 1
247
+ right_rid = f'rId{max_rid}'
248
+ new_rels.append(f'<Relationship Id="{right_rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image{next_img}.png"/>')
249
+
250
+ if has_left:
251
+ max_rid += 1
252
+ left_rid = f'rId{max_rid}'
253
+ new_rels.append(f'<Relationship Id="{left_rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image{next_img + 1}.png"/>')
254
+
255
+ if new_rels:
256
+ text = text.replace('</Relationships>', ''.join(new_rels) + '</Relationships>')
257
+ content = text.encode('utf-8')
258
+
259
+ # Store rIds for slide1 modification
260
+ zout.right_rid = right_rid
261
+ zout.left_rid = left_rid
262
+
263
+ # Update slide1.xml to add picture elements
264
+ if item.filename == 'ppt/slides/slide1.xml':
265
+ text = content.decode('utf-8')
266
+ # Find max id
267
+ ids = [int(m) for m in re.findall(r'id="(\\d+)"', text)]
268
+ max_id = max(ids) if ids else 0
269
+
270
+ pics = []
271
+ right_rid = getattr(zout, 'right_rid', None)
272
+ left_rid = getattr(zout, 'left_rid', None)
273
+
274
+ if has_right and right_rid:
275
+ max_id += 1
276
+ pics.append(f'<p:pic><p:nvPicPr><p:cNvPr id="{max_id}" name="Picture {max_id}"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="{right_rid}"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="9492000" y="5742001"/><a:ext cx="2700000" cy="1115999"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>')
277
+
278
+ if has_left and left_rid:
279
+ max_id += 1
280
+ pics.append(f'<p:pic><p:nvPicPr><p:cNvPr id="{max_id}" name="Picture {max_id}"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="{left_rid}"/><a:srcRect t="22495" b="27262"/><a:stretch/></p:blipFill><p:spPr><a:xfrm><a:off x="0" y="5904608"/><a:ext cx="3794408" cy="954349"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>')
281
+
282
+ if pics:
283
+ text = text.replace('</p:spTree>', ''.join(pics) + '</p:spTree>')
284
+ content = text.encode('utf-8')
285
+
286
+ zout.writestr(item, content)
287
+
288
+ # Add logo image files
289
+ if has_right:
290
+ zout.writestr(right_img_name, base64.b64decode(logo_right_b64))
291
+ if has_left:
292
+ zout.writestr(left_img_name, base64.b64decode(logo_left_b64))
293
+
294
+ os.replace(temp_path, pptx_path)
295
+ `;
296
+
297
+ writeFileSync(scriptPath, script);
298
+ try {
299
+ const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
300
+ execSync(`${pythonCmd} "${scriptPath}" "${pptxPath}"`, { stdio: 'pipe' });
301
+ } finally {
302
+ try { unlinkSync(scriptPath); } catch { /* ignore */ }
303
+ }
304
+ }
305
+
306
+ // Legacy exports for compatibility
307
+ export async function generatePptxTemplate(options: TemplateOptions): Promise<string | null> {
308
+ // No longer modifying template - just return the base template path
309
+ const { baseTemplate, outputPath } = options;
310
+ if (baseTemplate && existsSync(baseTemplate)) {
311
+ // Copy base template to output
312
+ writeFileSync(outputPath, readFileSync(baseTemplate));
313
+ return outputPath;
314
+ }
315
+ return null;
316
+ }
317
+
318
+ export function templateNeedsRegeneration(templatePath: string, mediaDir: string, baseTemplate: string): boolean {
319
+ return false; // No template regeneration needed - we use ref.pptx as-is
320
+ }
321
+
322
+ export async function injectMediaIntoPptx(pptxPath: string, mediaDir: string): Promise<void> {
323
+ // Redirect to the new function
324
+ return injectLogosIntoSlides(pptxPath, mediaDir);
325
+ }
326
+
327
+ /**
328
+ * Apply theme fonts to all text in a PPTX
329
+ * Pandoc generates slides with hardcoded fonts; this replaces them with theme font references.
330
+ * Uses in-place ZIP modification to preserve file structure.
331
+ */
332
+ export async function applyThemeFonts(pptxPath: string, theme: Theme): Promise<void> {
333
+ if (!existsSync(pptxPath) || !theme || !theme.fonts) return;
334
+
335
+ const { major, minor } = theme.fonts;
336
+ if (!major && !minor) return;
337
+
338
+ const scriptPath = join(dirname(pptxPath), '.apply-fonts.py');
339
+ const script = `import zipfile, sys, re, os
340
+
341
+ pptx_path = sys.argv[1]
342
+ temp_path = pptx_path + '.tmp'
343
+
344
+ # Fonts to replace with theme fonts
345
+ default_fonts = ['Calibri', 'Arial', 'Helvetica', 'Times New Roman', 'Cambria']
346
+
347
+ with zipfile.ZipFile(pptx_path, 'r') as zin:
348
+ with zipfile.ZipFile(temp_path, 'w') as zout:
349
+ for item in zin.infolist():
350
+ content = zin.read(item.filename)
351
+
352
+ # Process slide XML files
353
+ if item.filename.startswith('ppt/slides/slide') and item.filename.endswith('.xml'):
354
+ text = content.decode('utf-8')
355
+
356
+ # Replace common pandoc fonts with theme minor font reference
357
+ for font in default_fonts:
358
+ text = re.sub(rf'(<a:latin\\s+typeface="){font}(")', r'\\1+mn-lt\\2', text)
359
+
360
+ content = text.encode('utf-8')
361
+
362
+ zout.writestr(item, content)
363
+
364
+ os.replace(temp_path, pptx_path)
365
+ `;
366
+
367
+ writeFileSync(scriptPath, script);
368
+ try {
369
+ const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
370
+ execSync(`${pythonCmd} "${scriptPath}" "${pptxPath}"`, { stdio: 'pipe' });
371
+ } finally {
372
+ try { unlinkSync(scriptPath); } catch { /* ignore */ }
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Apply vertical centering to slides that have the .center class
378
+ * Uses in-place ZIP modification to preserve file structure.
379
+ */
380
+ export async function applyCentering(pptxPath: string, centeredSlideIndices: number[]): Promise<void> {
381
+ if (!existsSync(pptxPath) || !centeredSlideIndices || centeredSlideIndices.length === 0) return;
382
+
383
+ const scriptPath = join(dirname(pptxPath), '.apply-centering.py');
384
+ const indicesJson = JSON.stringify(centeredSlideIndices);
385
+ const script = `import zipfile, sys, re, os, json
386
+
387
+ pptx_path = sys.argv[1]
388
+ centered_indices = json.loads('${indicesJson}')
389
+ temp_path = pptx_path + '.tmp'
390
+
391
+ # Build set of slide filenames to center
392
+ centered_files = {f'ppt/slides/slide{i}.xml' for i in centered_indices}
393
+
394
+ with zipfile.ZipFile(pptx_path, 'r') as zin:
395
+ with zipfile.ZipFile(temp_path, 'w') as zout:
396
+ for item in zin.infolist():
397
+ content = zin.read(item.filename)
398
+
399
+ # Process centered slides
400
+ if item.filename in centered_files:
401
+ text = content.decode('utf-8')
402
+
403
+ # Process each shape (<p:sp>) separately to skip footer and slide number
404
+ def process_shape(shape_match):
405
+ shape = shape_match.group(0)
406
+ # Skip footer and slide number placeholders
407
+ if 'type="sldNum"' in shape or 'type="ftr"' in shape:
408
+ return shape
409
+
410
+ # Add algn="ctr" to existing <a:pPr> elements
411
+ # Handle both <a:pPr ...> and <a:pPr ... /> (self-closing)
412
+ def add_center_align(match):
413
+ before, attrs, closing = match.groups()
414
+ attrs = attrs.rstrip()
415
+ is_self_closing = '/' in closing
416
+
417
+ if attrs.endswith('/'):
418
+ attrs = attrs[:-1].rstrip()
419
+ is_self_closing = True
420
+
421
+ if 'algn=' not in attrs:
422
+ attrs += ' algn="ctr"'
423
+ else:
424
+ attrs = re.sub(r'algn="[^"]*"', 'algn="ctr"', attrs)
425
+
426
+ return before + attrs + (' />' if is_self_closing else '>')
427
+
428
+ shape = re.sub(r'(<a:pPr)((?:[^/>]|/(?!>))*)(\\s*/?>)', add_center_align, shape)
429
+
430
+ # Add <a:pPr algn="ctr"/> to paragraphs without pPr
431
+ shape = re.sub(r'(<a:p>)(<a:r>)', r'\\1<a:pPr algn="ctr"/>\\2', shape)
432
+
433
+ return shape
434
+
435
+ # Process all shapes
436
+ text = re.sub(r'<p:sp>.*?</p:sp>', process_shape, text, flags=re.DOTALL)
437
+
438
+ content = text.encode('utf-8')
439
+
440
+ zout.writestr(item, content)
441
+
442
+ os.replace(temp_path, pptx_path)
443
+ `;
444
+
445
+ writeFileSync(scriptPath, script);
446
+ try {
447
+ const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
448
+ execSync(`${pythonCmd} "${scriptPath}" "${pptxPath}"`, { stdio: 'pipe' });
449
+ } finally {
450
+ try { unlinkSync(scriptPath); } catch { /* ignore */ }
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Apply buildup greying to slides with buildup content
456
+ * Greys out all bullet items except the last one, which gets the accent color.
457
+ * Only affects actual bullet items (not intro text with buNone).
458
+ * Uses in-place ZIP modification to preserve file structure.
459
+ */
460
+ export async function applyBuildupColors(pptxPath: string, config: BuildupConfig = {}): Promise<void> {
461
+ if (!existsSync(pptxPath)) return;
462
+
463
+ // Check if buildup colors are disabled
464
+ if (config.enabled === false) return;
465
+
466
+ // Get colors from config with defaults
467
+ const defaultColor = (config.default || '608C32').replace(/^#/, '');
468
+ const titleColor = (config.title || defaultColor).replace(/^#/, '');
469
+ const greyColor = (config.grey || '888888').replace(/^#/, '');
470
+ const accentColor = (config.accent || defaultColor).replace(/^#/, '');
471
+
472
+ const scriptPath = join(dirname(pptxPath), '.apply-buildup.py');
473
+ const script = `import zipfile
474
+ import sys
475
+ import re
476
+ import os
477
+
478
+ pptx_path = sys.argv[1]
479
+ temp_path = pptx_path + '.tmp'
480
+
481
+ DEFAULT = '${defaultColor}'
482
+ TITLE = '${titleColor}'
483
+ GREY = '${greyColor}'
484
+ ACCENT = '${accentColor}'
485
+
486
+ def get_bullet_paragraphs(body):
487
+ """Return indices of paragraphs that are actual bullet items (have lvl="0" but NOT buNone)"""
488
+ paras = list(re.finditer(r'<a:p>.*?</a:p>', body, re.DOTALL))
489
+ bullet_indices = []
490
+
491
+ for i, p in enumerate(paras):
492
+ para_text = p.group(0)
493
+ if 'lvl="0"' in para_text and '<a:buNone' not in para_text:
494
+ bullet_indices.append(i)
495
+
496
+ return bullet_indices, paras
497
+
498
+
499
+ def apply_color_to_para(para, color):
500
+ """Apply a color to all text runs in a paragraph"""
501
+ new_para = re.sub(
502
+ r'<a:rPr\\s*/>',
503
+ f'<a:rPr><a:solidFill><a:srgbClr val="{color}"/></a:solidFill></a:rPr>',
504
+ para
505
+ )
506
+
507
+ def fix_rpr_with_attrs(m):
508
+ attrs = m.group(1).strip()
509
+ return f'<a:rPr {attrs}><a:solidFill><a:srgbClr val="{color}"/></a:solidFill></a:rPr>'
510
+
511
+ new_para = re.sub(
512
+ r'<a:rPr\\s+([^>]+?)\\s*/>',
513
+ fix_rpr_with_attrs,
514
+ new_para
515
+ )
516
+
517
+ return new_para
518
+
519
+
520
+ def is_buildup_slide(xml):
521
+ """Check if slide has buildup marker (animEffect with filter=wipe)"""
522
+ # Buildup slides have animation effects from pandoc's incremental lists
523
+ return 'animEffect' in xml or '<a:bldLst>' in xml
524
+
525
+
526
+ def color_content_placeholder(xml):
527
+ """Apply colors to all text in content placeholder.
528
+
529
+ For buildup slides: grey previous bullet items, accent on last bullet item.
530
+ For all text (bullets and non-bullets): apply default color unless overridden by buildup.
531
+ """
532
+
533
+ pattern = r'(<p:sp>.*?<p:ph idx="1"[^/]*/?>.*?<p:txBody>)(.*?)(</p:txBody>.*?</p:sp>)'
534
+ match = re.search(pattern, xml, re.DOTALL)
535
+
536
+ if not match:
537
+ return xml
538
+
539
+ before_body = match.group(1)
540
+ body = match.group(2)
541
+ after_body = match.group(3)
542
+
543
+ bullet_indices, paras = get_bullet_paragraphs(body)
544
+ is_buildup = is_buildup_slide(xml)
545
+
546
+ new_body = body
547
+ offset = 0
548
+
549
+ for i, para_match in enumerate(paras):
550
+ start = para_match.start() + offset
551
+ end = para_match.end() + offset
552
+ para = para_match.group(0)
553
+
554
+ # Determine color for this paragraph
555
+ if i in bullet_indices and is_buildup:
556
+ # Buildup bullet: grey all but last, accent on last
557
+ if i == bullet_indices[-1]:
558
+ color = ACCENT
559
+ else:
560
+ color = GREY
561
+ else:
562
+ # Non-bullet text OR non-buildup slide: use default color
563
+ color = DEFAULT
564
+
565
+ new_para = apply_color_to_para(para, color)
566
+ new_body = new_body[:start] + new_para + new_body[end:]
567
+ offset += len(new_para) - len(para)
568
+
569
+ return xml[:match.start()] + before_body + new_body + after_body + xml[match.end():]
570
+
571
+
572
+ def color_title_placeholder(xml):
573
+ """Apply title color to title placeholder (type='title' or type='ctrTitle')."""
574
+
575
+ # Match title placeholders: type="title" or type="ctrTitle"
576
+ pattern = r'(<p:sp>.*?<p:ph[^>]*type="(?:title|ctrTitle)"[^/]*/?>.*?<p:txBody>)(.*?)(</p:txBody>.*?</p:sp>)'
577
+ match = re.search(pattern, xml, re.DOTALL)
578
+
579
+ if not match:
580
+ return xml
581
+
582
+ before_body = match.group(1)
583
+ body = match.group(2)
584
+ after_body = match.group(3)
585
+
586
+ paras = list(re.finditer(r'<a:p>.*?</a:p>', body, re.DOTALL))
587
+
588
+ new_body = body
589
+ offset = 0
590
+
591
+ for para_match in paras:
592
+ start = para_match.start() + offset
593
+ end = para_match.end() + offset
594
+ para = para_match.group(0)
595
+
596
+ new_para = apply_color_to_para(para, TITLE)
597
+ new_body = new_body[:start] + new_para + new_body[end:]
598
+ offset += len(new_para) - len(para)
599
+
600
+ return xml[:match.start()] + before_body + new_body + after_body + xml[match.end():]
601
+
602
+ with zipfile.ZipFile(pptx_path, 'r') as zin:
603
+ with zipfile.ZipFile(temp_path, 'w') as zout:
604
+ for item in zin.infolist():
605
+ content = zin.read(item.filename)
606
+
607
+ if item.filename.startswith('ppt/slides/slide') and item.filename.endswith('.xml'):
608
+ text = content.decode('utf-8')
609
+ text = color_content_placeholder(text)
610
+ text = color_title_placeholder(text)
611
+ content = text.encode('utf-8')
612
+
613
+ zout.writestr(item, content)
614
+
615
+ os.replace(temp_path, pptx_path)
616
+ `;
617
+
618
+ writeFileSync(scriptPath, script);
619
+ try {
620
+ const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
621
+ execSync(`${pythonCmd} "${scriptPath}" "${pptxPath}"`, { stdio: 'pipe' });
622
+ } finally {
623
+ try { unlinkSync(scriptPath); } catch { /* ignore */ }
624
+ }
625
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file