@erclx/canon 5.25.0 → 5.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +2 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/canvas/SKILL.md +1 -1
  4. package/claude/skills/context-fold/REQUIREMENT.md +6 -2
  5. package/claude/skills/context-fold/SKILL.md +8 -6
  6. package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
  7. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  8. package/claude/skills/design-taste/SKILL.md +4 -2
  9. package/claude/skills/design-taste/references/adopted.md +40 -0
  10. package/claude/skills/design-taste/references/kinds.md +1 -1
  11. package/claude/skills/design-taste/references/systems.md +1 -7
  12. package/claude/skills/design-taste/references/tells.md +2 -24
  13. package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
  14. package/claude/skills/document-health/REQUIREMENT.md +2 -0
  15. package/claude/skills/document-health/SKILL.md +21 -0
  16. package/claude/skills/document-health/references/axes.md +12 -0
  17. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  18. package/claude/skills/draft-identity/SKILL.md +1 -1
  19. package/claude/skills/draft-screencast/SKILL.md +9 -7
  20. package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
  21. package/claude/skills/draft-slides/SKILL.md +31 -20
  22. package/claude/skills/git-issue/references/issue.md +10 -0
  23. package/claude/skills/git-pr/references/evidence.md +8 -0
  24. package/claude/skills/git-pr/references/labels.md +12 -0
  25. package/claude/skills/git-split/SKILL.md +19 -8
  26. package/claude/skills/markdown-propose/references/format.md +9 -0
  27. package/claude/skills/memory-review/references/later-phases.md +7 -0
  28. package/claude/skills/review-ui/references/post.md +4 -2
  29. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
  30. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
  31. package/claude/skills/search-craft/REQUIREMENT.md +1 -1
  32. package/claude/skills/search-craft/SKILL.md +3 -1
  33. package/claude/skills/search-craft/references/adopted.md +30 -0
  34. package/claude/skills/target-setup/references/detect.md +8 -0
  35. package/claude/skills/target-setup/references/indexes.md +13 -0
  36. package/claude/skills/target-setup/references/verify.md +8 -0
  37. package/claude/skills/test-first/SKILL.md +2 -0
  38. package/claude/skills/test-first/references/adopted.md +36 -0
  39. package/claude/skills/write-human/SKILL.md +1 -1
  40. package/claude/skills/write-human/references/machine-tells.md +13 -0
  41. package/docs/agents/canvas.md +3 -1
  42. package/docs/agents/capture.md +1 -1
  43. package/docs/agents/commands.md +3 -3
  44. package/docs/agents/index.md +2 -2
  45. package/docs/agents/records-stale.md +22 -9
  46. package/docs/agents/records.md +1 -1
  47. package/docs/agents/sandbox.md +1 -1
  48. package/docs/agents/skills-audit.md +10 -3
  49. package/docs/agents/slides.md +15 -3
  50. package/docs/workflow/ai-workflow.md +1 -1
  51. package/docs/workflow/index.md +1 -0
  52. package/docs/workflow/parallel-features.md +1 -1
  53. package/docs/workflow/skill-map-on-request.md +42 -0
  54. package/docs/workflow/skill-map.md +1 -34
  55. package/package.json +1 -1
  56. package/scripts/lib/sandbox-dispatch.sh +1 -1
  57. package/scripts/lib/sandbox-git.sh +1 -9
  58. package/scripts/lib/sandbox-path.sh +1 -1
  59. package/scripts/sandbox-hook.sh +99 -0
  60. package/src/canvas/capture.ts +1 -9
  61. package/src/canvas/client/inspector/color-field.tsx +368 -0
  62. package/src/canvas/client/inspector/fields.css +163 -22
  63. package/src/canvas/client/inspector/values.ts +123 -0
  64. package/src/canvas/client/inspector.tsx +46 -69
  65. package/src/capture/render.ts +45 -7
  66. package/src/capture/sources.ts +43 -0
  67. package/src/census/count.ts +1 -1
  68. package/src/claude/cases/workflow.ts +5 -0
  69. package/src/claude/seeds.ts +1 -1
  70. package/src/claude/skills-audit.ts +68 -14
  71. package/src/client-commands.ts +1 -1
  72. package/src/commands/claude.ts +27 -2
  73. package/src/commands/feedback.ts +3 -3
  74. package/src/commands/records.ts +88 -6
  75. package/src/commands/sandbox.ts +50 -10
  76. package/src/commands/slides.ts +31 -77
  77. package/src/context/audit.ts +1 -1
  78. package/src/design/tokens.ts +2 -18
  79. package/src/docs/read.ts +1 -1
  80. package/src/{github.ts → feedback/github.ts} +1 -1
  81. package/src/gate/measures.ts +6 -4
  82. package/src/gov/citations.ts +1 -1
  83. package/src/gov/payload.ts +1 -1
  84. package/src/gov/superseded.ts +2 -2
  85. package/src/markdown/ceiling.ts +1 -1
  86. package/src/record-root.ts +1 -0
  87. package/src/records/backup.ts +1 -0
  88. package/src/records/push-guard.ts +1 -1
  89. package/src/records/stale-canonical.ts +146 -0
  90. package/src/records/stale.ts +1 -1
  91. package/src/sandbox/hooks.ts +128 -0
  92. package/src/sandbox/provision.ts +596 -0
  93. package/src/sandbox/tree.ts +103 -4
  94. package/src/secrets/marker.ts +2 -2
  95. package/src/secrets/scan.ts +1 -1
  96. package/src/shipped/references.ts +2 -2
  97. package/src/slides/convert/export.ts +2 -16
  98. package/src/slides/decks.ts +110 -0
  99. package/src/standards/read.ts +1 -1
  100. package/src/sync/seeds-report.ts +1 -1
  101. package/src/tasks/answers.ts +1 -1
  102. package/src/tasks/archive.ts +1 -1
  103. package/src/teach/nav.ts +1 -1
  104. package/src/tooling/inject.ts +1 -1
  105. package/src/web/readme-citations.ts +1 -1
  106. package/standards/architecture.md +1 -0
  107. package/standards/canvas.md +1 -1
  108. package/standards/requirements.md +2 -0
  109. package/standards/skill-practice.md +2 -1
  110. package/standards/skill.md +1 -0
  111. package/scripts/manage-sandbox.sh +0 -472
  112. package/src/slides/layouts.ts +0 -478
  113. package/src/slides/parse.ts +0 -84
  114. package/src/slides/render.ts +0 -110
  115. package/src/slides/styles.ts +0 -96
  116. /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
  117. /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
  118. /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
  119. /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
  120. /package/src/{binary.ts → sweep/binary.ts} +0 -0
  121. /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
  122. /package/src/{paths.ts → tasks/paths.ts} +0 -0
  123. /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
@@ -1,478 +0,0 @@
1
- import type PptxGenJS from 'pptxgenjs'
2
- import type { Slide } from '@/slides/parse'
3
- import { FONTS, type Theme, TYPE } from '@/slides/styles'
4
-
5
- type Deck = InstanceType<typeof PptxGenJS>
6
- type PptxSlide = ReturnType<Deck['addSlide']>
7
-
8
- export interface LayoutInfo {
9
- name: string
10
- description: string
11
- }
12
-
13
- export interface NavEntry {
14
- title: string
15
- slideNumber: number
16
- }
17
-
18
- export interface DeckNav {
19
- deckTitle: string
20
- tocSlide?: number
21
- sections: NavEntry[]
22
- }
23
-
24
- export const LAYOUTS: LayoutInfo[] = [
25
- { name: 'title', description: 'Cover slide with deck title and subtitle' },
26
- { name: 'toc', description: 'Contents slide linking to each section' },
27
- { name: 'section', description: 'Section divider with one large heading' },
28
- { name: 'bullets', description: 'Title above a bullet list' },
29
- { name: 'two-column', description: 'Two labeled columns of bullets' },
30
- { name: 'stat-callout', description: 'Row of large stats with captions' },
31
- { name: 'grid', description: 'Two-by-two cards, each a term and detail' },
32
- { name: 'quote', description: 'Large pull quote with attribution' },
33
- {
34
- name: 'freeform',
35
- description:
36
- 'Explicit shapes at a declared position and size, for a figure the other layouts cannot draw',
37
- },
38
- ]
39
-
40
- const LAYOUT_NAMES = new Set(LAYOUTS.map((layout) => layout.name))
41
-
42
- export function isKnownLayout(name: string): boolean {
43
- return LAYOUT_NAMES.has(name)
44
- }
45
-
46
- const W = 13.333
47
- const H = 7.5
48
- const MX = 0.7
49
- const CW = W - MX * 2
50
- const BODY_Y = 1.7
51
-
52
- const RENDERERS: Record<
53
- string,
54
- (s: PptxSlide, slide: Slide, theme: Theme) => void
55
- > = {
56
- title: renderTitle,
57
- section: renderSection,
58
- bullets: renderBullets,
59
- 'two-column': renderTwoColumn,
60
- 'stat-callout': renderStatCallout,
61
- grid: renderGrid,
62
- quote: renderQuote,
63
- freeform: renderFreeform,
64
- }
65
-
66
- export function renderDeckSlide(
67
- s: PptxSlide,
68
- slide: Slide,
69
- theme: Theme,
70
- ): void {
71
- const renderer = RENDERERS[slide.layout] ?? renderBullets
72
- renderer(s, slide, theme)
73
- }
74
-
75
- export function renderToc(
76
- s: PptxSlide,
77
- slide: Slide,
78
- theme: Theme,
79
- nav: DeckNav,
80
- ): void {
81
- addTitle(s, slide.title || 'Contents', theme)
82
- if (nav.sections.length === 0) return
83
- s.addText(
84
- nav.sections.map((entry) => ({
85
- text: entry.title,
86
- options: { breakLine: true, hyperlink: { slide: entry.slideNumber } },
87
- })),
88
- {
89
- x: MX,
90
- y: BODY_Y,
91
- w: CW,
92
- h: 5,
93
- fontFace: FONTS.body,
94
- fontSize: TYPE.body + 2,
95
- color: theme.accent,
96
- valign: 'top',
97
- lineSpacingMultiple: 1.6,
98
- },
99
- )
100
- }
101
-
102
- export function addFooter(
103
- s: PptxSlide,
104
- slide: Slide,
105
- theme: Theme,
106
- nav: DeckNav,
107
- ): void {
108
- if (slide.layout === 'title' || slide.layout === 'toc') return
109
- if (nav.deckTitle) {
110
- s.addText(nav.deckTitle, {
111
- x: MX,
112
- y: H - 0.5,
113
- w: CW / 2,
114
- h: 0.3,
115
- fontFace: FONTS.body,
116
- fontSize: TYPE.caption,
117
- color: theme.muted,
118
- valign: 'middle',
119
- })
120
- }
121
- if (nav.tocSlide) {
122
- s.addText('Contents', {
123
- x: MX + CW / 2,
124
- y: H - 0.5,
125
- w: CW / 2,
126
- h: 0.3,
127
- fontFace: FONTS.body,
128
- fontSize: TYPE.caption,
129
- color: theme.muted,
130
- align: 'right',
131
- valign: 'middle',
132
- hyperlink: { slide: nav.tocSlide },
133
- })
134
- }
135
- }
136
-
137
- function renderTitle(s: PptxSlide, slide: Slide, theme: Theme): void {
138
- s.addText(clean(slide.title), {
139
- x: MX,
140
- y: 2.6,
141
- w: CW,
142
- h: 1.4,
143
- fontFace: FONTS.heading,
144
- fontSize: TYPE.cover,
145
- bold: true,
146
- color: theme.ink,
147
- valign: 'top',
148
- })
149
- const subtitle = slide.content.join(' ').trim()
150
- if (subtitle) {
151
- s.addText(clean(subtitle), {
152
- x: MX,
153
- y: 4.1,
154
- w: CW,
155
- h: 1,
156
- fontFace: FONTS.body,
157
- fontSize: TYPE.body + 4,
158
- color: theme.muted,
159
- valign: 'top',
160
- })
161
- }
162
- }
163
-
164
- function renderSection(s: PptxSlide, slide: Slide, theme: Theme): void {
165
- s.addText(clean(slide.title), {
166
- x: MX,
167
- y: 2.9,
168
- w: CW,
169
- h: 1.6,
170
- fontFace: FONTS.heading,
171
- fontSize: TYPE.section,
172
- bold: true,
173
- color: theme.accent,
174
- valign: 'middle',
175
- })
176
- }
177
-
178
- function renderBullets(s: PptxSlide, slide: Slide, theme: Theme): void {
179
- addTitle(s, slide.title, theme)
180
- const items = bullets(slide.content)
181
- if (items.length === 0) return
182
- s.addText(
183
- items.map((text) => ({
184
- text,
185
- options: { bullet: { code: '2022' }, breakLine: true },
186
- })),
187
- {
188
- x: MX,
189
- y: BODY_Y,
190
- w: CW,
191
- h: 5,
192
- fontFace: FONTS.body,
193
- fontSize: TYPE.body,
194
- color: theme.ink,
195
- valign: 'top',
196
- lineSpacingMultiple: 1.3,
197
- paraSpaceAfter: 10,
198
- },
199
- )
200
- }
201
-
202
- function renderTwoColumn(s: PptxSlide, slide: Slide, theme: Theme): void {
203
- addTitle(s, slide.title, theme)
204
- const cols = columns(slide.content)
205
- const gap = 0.6
206
- const colWidth = (CW - gap) / 2
207
- cols.slice(0, 2).forEach((col, index) => {
208
- const x = MX + index * (colWidth + gap)
209
- s.addText(col.heading, {
210
- x,
211
- y: BODY_Y,
212
- w: colWidth,
213
- h: 0.6,
214
- fontFace: FONTS.heading,
215
- fontSize: TYPE.body + 4,
216
- bold: true,
217
- color: theme.accent,
218
- valign: 'top',
219
- })
220
- if (col.items.length === 0) return
221
- s.addText(
222
- col.items.map((text) => ({
223
- text,
224
- options: { bullet: { code: '2022' }, breakLine: true },
225
- })),
226
- {
227
- x,
228
- y: BODY_Y + 0.7,
229
- w: colWidth,
230
- h: 4.3,
231
- fontFace: FONTS.body,
232
- fontSize: TYPE.body,
233
- color: theme.ink,
234
- valign: 'top',
235
- lineSpacingMultiple: 1.3,
236
- paraSpaceAfter: 8,
237
- },
238
- )
239
- })
240
- }
241
-
242
- function renderStatCallout(s: PptxSlide, slide: Slide, theme: Theme): void {
243
- addTitle(s, slide.title, theme)
244
- const stats = pairs(slide.content).slice(0, 4)
245
- if (stats.length === 0) return
246
- const gap = 0.5
247
- const cellWidth = (CW - gap * (stats.length - 1)) / stats.length
248
- stats.forEach((stat, index) => {
249
- const x = MX + index * (cellWidth + gap)
250
- s.addText(stat.left, {
251
- x,
252
- y: 2.6,
253
- w: cellWidth,
254
- h: 1.3,
255
- fontFace: FONTS.heading,
256
- fontSize: TYPE.stat,
257
- bold: true,
258
- color: theme.accent,
259
- align: 'left',
260
- valign: 'top',
261
- })
262
- s.addText(stat.right, {
263
- x,
264
- y: 3.9,
265
- w: cellWidth,
266
- h: 1.4,
267
- fontFace: FONTS.body,
268
- fontSize: TYPE.body,
269
- color: theme.muted,
270
- align: 'left',
271
- valign: 'top',
272
- })
273
- })
274
- }
275
-
276
- function renderGrid(s: PptxSlide, slide: Slide, theme: Theme): void {
277
- addTitle(s, slide.title, theme)
278
- const cards = pairs(slide.content).slice(0, 4)
279
- const gap = 0.5
280
- const cardWidth = (CW - gap) / 2
281
- const cardHeight = 2.1
282
- cards.forEach((card, index) => {
283
- const col = index % 2
284
- const row = Math.floor(index / 2)
285
- const x = MX + col * (cardWidth + gap)
286
- const y = BODY_Y + row * (cardHeight + gap)
287
- s.addShape('roundRect', {
288
- x,
289
- y,
290
- w: cardWidth,
291
- h: cardHeight,
292
- fill: { color: theme.surface },
293
- line: { type: 'none' },
294
- rectRadius: 0.08,
295
- })
296
- s.addText(card.left, {
297
- x: x + 0.3,
298
- y: y + 0.25,
299
- w: cardWidth - 0.6,
300
- h: 0.6,
301
- fontFace: FONTS.heading,
302
- fontSize: TYPE.body + 4,
303
- bold: true,
304
- color: theme.ink,
305
- valign: 'top',
306
- })
307
- s.addText(card.right, {
308
- x: x + 0.3,
309
- y: y + 0.95,
310
- w: cardWidth - 0.6,
311
- h: cardHeight - 1.2,
312
- fontFace: FONTS.body,
313
- fontSize: TYPE.body - 2,
314
- color: theme.ink,
315
- valign: 'top',
316
- lineSpacingMultiple: 1.2,
317
- })
318
- })
319
- }
320
-
321
- function renderQuote(s: PptxSlide, slide: Slide, theme: Theme): void {
322
- const lines = slide.content
323
- const attribution = lines.find((line) => /^(—|-)\s/.test(line.trim()))
324
- const quote = lines
325
- .filter((line) => line !== attribution)
326
- .join(' ')
327
- .trim()
328
- s.addText(clean(quote), {
329
- x: MX,
330
- y: 2.2,
331
- w: CW,
332
- h: 2.6,
333
- fontFace: FONTS.heading,
334
- fontSize: TYPE.quote,
335
- italic: true,
336
- color: theme.ink,
337
- valign: 'top',
338
- lineSpacingMultiple: 1.25,
339
- })
340
- if (attribution) {
341
- s.addText(clean(attribution.replace(/^(—|-)\s+/, '')), {
342
- x: MX,
343
- y: 5,
344
- w: CW,
345
- h: 0.6,
346
- fontFace: FONTS.body,
347
- fontSize: TYPE.body,
348
- bold: true,
349
- color: theme.accent,
350
- valign: 'top',
351
- })
352
- }
353
- }
354
-
355
- const SHAPE_KEYS = new Set(['x', 'y', 'w', 'h', 'color'])
356
- const COLOR_ROLES: (keyof Theme)[] = [
357
- 'background',
358
- 'surface',
359
- 'ink',
360
- 'muted',
361
- 'accent',
362
- ]
363
-
364
- function renderFreeform(s: PptxSlide, slide: Slide, theme: Theme): void {
365
- addTitle(s, slide.title, theme)
366
- for (const line of slide.content) {
367
- if (!line.trim().startsWith('-')) throw malformedShape(slide.title, line)
368
- renderShapeLine(s, line, slide.title, theme)
369
- }
370
- }
371
-
372
- function malformedShape(title: string, line: string): Error {
373
- return new Error(
374
- `Malformed freeform shape on slide "${title}": ${line.trim()}`,
375
- )
376
- }
377
-
378
- function renderShapeLine(
379
- s: PptxSlide,
380
- line: string,
381
- title: string,
382
- theme: Theme,
383
- ): void {
384
- const stripped = line.replace(/^\s*-\s*/, '')
385
- const colonIndex = stripped.indexOf(':')
386
- const head = (
387
- colonIndex === -1 ? stripped : stripped.slice(0, colonIndex)
388
- ).trim()
389
- const text = colonIndex === -1 ? '' : stripped.slice(colonIndex + 1).trim()
390
-
391
- const tokens = head.split(/\s+/).filter(Boolean)
392
- const kind = tokens.shift()
393
- if (kind !== 'text' && kind !== 'rect') throw malformedShape(title, line)
394
- if (kind === 'text' && colonIndex === -1) throw malformedShape(title, line)
395
-
396
- const attrs: Record<string, string> = {}
397
- for (const token of tokens) {
398
- const match = token.match(/^(\w+)=(.+)$/)
399
- if (!match) throw malformedShape(title, line)
400
- if (!SHAPE_KEYS.has(match[1])) throw malformedShape(title, line)
401
- attrs[match[1]] = match[2]
402
- }
403
-
404
- const x = Number(attrs.x)
405
- const y = Number(attrs.y)
406
- const w = Number(attrs.w)
407
- const h = Number(attrs.h)
408
- if (![x, y, w, h].every(Number.isFinite)) throw malformedShape(title, line)
409
-
410
- const role = attrs.color as keyof Theme
411
- if (!COLOR_ROLES.includes(role)) throw malformedShape(title, line)
412
- const color = theme[role]
413
-
414
- if (kind === 'text') {
415
- s.addText(clean(text), {
416
- x,
417
- y,
418
- w,
419
- h,
420
- fontFace: FONTS.body,
421
- fontSize: TYPE.body,
422
- color,
423
- valign: 'top',
424
- })
425
- } else {
426
- s.addShape('rect', { x, y, w, h, fill: { color } })
427
- }
428
- }
429
-
430
- function addTitle(s: PptxSlide, title: string, theme: Theme): void {
431
- if (!title) return
432
- s.addText(clean(title), {
433
- x: MX,
434
- y: 0.5,
435
- w: CW,
436
- h: 0.9,
437
- fontFace: FONTS.heading,
438
- fontSize: TYPE.title,
439
- bold: true,
440
- color: theme.ink,
441
- valign: 'top',
442
- })
443
- }
444
-
445
- function clean(text: string): string {
446
- return text.replace(/\*\*/g, '').replace(/`/g, '').trim()
447
- }
448
-
449
- function bullets(content: string[]): string[] {
450
- return content
451
- .filter((line) => line.trim().startsWith('-'))
452
- .map((line) => clean(line.replace(/^\s*-\s+/, '')))
453
- }
454
-
455
- function pairs(content: string[]): { left: string; right: string }[] {
456
- return content
457
- .filter((line) => line.trim().startsWith('-'))
458
- .map((line) => line.replace(/^\s*-\s+/, ''))
459
- .map((line) => {
460
- const [left, ...rest] = line.split(/\s*:\s*/)
461
- return { left: clean(left), right: clean(rest.join(': ')) }
462
- })
463
- }
464
-
465
- function columns(content: string[]): { heading: string; items: string[] }[] {
466
- const cols: { heading: string; items: string[] }[] = []
467
- for (const line of content) {
468
- const heading = line.match(/^##\s+(.+)/)
469
- if (heading) {
470
- cols.push({ heading: clean(heading[1]), items: [] })
471
- continue
472
- }
473
- if (line.trim().startsWith('-') && cols.length > 0) {
474
- cols[cols.length - 1].items.push(clean(line.replace(/^\s*-\s+/, '')))
475
- }
476
- }
477
- return cols
478
- }
@@ -1,84 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import type { Variant } from '@/slides/styles'
3
-
4
- export interface DeckMeta {
5
- title: string
6
- variant: Variant
7
- }
8
-
9
- export interface Slide {
10
- layout: string
11
- title: string
12
- content: string[]
13
- }
14
-
15
- export interface Deck {
16
- meta: DeckMeta
17
- slides: Slide[]
18
- }
19
-
20
- const DEFAULT_META: DeckMeta = {
21
- title: '',
22
- variant: 'light',
23
- }
24
-
25
- export function parseSlidesDoc(path: string): Deck {
26
- return parseSlides(readFileSync(path, 'utf8'))
27
- }
28
-
29
- export function parseSlides(raw: string): Deck {
30
- const { meta, body } = splitFrontmatter(raw)
31
- const slides = body
32
- .split(/^---\s*$/m)
33
- .map(parseSlide)
34
- .filter((slide): slide is Slide => slide !== null)
35
- return { meta, slides }
36
- }
37
-
38
- function splitFrontmatter(raw: string): { meta: DeckMeta; body: string } {
39
- const match = raw.match(/^---\n([\s\S]*?)\n---\n?/)
40
- if (!match) return { meta: { ...DEFAULT_META }, body: raw }
41
- return { meta: parseMeta(match[1]), body: raw.slice(match[0].length) }
42
- }
43
-
44
- function parseMeta(block: string): DeckMeta {
45
- const meta: DeckMeta = { ...DEFAULT_META }
46
- for (const line of block.split('\n')) {
47
- const match = line.match(/^(\w+):\s*(.+?)\s*$/)
48
- if (!match) continue
49
- const [, key, value] = match
50
- if (key === 'title') meta.title = value
51
- if (key === 'variant' && (value === 'light' || value === 'dark')) {
52
- meta.variant = value
53
- }
54
- }
55
- return meta
56
- }
57
-
58
- function parseSlide(chunk: string): Slide | null {
59
- let title = ''
60
- let layout = ''
61
- const content: string[] = []
62
- for (const line of chunk.split('\n')) {
63
- const heading = line.match(/^#\s+(.+?)\s*$/)
64
- const declared = line.match(/^layout:\s*(.+?)\s*$/)
65
- if (heading && !title) {
66
- title = heading[1]
67
- continue
68
- }
69
- if (declared && !layout) {
70
- layout = declared[1].trim()
71
- continue
72
- }
73
- content.push(line)
74
- }
75
- const trimmed = content.filter((line) => line.trim())
76
- if (!title && !layout && trimmed.length === 0) return null
77
- return { layout: layout || inferLayout(trimmed), title, content: trimmed }
78
- }
79
-
80
- function inferLayout(content: string[]): string {
81
- return content.some((line) => line.trim().startsWith('-'))
82
- ? 'bullets'
83
- : 'title'
84
- }
@@ -1,110 +0,0 @@
1
- import { copyFileSync, mkdirSync } from 'node:fs'
2
- import { basename, join } from 'node:path'
3
- import PptxGenJS from 'pptxgenjs'
4
- import {
5
- addFooter,
6
- type DeckNav,
7
- isKnownLayout,
8
- renderDeckSlide,
9
- renderToc,
10
- } from '@/slides/layouts'
11
- import { type Deck, parseSlidesDoc } from '@/slides/parse'
12
- import { buildTheme, type Variant } from '@/slides/styles'
13
-
14
- export interface UnrecognizedLayout {
15
- value: string
16
- slideNumbers: number[]
17
- }
18
-
19
- export interface RenderResult {
20
- pptxPath: string
21
- mirrorPath?: string
22
- slideCount: number
23
- unrecognizedLayouts: UnrecognizedLayout[]
24
- }
25
-
26
- export interface RenderOptions {
27
- variant?: Variant
28
- mirror?: string
29
- }
30
-
31
- export async function renderSlidesDoc(
32
- sourcePath: string,
33
- outDir: string,
34
- options: RenderOptions = {},
35
- ): Promise<RenderResult> {
36
- const deck = parseSlidesDoc(sourcePath)
37
- const variant = options.variant ?? deck.meta.variant
38
- const theme = buildTheme(variant)
39
-
40
- const pptx = new PptxGenJS()
41
- pptx.layout = 'LAYOUT_WIDE'
42
- if (deck.meta.title) pptx.title = deck.meta.title
43
-
44
- const nav = buildNav(deck)
45
- const unrecognizedSlideNumbers = new Map<string, number[]>()
46
- deck.slides.forEach((slide, index) => {
47
- const target = pptx.addSlide()
48
- target.background = { color: theme.background }
49
- if (!isKnownLayout(slide.layout)) {
50
- const slideNumbers = unrecognizedSlideNumbers.get(slide.layout) ?? []
51
- slideNumbers.push(index + 1)
52
- unrecognizedSlideNumbers.set(slide.layout, slideNumbers)
53
- }
54
- if (slide.layout === 'toc') {
55
- renderToc(target, slide, theme, nav)
56
- } else {
57
- renderDeckSlide(target, slide, theme)
58
- }
59
- addFooter(target, slide, theme, nav)
60
- })
61
-
62
- mkdirSync(outDir, { recursive: true })
63
- const fileName = outputName(sourcePath)
64
- const pptxPath = join(outDir, fileName)
65
- await pptx.writeFile({ fileName: pptxPath })
66
-
67
- const mirrorPath = options.mirror
68
- ? mirrorDeck(pptxPath, options.mirror, fileName)
69
- : undefined
70
- const unrecognizedLayouts = Array.from(
71
- unrecognizedSlideNumbers,
72
- ([value, slideNumbers]) => ({ value, slideNumbers }),
73
- )
74
- return {
75
- pptxPath,
76
- mirrorPath,
77
- slideCount: deck.slides.length,
78
- unrecognizedLayouts,
79
- }
80
- }
81
-
82
- function mirrorDeck(
83
- pptxPath: string,
84
- mirrorDir: string,
85
- fileName: string,
86
- ): string {
87
- mkdirSync(mirrorDir, { recursive: true })
88
- const mirrorPath = join(mirrorDir, fileName)
89
- copyFileSync(pptxPath, mirrorPath)
90
- return mirrorPath
91
- }
92
-
93
- export function buildNav(deck: Deck): DeckNav {
94
- let tocSlide: number | undefined
95
- const sections: DeckNav['sections'] = []
96
- deck.slides.forEach((slide, index) => {
97
- const slideNumber = index + 1
98
- if (slide.layout === 'toc' && tocSlide === undefined) {
99
- tocSlide = slideNumber
100
- }
101
- if (slide.layout === 'section') {
102
- sections.push({ title: slide.title, slideNumber })
103
- }
104
- })
105
- return { deckTitle: deck.meta.title, tocSlide, sections }
106
- }
107
-
108
- function outputName(sourcePath: string): string {
109
- return basename(sourcePath).replace(/\.md$/, '') + '.pptx'
110
- }