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
package/lib/slides.ts ADDED
@@ -0,0 +1,793 @@
1
+ /**
2
+ * Slide processing for Beamer and PPTX output
3
+ *
4
+ * Handles:
5
+ * - ::: step blocks for incremental reveals
6
+ * - ::: buildup blocks for progressive bullet reveals with greying
7
+ * - ::: notes blocks for speaker notes
8
+ * - Slide boundaries (---)
9
+ * - Slide styles: {.dark}, {.light}, {.accent}, {.inverse}
10
+ * - Special slides: {.cover}, {.thanks}, {.section}, {.plain}
11
+ *
12
+ * Syntax examples:
13
+ * ## Title {.dark} - Dark background slide
14
+ * ## Welcome {.cover} - Cover slide (no numbering, centered)
15
+ * ## Thank You {.thanks} - Thanks slide (no numbering)
16
+ * # Part 1 {.section} - Section divider slide
17
+ * ## Image {.plain} - No header/footer, full content
18
+ * ## Highlight {.accent .nonumber} - Accent color, no slide number
19
+ *
20
+ * Buildup syntax:
21
+ * ::: buildup
22
+ * - First point
23
+ * - Sub A
24
+ * - Sub B
25
+ * - Second point
26
+ * :::
27
+ *
28
+ * Generates slides where current point is colored, previous are greyed out.
29
+ * Subpoints appear sequentially within their parent.
30
+ */
31
+
32
+ interface Step {
33
+ index: number;
34
+ content: string;
35
+ }
36
+
37
+ interface SlideStyle {
38
+ background: string | null;
39
+ type: string | null;
40
+ nonumber: boolean;
41
+ center: boolean;
42
+ classes: string[];
43
+ }
44
+
45
+ interface Slide {
46
+ title: string;
47
+ titleLevel: number;
48
+ steps: Step[];
49
+ notes: string | null;
50
+ preamble: string;
51
+ style: SlideStyle;
52
+ _frontmatter?: string;
53
+ }
54
+
55
+ interface BulletItem {
56
+ text: string;
57
+ indent: number;
58
+ children: BulletItem[];
59
+ }
60
+
61
+ interface BuildupStep {
62
+ itemIndex: number;
63
+ subIndex: number | null;
64
+ }
65
+
66
+ interface BuildupResult {
67
+ content: string;
68
+ afterContent?: string;
69
+ buildupSteps: string[] | null;
70
+ }
71
+
72
+ interface StepWithBuildup extends Step {
73
+ beforeBuildup?: string;
74
+ afterBuildup?: string;
75
+ buildupSteps?: string[] | null;
76
+ }
77
+
78
+ /**
79
+ * Grey color for "completed" buildup items
80
+ */
81
+ const GREY_COLOR = '#888888';
82
+
83
+ /**
84
+ * Background styles mapped to Beamer options
85
+ */
86
+ const BEAMER_BACKGROUNDS: Record<string, string> = {
87
+ dark: '\\setbeamercolor{background canvas}{bg=black}\\setbeamercolor{normal text}{fg=white}\\usebeamercolor[fg]{normal text}',
88
+ light: '\\setbeamercolor{background canvas}{bg=white}\\setbeamercolor{normal text}{fg=black}\\usebeamercolor[fg]{normal text}',
89
+ accent: '\\setbeamercolor{background canvas}{bg=structure.fg}\\setbeamercolor{normal text}{fg=white}\\usebeamercolor[fg]{normal text}',
90
+ inverse: '\\setbeamercolor{background canvas}{bg=structure.fg!90!black}\\setbeamercolor{normal text}{fg=white}\\usebeamercolor[fg]{normal text}',
91
+ };
92
+
93
+ /**
94
+ * Special slide types mapped to Beamer frame options
95
+ */
96
+ const BEAMER_FRAME_OPTIONS: Record<string, string> = {
97
+ cover: 'plain,noframenumbering,c',
98
+ thanks: 'plain,noframenumbering,c',
99
+ section: 'plain,noframenumbering,c',
100
+ plain: 'plain',
101
+ };
102
+
103
+ /**
104
+ * Parse a bullet list into a tree structure
105
+ */
106
+ function parseBulletList(content: string): BulletItem[] {
107
+ const lines = content.split('\n');
108
+ const items: BulletItem[] = [];
109
+ const stack: Array<BulletItem & { children: BulletItem[] }> = [{ children: items, indent: -1 } as any];
110
+
111
+ for (const line of lines) {
112
+ // Match bullet lines: "- text" or " - text" etc.
113
+ const match = line.match(/^(\s*)[-*]\s+(.+)$/);
114
+ if (!match) continue;
115
+
116
+ const indent = (match[1] || '').length;
117
+ const text = (match[2] || '').trim();
118
+
119
+ const item: BulletItem = { text, indent, children: [] };
120
+
121
+ // Find parent based on indentation
122
+ while (stack.length > 1) {
123
+ const top = stack[stack.length - 1];
124
+ if (!top || top.indent < indent) break;
125
+ stack.pop();
126
+ }
127
+
128
+ const parent = stack[stack.length - 1];
129
+ if (parent) {
130
+ parent.children.push(item);
131
+ }
132
+ stack.push(item as any);
133
+ }
134
+
135
+ return items;
136
+ }
137
+
138
+ /**
139
+ * Flatten bullet tree into sequential reveal steps
140
+ * Each step is: { itemIndex, subIndex, isSubItem }
141
+ */
142
+ function flattenBuildupSteps(items: BulletItem[]): BuildupStep[] {
143
+ const steps: BuildupStep[] = [];
144
+
145
+ for (let i = 0; i < items.length; i++) {
146
+ const item = items[i];
147
+ if (!item) continue;
148
+
149
+ if (item.children.length === 0) {
150
+ // No children - single step for this item
151
+ steps.push({ itemIndex: i, subIndex: null });
152
+ } else {
153
+ // Has children - first show parent, then each child
154
+ steps.push({ itemIndex: i, subIndex: -1 }); // Parent only
155
+
156
+ for (let j = 0; j < item.children.length; j++) {
157
+ steps.push({ itemIndex: i, subIndex: j });
158
+ }
159
+ }
160
+ }
161
+
162
+ return steps;
163
+ }
164
+
165
+ /**
166
+ * Render a bullet item with optional color
167
+ */
168
+ function renderBulletItem(
169
+ item: BulletItem,
170
+ color: string | null,
171
+ indentLevel: number = 0,
172
+ showChildrenUpTo: number | null = null
173
+ ): string {
174
+ const indent = ' '.repeat(indentLevel);
175
+ const lines: string[] = [];
176
+
177
+ // Render the main item
178
+ const text = color ? `[${item.text}]{color=${color}}` : item.text;
179
+ lines.push(`${indent}- ${text}`);
180
+
181
+ // Render children if any should be shown
182
+ if (showChildrenUpTo !== null && showChildrenUpTo >= 0 && item.children) {
183
+ for (let i = 0; i <= showChildrenUpTo && i < item.children.length; i++) {
184
+ const child = item.children[i];
185
+ if (!child) continue;
186
+ const childColor = color; // Children inherit parent's color state
187
+ const childText = childColor ? `[${child.text}]{color=${childColor}}` : child.text;
188
+ lines.push(`${indent} - ${childText}`);
189
+ }
190
+ }
191
+
192
+ return lines.join('\n');
193
+ }
194
+
195
+ /**
196
+ * Generate buildup slides from a bullet list
197
+ */
198
+ function expandBuildup(content: string, format: string): string[] {
199
+ const items = parseBulletList(content);
200
+ if (items.length === 0) return [content];
201
+
202
+ const steps = flattenBuildupSteps(items);
203
+ const slideContents: string[] = [];
204
+
205
+ for (const step of steps) {
206
+ const lines: string[] = [];
207
+
208
+ for (let i = 0; i < items.length; i++) {
209
+ const item = items[i];
210
+ if (!item) continue;
211
+
212
+ if (i < step.itemIndex) {
213
+ // Previous top-level item - greyed out with all children
214
+ const allChildrenIndex = item.children.length > 0 ? item.children.length - 1 : null;
215
+ lines.push(renderBulletItem(item, GREY_COLOR, 0, allChildrenIndex));
216
+ } else if (i === step.itemIndex) {
217
+ // Current top-level item - colored
218
+ if (step.subIndex === null) {
219
+ // No children case - just show item
220
+ lines.push(renderBulletItem(item, null, 0, null));
221
+ } else if (step.subIndex === -1) {
222
+ // Has children but showing parent only first
223
+ lines.push(renderBulletItem(item, null, 0, null));
224
+ } else {
225
+ // Showing parent + children up to subIndex
226
+ lines.push(renderBulletItem(item, null, 0, step.subIndex));
227
+ }
228
+ }
229
+ // Future items (i > step.itemIndex) - not shown yet
230
+ }
231
+
232
+ slideContents.push(lines.join('\n'));
233
+ }
234
+
235
+ return slideContents;
236
+ }
237
+
238
+ /**
239
+ * Process ::: buildup blocks in content and expand to steps
240
+ */
241
+ function processBuildupBlocks(content: string, format: string): BuildupResult {
242
+ const buildupMatch = content.match(/^:::\s*buildup\s*\n([\s\S]*?)\n:::\s*$/m);
243
+
244
+ if (!buildupMatch || buildupMatch.index === undefined) {
245
+ return { content, buildupSteps: null };
246
+ }
247
+
248
+ const buildupContent = buildupMatch[1] || '';
249
+ const buildupSteps = expandBuildup(buildupContent, format);
250
+
251
+ // Remove the buildup block from content (will be replaced by steps)
252
+ const beforeBuildup = content.slice(0, buildupMatch.index).trim();
253
+ const afterBuildup = content.slice(buildupMatch.index + buildupMatch[0].length).trim();
254
+
255
+ return {
256
+ content: beforeBuildup,
257
+ afterContent: afterBuildup,
258
+ buildupSteps,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Parse slide style attributes from heading
264
+ */
265
+ function parseSlideStyle(heading: string): { title: string; style: SlideStyle } {
266
+ const style: SlideStyle = {
267
+ background: null,
268
+ type: null,
269
+ nonumber: false,
270
+ center: false,
271
+ classes: [],
272
+ };
273
+
274
+ // Match {.class1 .class2} at end of heading
275
+ const attrMatch = heading.match(/\s*\{([^}]+)\}\s*$/);
276
+ if (!attrMatch || attrMatch.index === undefined) {
277
+ return { title: heading.trim(), style };
278
+ }
279
+
280
+ const title = heading.slice(0, attrMatch.index).trim();
281
+ const attrs = attrMatch[1] || '';
282
+
283
+ // Parse each .class
284
+ const classMatches = attrs.matchAll(/\.(\w+)/g);
285
+ for (const match of classMatches) {
286
+ if (!match[1]) continue;
287
+ const cls = match[1].toLowerCase();
288
+ style.classes.push(cls);
289
+
290
+ // Background styles
291
+ if (['dark', 'light', 'accent', 'inverse'].includes(cls)) {
292
+ style.background = cls;
293
+ }
294
+ // Special slide types
295
+ else if (['cover', 'thanks', 'section', 'plain'].includes(cls)) {
296
+ style.type = cls;
297
+ // Cover, thanks, section slides default to no numbering
298
+ if (['cover', 'thanks', 'section'].includes(cls)) {
299
+ style.nonumber = true;
300
+ style.center = true;
301
+ }
302
+ }
303
+ // Explicit options
304
+ else if (cls === 'nonumber' || cls === 'unnumbered') {
305
+ style.nonumber = true;
306
+ } else if (cls === 'center' || cls === 'centered') {
307
+ style.center = true;
308
+ }
309
+ }
310
+
311
+ return { title, style };
312
+ }
313
+
314
+ /**
315
+ * Parse a single slide's content into steps and notes
316
+ */
317
+ export function parseSlide(slideContent: string): Slide {
318
+ const lines = slideContent.split('\n');
319
+
320
+ // Extract title (first heading)
321
+ let title = '';
322
+ let titleLevel = 2;
323
+ let titleLineIndex = -1;
324
+ let style: SlideStyle = {
325
+ background: null,
326
+ type: null,
327
+ nonumber: false,
328
+ center: false,
329
+ classes: [],
330
+ };
331
+
332
+ for (let i = 0; i < lines.length; i++) {
333
+ const line = lines[i];
334
+ if (!line) continue;
335
+ const match = line.match(/^(#{1,6})\s+(.+)$/);
336
+ if (match && match[1] && match[2]) {
337
+ titleLevel = match[1].length;
338
+ // Parse style from heading
339
+ const parsed = parseSlideStyle(match[2]);
340
+ title = parsed.title;
341
+ style = parsed.style;
342
+ titleLineIndex = i;
343
+ break;
344
+ }
345
+ }
346
+
347
+ // Content after title
348
+ const contentStart = titleLineIndex >= 0 ? titleLineIndex + 1 : 0;
349
+ const bodyContent = lines.slice(contentStart).join('\n');
350
+
351
+ // Parse ::: step and ::: notes blocks
352
+ const steps: Step[] = [];
353
+ let notes: string | null = null;
354
+ let preamble = '';
355
+
356
+ // Regex to match fenced div blocks
357
+ const bodyLines = bodyContent.split('\n');
358
+ let inBlock = false;
359
+ let blockType: string | null = null;
360
+ let currentBlockContent: string[] = [];
361
+ let beforeFirstStep: string[] = [];
362
+ let foundFirstStep = false;
363
+
364
+ for (let i = 0; i < bodyLines.length; i++) {
365
+ const line = bodyLines[i];
366
+ if (!line && line !== '') continue;
367
+
368
+ if (!inBlock) {
369
+ // Check for block start
370
+ const stepMatch = line.match(/^:::\s*step\s*$/);
371
+ const notesMatch = line.match(/^:::\s*notes\s*$/);
372
+
373
+ if (stepMatch) {
374
+ inBlock = true;
375
+ blockType = 'step';
376
+ currentBlockContent = [];
377
+ foundFirstStep = true;
378
+ } else if (notesMatch) {
379
+ inBlock = true;
380
+ blockType = 'notes';
381
+ currentBlockContent = [];
382
+ } else if (!foundFirstStep) {
383
+ beforeFirstStep.push(line);
384
+ }
385
+ } else {
386
+ // Check for block end
387
+ if (line.match(/^:::\s*$/)) {
388
+ // End of block
389
+ if (blockType === 'step') {
390
+ steps.push({
391
+ index: steps.length + 1,
392
+ content: currentBlockContent.join('\n').trim(),
393
+ });
394
+ } else if (blockType === 'notes') {
395
+ notes = currentBlockContent.join('\n').trim();
396
+ }
397
+ inBlock = false;
398
+ blockType = null;
399
+ currentBlockContent = [];
400
+ } else {
401
+ currentBlockContent.push(line);
402
+ }
403
+ }
404
+ }
405
+
406
+ // Handle content before first step as preamble
407
+ preamble = beforeFirstStep.join('\n').trim();
408
+
409
+ // If no explicit steps, treat entire body as single step
410
+ if (steps.length === 0) {
411
+ // Remove notes from body if present
412
+ let bodyWithoutNotes = bodyContent;
413
+ const notesBlockMatch = bodyContent.match(/^:::\s*notes\s*$[\s\S]*?^:::\s*$/m);
414
+ if (notesBlockMatch) {
415
+ bodyWithoutNotes = bodyContent.replace(notesBlockMatch[0], '').trim();
416
+ }
417
+
418
+ steps.push({
419
+ index: 1,
420
+ content: bodyWithoutNotes.trim(),
421
+ });
422
+ preamble = '';
423
+ }
424
+
425
+ return {
426
+ title,
427
+ titleLevel,
428
+ steps,
429
+ notes,
430
+ preamble,
431
+ style,
432
+ };
433
+ }
434
+
435
+ /**
436
+ * Parse markdown document into slides
437
+ */
438
+ export function parseSlides(markdown: string): Slide[] {
439
+ // Normalize line endings to \n
440
+ const normalized = markdown.replace(/\r\n/g, '\n');
441
+
442
+ // Split by --- (horizontal rule / slide delimiter)
443
+ // Handle YAML frontmatter by checking for --- at start
444
+ let content = normalized;
445
+ let frontmatter: string | null = null;
446
+
447
+ // Extract YAML frontmatter if present
448
+ if (normalized.startsWith('---')) {
449
+ const endMatch = normalized.slice(3).indexOf('\n---');
450
+ if (endMatch !== -1) {
451
+ frontmatter = normalized.slice(0, endMatch + 7); // Include both ---
452
+ content = normalized.slice(endMatch + 7).trim();
453
+ }
454
+ }
455
+
456
+ // Split remaining content by ---
457
+ const parts = content.split(/\n---\n/);
458
+
459
+ const slides: Slide[] = [];
460
+ for (const part of parts) {
461
+ if (!part) continue;
462
+ const trimmed = part.trim();
463
+ if (trimmed) {
464
+ slides.push(parseSlide(trimmed));
465
+ }
466
+ }
467
+
468
+ // Attach frontmatter to first slide's preamble if exists
469
+ if (frontmatter && slides.length > 0 && slides[0]) {
470
+ slides[0]._frontmatter = frontmatter;
471
+ }
472
+
473
+ return slides;
474
+ }
475
+
476
+ /**
477
+ * Build Beamer frame options string
478
+ */
479
+ function buildBeamerFrameOptions(style: SlideStyle | null): string {
480
+ if (!style) return '';
481
+
482
+ const options: string[] = [];
483
+
484
+ // Special slide type options
485
+ if (style.type && BEAMER_FRAME_OPTIONS[style.type]) {
486
+ const opts = BEAMER_FRAME_OPTIONS[style.type];
487
+ if (opts) options.push(...opts.split(','));
488
+ } else {
489
+ // Individual options
490
+ if (style.nonumber) {
491
+ options.push('noframenumbering');
492
+ }
493
+ if (style.center) {
494
+ options.push('c');
495
+ }
496
+ }
497
+
498
+ // Deduplicate
499
+ const unique = [...new Set(options)];
500
+ return unique.length > 0 ? `[${unique.join(',')}]` : '';
501
+ }
502
+
503
+ /**
504
+ * Generate Beamer markdown using pandoc's native slide structure
505
+ * Works WITH pandoc, not against it - pandoc creates frames, we add overlays
506
+ */
507
+ export function generateBeamerMarkdown(slides: Slide[]): string {
508
+ const output: string[] = [];
509
+
510
+ // Check if first slide is a cover slide - if so, skip pandoc's auto title page
511
+ const hasExplicitCover = slides.length > 0 && slides[0] && slides[0].style && slides[0].style.type === 'cover';
512
+
513
+ for (const slide of slides) {
514
+ // Include frontmatter if present
515
+ if (slide._frontmatter) {
516
+ let frontmatter = slide._frontmatter;
517
+
518
+ // If we have an explicit cover slide, remove title/author/date to prevent
519
+ // pandoc from generating a duplicate title frame
520
+ if (hasExplicitCover) {
521
+ // Remove title, author, date lines but keep other frontmatter
522
+ frontmatter = frontmatter
523
+ .replace(/^title:.*\n?/m, '')
524
+ .replace(/^author:.*\n?/m, '')
525
+ .replace(/^date:.*\n?/m, '')
526
+ .replace(/\n{2,}/g, '\n'); // Clean up extra blank lines
527
+
528
+ // Check if frontmatter is now empty (just --- and ---)
529
+ const content = frontmatter.replace(/---/g, '').trim();
530
+ if (!content) {
531
+ // Skip empty frontmatter entirely - don't output anything
532
+ // The slide content will follow directly
533
+ } else {
534
+ output.push(frontmatter);
535
+ output.push('');
536
+ }
537
+ } else {
538
+ output.push(frontmatter);
539
+ output.push('');
540
+ }
541
+ }
542
+
543
+ // Build pandoc heading with beamer attributes
544
+ let headingAttrs = '';
545
+ const attrList: string[] = [];
546
+
547
+ if (slide.style) {
548
+ if (slide.style.type === 'cover' || slide.style.type === 'thanks' || slide.style.type === 'section') {
549
+ attrList.push('.plain');
550
+ attrList.push('.noframenumbering');
551
+ attrList.push('.c');
552
+ } else if (slide.style.type === 'plain') {
553
+ attrList.push('.plain');
554
+ }
555
+ if (slide.style.nonumber && !attrList.includes('.noframenumbering')) {
556
+ attrList.push('.noframenumbering');
557
+ }
558
+ if (slide.style.center && !attrList.includes('.c')) {
559
+ attrList.push('.c');
560
+ }
561
+ }
562
+
563
+ if (attrList.length > 0) {
564
+ headingAttrs = ' {' + attrList.join(' ') + '}';
565
+ }
566
+
567
+ // Frame heading - pandoc creates the frame from this
568
+ const heading = '#'.repeat(slide.titleLevel) + ' ' + slide.title + headingAttrs;
569
+ output.push(heading);
570
+ output.push('');
571
+
572
+ // Note: Per-frame background colors (.dark, .accent) are not yet supported
573
+ // in the pandoc-based beamer output. The classes are preserved for PPTX.
574
+
575
+ // Add preamble if present (visible on all overlays)
576
+ if (slide.preamble) {
577
+ output.push(slide.preamble);
578
+ output.push('');
579
+ }
580
+
581
+ // Generate content
582
+ if (slide.steps.length === 1 && slide.steps[0]) {
583
+ // Single step - just output content, no overlays needed
584
+ output.push(slide.steps[0].content);
585
+ } else {
586
+ // Multiple steps - use \pause between steps
587
+ // This is the pandoc-friendly way to do incremental reveals
588
+ for (let i = 0; i < slide.steps.length; i++) {
589
+ const step = slide.steps[i];
590
+ if (!step) continue;
591
+ output.push(step.content);
592
+ output.push('');
593
+ // Add pause after each step except the last
594
+ if (i < slide.steps.length - 1) {
595
+ output.push('. . .');
596
+ output.push('');
597
+ }
598
+ }
599
+ }
600
+
601
+ // Add speaker notes as LaTeX \note{} command
602
+ if (slide.notes) {
603
+ output.push('');
604
+ // Escape special LaTeX characters in notes
605
+ const notes = slide.notes || '';
606
+ const escapedNotes = notes
607
+ .replace(/\\/g, '\\textbackslash{}')
608
+ .replace(/[&%$#_{}]/g, '\\$&')
609
+ .replace(/\n/g, '\\\\');
610
+ output.push('\\note{' + escapedNotes + '}');
611
+ }
612
+
613
+ output.push('');
614
+ output.push('---');
615
+ output.push('');
616
+ }
617
+
618
+ // Remove trailing ---
619
+ while (output.length > 0) {
620
+ const last = output[output.length - 1];
621
+ if (!last || last.trim() !== '') break;
622
+ output.pop();
623
+ }
624
+ if (output.length > 0 && output[output.length - 1] === '---') {
625
+ output.pop();
626
+ }
627
+
628
+ return output.join('\n');
629
+ }
630
+
631
+ /**
632
+ * Build PPTX slide class attribute string
633
+ */
634
+ function buildPptxSlideClasses(style: SlideStyle | null): string {
635
+ if (!style || !style.classes || style.classes.length === 0) {
636
+ return '';
637
+ }
638
+ return ' {.' + style.classes.join(' .') + '}';
639
+ }
640
+
641
+ /**
642
+ * Generate PPTX markdown with duplicated slides for steps
643
+ * Each step becomes a separate physical slide
644
+ * Handles ::: buildup blocks by expanding them into multiple slides
645
+ */
646
+ export function generatePptxMarkdown(slides: Slide[]): string {
647
+ const output: string[] = [];
648
+
649
+ for (const slide of slides) {
650
+ // Include frontmatter if present (only on first slide)
651
+ if (slide._frontmatter) {
652
+ output.push(slide._frontmatter);
653
+ output.push('');
654
+ }
655
+
656
+ // Check if any step contains a buildup block
657
+ const stepsWithBuildup: StepWithBuildup[] = slide.steps.map((step) => {
658
+ const result = processBuildupBlocks(step.content, 'pptx');
659
+ return {
660
+ ...step,
661
+ beforeBuildup: result.content,
662
+ afterBuildup: result.afterContent || '',
663
+ buildupSteps: result.buildupSteps,
664
+ };
665
+ });
666
+
667
+ // Generate slides - handle buildup expansion
668
+ for (let i = 0; i < stepsWithBuildup.length; i++) {
669
+ const step = stepsWithBuildup[i];
670
+ if (!step) continue;
671
+
672
+ if (step.buildupSteps && step.buildupSteps.length > 0) {
673
+ // This step has a buildup block - generate one slide per buildup step
674
+ for (const buildupContent of step.buildupSteps) {
675
+ const classes = buildPptxSlideClasses(slide.style);
676
+ const heading = '#'.repeat(slide.titleLevel) + ' ' + slide.title + classes;
677
+ output.push(heading);
678
+ output.push('');
679
+
680
+ // Add preamble if present
681
+ if (slide.preamble) {
682
+ output.push(slide.preamble);
683
+ output.push('');
684
+ }
685
+
686
+ // Add content before buildup block
687
+ if (step.beforeBuildup) {
688
+ output.push(step.beforeBuildup);
689
+ output.push('');
690
+ }
691
+
692
+ // Add this buildup step content
693
+ output.push(buildupContent);
694
+ output.push('');
695
+
696
+ // Add content after buildup block
697
+ if (step.afterBuildup) {
698
+ output.push(step.afterBuildup);
699
+ output.push('');
700
+ }
701
+
702
+ // Add speaker notes
703
+ if (slide.notes) {
704
+ output.push('::: notes');
705
+ output.push(slide.notes);
706
+ output.push(':::');
707
+ }
708
+
709
+ output.push('');
710
+ output.push('---');
711
+ output.push('');
712
+ }
713
+ } else {
714
+ // Regular step - no buildup
715
+ const classes = buildPptxSlideClasses(slide.style);
716
+ const heading = '#'.repeat(slide.titleLevel) + ' ' + slide.title + classes;
717
+ output.push(heading);
718
+ output.push('');
719
+
720
+ // Add preamble if present
721
+ if (slide.preamble) {
722
+ output.push(slide.preamble);
723
+ output.push('');
724
+ }
725
+
726
+ // Add cumulative steps up to current
727
+ for (let j = 0; j <= i; j++) {
728
+ const s = stepsWithBuildup[j];
729
+ if (s) {
730
+ output.push(s.content);
731
+ output.push('');
732
+ }
733
+ }
734
+
735
+ // Add speaker notes
736
+ if (slide.notes) {
737
+ output.push('::: notes');
738
+ output.push(slide.notes);
739
+ output.push(':::');
740
+ }
741
+
742
+ output.push('');
743
+ output.push('---');
744
+ output.push('');
745
+ }
746
+ }
747
+ }
748
+
749
+ // Remove trailing ---
750
+ while (output.length > 0) {
751
+ const last = output[output.length - 1];
752
+ if (!last || last.trim() !== '') break;
753
+ output.pop();
754
+ }
755
+ if (output.length > 0 && output[output.length - 1] === '---') {
756
+ output.pop();
757
+ }
758
+
759
+ return output.join('\n');
760
+ }
761
+
762
+ /**
763
+ * Process markdown for slide output format
764
+ */
765
+ export function processSlideMarkdown(markdown: string, format: 'beamer' | 'pptx'): string {
766
+ const slides = parseSlides(markdown);
767
+
768
+ if (format === 'beamer') {
769
+ return generateBeamerMarkdown(slides);
770
+ } else if (format === 'pptx') {
771
+ return generatePptxMarkdown(slides);
772
+ }
773
+
774
+ return markdown;
775
+ }
776
+
777
+ /**
778
+ * Check if markdown contains slide syntax (steps, notes, buildup, or slide styles)
779
+ */
780
+ export function hasSlideSyntax(markdown: string): boolean {
781
+ // Check for ::: step, ::: notes, or ::: buildup
782
+ if (/^:::\s*(step|notes|buildup)\s*$/m.test(markdown)) {
783
+ return true;
784
+ }
785
+ // Check for slide style attributes on headings: ## Title {.dark}
786
+ if (/^#{1,6}\s+.+\{[^}]*\.(dark|light|accent|inverse|cover|thanks|section|plain|nonumber|center)/m.test(markdown)) {
787
+ return true;
788
+ }
789
+ return false;
790
+ }
791
+
792
+ // Export style parser for testing
793
+ export { parseSlideStyle };