@erclx/canon 5.52.0 → 5.54.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 (201) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +11 -13
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -8
  5. package/claude/skills/context-fold/REQUIREMENT.md +0 -1
  6. package/claude/skills/design-identity/REQUIREMENT.md +2 -2
  7. package/claude/skills/design-identity/SKILL.md +6 -6
  8. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  9. package/claude/skills/design-record/SKILL.md +4 -4
  10. package/claude/skills/design-record/references/folder.md +1 -1
  11. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  12. package/claude/skills/design-taste/references/kinds.md +0 -1
  13. package/claude/skills/draw-figure/REQUIREMENT.md +3 -4
  14. package/claude/skills/draw-figure/SKILL.md +5 -6
  15. package/claude/skills/draw-figure/references/figures.md +0 -1
  16. package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
  17. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -5
  18. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  19. package/claude/skills/pick-by-eye/references/board.md +42 -0
  20. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  21. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  22. package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
  23. package/claude/skills/teach-craft/SKILL.md +70 -0
  24. package/claude/skills/teach-craft/references/adopted.md +113 -0
  25. package/claude/skills/teach-craft/references/lesson-craft.md +60 -0
  26. package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +41 -6
  27. package/docs/agents/audits.md +0 -1
  28. package/docs/agents/install-and-sync.md +0 -8
  29. package/docs/target/migrations.md +16 -2
  30. package/docs/target/projects.md +1 -1
  31. package/docs/workflow/parallel-features.md +1 -1
  32. package/docs/workflow/skill-map-on-request.md +10 -22
  33. package/docs/workflow/skill-map.md +11 -14
  34. package/docs/workflow/visual-design-workflow.md +1 -4
  35. package/governance/detect.toml +1 -6
  36. package/governance/renames.toml +0 -1
  37. package/package.json +2 -8
  38. package/scripts/core/regen-web-previews.ts +2 -67
  39. package/src/audits/catalog.ts +0 -1
  40. package/src/browser/engine.ts +3 -6
  41. package/src/capture/render.ts +1 -1
  42. package/src/claude/cases/authoring.ts +0 -12
  43. package/src/claude/cases/misc.ts +0 -22
  44. package/src/claude/cases/workflow.ts +2 -38
  45. package/src/claude/skills-audit.ts +1 -1
  46. package/src/cli/help.ts +0 -10
  47. package/src/cli.ts +0 -8
  48. package/src/commands/design.ts +16 -315
  49. package/src/commands/records.ts +0 -1
  50. package/src/commands/serve.ts +2 -2
  51. package/src/commands/sync.ts +4 -5
  52. package/src/design/components.ts +8 -1514
  53. package/src/design/css.ts +5 -6
  54. package/src/design/fonts.ts +3 -5
  55. package/src/design/regen.ts +5 -32
  56. package/src/design/render.ts +1 -1
  57. package/src/design/tokens.ts +3 -2
  58. package/src/driver/drive.ts +3 -4
  59. package/src/driver/steps.ts +2 -5
  60. package/src/gate/stages.ts +4 -10
  61. package/src/gov/audit.ts +1 -6
  62. package/src/inventory/walk.ts +1 -1
  63. package/src/markdown/scope.ts +6 -35
  64. package/src/records/validate.ts +4 -176
  65. package/src/sandbox/equivalence.ts +0 -5
  66. package/src/shipped/references.ts +1 -2
  67. package/src/sync/check.ts +4 -29
  68. package/src/sync/stamp.ts +6 -6
  69. package/src/sync/target.ts +1 -4
  70. package/tooling/claude/reference.md +1 -1
  71. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  72. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  73. package/claude/skills/canvas-board/SKILL.md +0 -143
  74. package/claude/skills/canvas-board/references/canvas.md +0 -170
  75. package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
  76. package/claude/skills/draft-and-pick/SKILL.md +0 -11
  77. package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
  78. package/claude/skills/draft-doc/SKILL.md +0 -11
  79. package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
  80. package/claude/skills/draft-figure/SKILL.md +0 -11
  81. package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
  82. package/claude/skills/draft-identity/SKILL.md +0 -11
  83. package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
  84. package/claude/skills/draft-ready/SKILL.md +0 -11
  85. package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
  86. package/claude/skills/draft-screencast/SKILL.md +0 -11
  87. package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
  88. package/claude/skills/draft-slides/SKILL.md +0 -11
  89. package/claude/skills/make-deck/REQUIREMENT.md +0 -36
  90. package/claude/skills/make-deck/SKILL.md +0 -80
  91. package/claude/skills/make-deck/references/deck.md +0 -183
  92. package/claude/skills/read-frames/REQUIREMENT.md +0 -30
  93. package/claude/skills/read-frames/SKILL.md +0 -55
  94. package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
  95. package/claude/skills/record-screencast/SKILL.md +0 -99
  96. package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
  97. package/claude/skills/script-screencast/SKILL.md +0 -70
  98. package/claude/skills/script-screencast/references/beats-template.md +0 -119
  99. package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
  100. package/claude/skills/teach-workspace/SKILL.md +0 -196
  101. package/claude/skills/teach-workspace/references/glossary.md +0 -90
  102. package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
  103. package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
  104. package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
  105. package/claude/skills/teach-workspace/references/promotion.md +0 -116
  106. package/claude/skills/teach-workspace/references/revisit.md +0 -28
  107. package/claude/skills/teach-workspace/references/teach.md +0 -211
  108. package/claude/skills/video-craft/REQUIREMENT.md +0 -35
  109. package/claude/skills/video-craft/SKILL.md +0 -96
  110. package/claude/skills/video-craft/references/adopted.md +0 -110
  111. package/governance/rules/ui/canvas.md +0 -13
  112. package/src/canvas/address.ts +0 -227
  113. package/src/canvas/arrange.ts +0 -66
  114. package/src/canvas/capture.ts +0 -391
  115. package/src/canvas/client/app.tsx +0 -188
  116. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  117. package/src/canvas/client/inspector/color-field.tsx +0 -398
  118. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  119. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  120. package/src/canvas/client/inspector/color-space.ts +0 -240
  121. package/src/canvas/client/inspector/field.tsx +0 -263
  122. package/src/canvas/client/inspector/fields.css +0 -763
  123. package/src/canvas/client/inspector/section.tsx +0 -27
  124. package/src/canvas/client/inspector/size-field.tsx +0 -69
  125. package/src/canvas/client/inspector/token-select.tsx +0 -109
  126. package/src/canvas/client/inspector/values.ts +0 -297
  127. package/src/canvas/client/inspector.tsx +0 -692
  128. package/src/canvas/client/keys.ts +0 -71
  129. package/src/canvas/client/layers.tsx +0 -118
  130. package/src/canvas/client/main.tsx +0 -71
  131. package/src/canvas/client/pages-panel.tsx +0 -212
  132. package/src/canvas/client/panel-handle.tsx +0 -90
  133. package/src/canvas/client/selection.tsx +0 -199
  134. package/src/canvas/client/shell.css +0 -762
  135. package/src/canvas/client/state.ts +0 -792
  136. package/src/canvas/client/surface.tsx +0 -803
  137. package/src/canvas/client/theme-panel.tsx +0 -55
  138. package/src/canvas/client/tools.tsx +0 -153
  139. package/src/canvas/content.ts +0 -781
  140. package/src/canvas/edit.ts +0 -575
  141. package/src/canvas/editing.ts +0 -203
  142. package/src/canvas/fit.ts +0 -238
  143. package/src/canvas/global.d.ts +0 -15
  144. package/src/canvas/history.ts +0 -116
  145. package/src/canvas/index.html +0 -20
  146. package/src/canvas/server.ts +0 -766
  147. package/src/canvas/tokens.ts +0 -221
  148. package/src/commands/canvas.ts +0 -1097
  149. package/src/commands/demo.ts +0 -542
  150. package/src/commands/slides.ts +0 -184
  151. package/src/commands/teach.ts +0 -1401
  152. package/src/demo/beats.ts +0 -135
  153. package/src/demo/compile.ts +0 -350
  154. package/src/demo/container.ts +0 -219
  155. package/src/demo/cursors.ts +0 -55
  156. package/src/demo/drive.ts +0 -442
  157. package/src/demo/pointer.ts +0 -178
  158. package/src/demo/theme.ts +0 -112
  159. package/src/demo/timeline.ts +0 -64
  160. package/src/design/adapter.ts +0 -59
  161. package/src/design/base.css +0 -203
  162. package/src/design/brand-favicon.ts +0 -18
  163. package/src/design/neutral.ts +0 -127
  164. package/src/design/project-fonts.ts +0 -392
  165. package/src/slides/convert/chart.ts +0 -165
  166. package/src/slides/convert/deck.ts +0 -244
  167. package/src/slides/convert/export.ts +0 -567
  168. package/src/slides/convert/master.ts +0 -198
  169. package/src/slides/convert/shapes.ts +0 -804
  170. package/src/slides/convert/svg.ts +0 -38
  171. package/src/slides/convert/walk.ts +0 -671
  172. package/src/slides/decks.ts +0 -110
  173. package/src/slides/open.ts +0 -18
  174. package/src/slides/package/fonts.ts +0 -301
  175. package/src/slides/package/motion.ts +0 -316
  176. package/src/slides/package/test-face.ts +0 -113
  177. package/src/teach/browser/compile.ts +0 -44
  178. package/src/teach/browser/dismiss.ts +0 -76
  179. package/src/teach/browser/focus-line.ts +0 -19
  180. package/src/teach/browser/glossary-filter.ts +0 -44
  181. package/src/teach/browser/head.ts +0 -30
  182. package/src/teach/browser/quiz.ts +0 -19
  183. package/src/teach/browser/reload.ts +0 -9
  184. package/src/teach/browser/sidebar.ts +0 -233
  185. package/src/teach/browser/theme.ts +0 -18
  186. package/src/teach/components/code.tsx +0 -24
  187. package/src/teach/components/heading.tsx +0 -11
  188. package/src/teach/components/list.tsx +0 -17
  189. package/src/teach/components/paragraph.tsx +0 -29
  190. package/src/teach/components/refs.tsx +0 -28
  191. package/src/teach/cross-references.ts +0 -78
  192. package/src/teach/fonts.ts +0 -16
  193. package/src/teach/highlight.ts +0 -71
  194. package/src/teach/html/jsx-dev-runtime.ts +0 -13
  195. package/src/teach/html/jsx-runtime.ts +0 -113
  196. package/src/teach/html/raw.ts +0 -28
  197. package/src/teach/lesson.ts +0 -178
  198. package/src/teach/nav.ts +0 -1337
  199. package/src/teach/render.ts +0 -240
  200. package/src/teach/workspace.ts +0 -1164
  201. package/src/teach/write-if-changed.ts +0 -17
@@ -1,567 +0,0 @@
1
- import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
- import { basename, join } from 'node:path'
3
- import { fileURLToPath, pathToFileURL } from 'node:url'
4
- import JSZip from 'jszip'
5
- import type { Browser, Page } from 'playwright-core'
6
- import PptxGenJS from 'pptxgenjs'
7
- import { isBrowserMissing, isEngineMissing } from '@/browser/engine'
8
- import { resolveFrameTokens } from '@/canvas/tokens'
9
- import {
10
- type ChartTheme,
11
- planChart,
12
- seriesColors,
13
- } from '@/slides/convert/chart'
14
- import { type DeckConfig, readDeck } from '@/slides/convert/deck'
15
- import {
16
- bandTexts,
17
- buildMaster,
18
- type DeckTheme,
19
- deckTheme,
20
- type MarkSize,
21
- type MasterBands,
22
- masterName,
23
- slideNumberProps,
24
- } from '@/slides/convert/master'
25
- import {
26
- type DrawOp,
27
- type Fallback,
28
- planSlide,
29
- type RefusedLink,
30
- } from '@/slides/convert/shapes'
31
- import {
32
- type BandOverride,
33
- ID_ATTRIBUTE,
34
- readTheme,
35
- type SlideMeta,
36
- type WalkedSlide,
37
- walkSlide,
38
- } from '@/slides/convert/walk'
39
- import { slideFiles } from '@/slides/decks'
40
- import { embedFonts, type FontNotice } from '@/slides/package/fonts'
41
- import {
42
- type EntranceSpec,
43
- entranceShapes,
44
- type MotionNotice,
45
- type SlideMotionEdit,
46
- writeMotion,
47
- } from '@/slides/package/motion'
48
-
49
- /**
50
- * Turns a folder of HTML slides into one editable deck. Chromium lays each
51
- * slide out at 1280 by 720 in the project's tokens, `walkSlide` reads what it
52
- * placed, `planSlide` maps the records onto shapes, and an element whose CSS has
53
- * no mapping lands as a screenshot of itself. The first slide's computed tokens
54
- * build the masters, and `deck.json` sets the bands they carry. Once pptxgenjs
55
- * has written the package, `src/slides/package/` adds the transitions,
56
- * entrances, and embedded faces it declares nothing for.
57
- *
58
- * Every browser reference this feature adds lives here, and the command reaches
59
- * it through a dynamic import so no other command resolves the engine.
60
- */
61
-
62
- export const SLIDE_WIDTH = 1280
63
- export const SLIDE_HEIGHT = 720
64
-
65
- /** Device pixels per CSS pixel in every screenshot, so a picture stays sharp. */
66
- const SCREENSHOT_SCALE = 2
67
-
68
- /** English Metric Units per inch, the unit a slide's XML measures in. */
69
- const EMU_PER_INCH = 914400
70
-
71
- export interface SlideFallback extends Fallback {
72
- readonly slide: number
73
- }
74
-
75
- export interface SlideRefusedLink extends RefusedLink {
76
- readonly slide: number
77
- }
78
-
79
- /** A chart left out of the deck, with the reason the report prints. */
80
- export interface RefusedChart {
81
- readonly slide: number
82
- readonly selector: string
83
- readonly message: string
84
- }
85
-
86
- export type ExportRefusal =
87
- | 'empty-source'
88
- | 'deck-invalid'
89
- | 'engine-missing'
90
- | 'browser-missing'
91
- | 'export-failed'
92
-
93
- export type ExportResult =
94
- | {
95
- readonly status: 'written'
96
- readonly pptxPath: string
97
- readonly mirrorPath?: string
98
- readonly slideCount: number
99
- readonly fallbacks: readonly SlideFallback[]
100
- readonly refusedLinks: readonly SlideRefusedLink[]
101
- readonly refusedCharts: readonly RefusedChart[]
102
- /** Transitions and entrances left out, each with the reason. */
103
- readonly refusedMotion: readonly MotionNotice[]
104
- /** Faces `deck.json` lists that the deck does not carry, with the reason. */
105
- readonly refusedFonts: readonly FontNotice[]
106
- /** What the deck master could not take from the project's tokens. */
107
- readonly notices: readonly string[]
108
- }
109
- | {
110
- readonly status: 'refused'
111
- readonly reason: ExportRefusal
112
- readonly message: string
113
- }
114
-
115
- export interface ExportOptions {
116
- /** The project whose tokens the slides are laid out in. */
117
- readonly root: string
118
- readonly mirror?: string
119
- }
120
-
121
- function refused(reason: ExportRefusal, message: string): ExportResult {
122
- return { status: 'refused', reason, message }
123
- }
124
-
125
- /** A picture whose bytes the post-write pass replaces or whose corners it rounds. */
126
- interface Patch {
127
- readonly slide: number
128
- readonly name: string
129
- readonly png?: Buffer
130
- readonly radius?: number
131
- }
132
-
133
- export async function exportHtmlDeck(
134
- sourceDir: string,
135
- outDir: string,
136
- options: ExportOptions,
137
- ): Promise<ExportResult> {
138
- const files = slideFiles(sourceDir)
139
- if (files.length === 0) {
140
- return refused('empty-source', `${sourceDir} holds no .html slides`)
141
- }
142
- const deckRead = readDeck(sourceDir)
143
- if (deckRead.status === 'refused') {
144
- return refused('deck-invalid', deckRead.message)
145
- }
146
- const deck = deckRead.deck
147
-
148
- let chromium: typeof import('playwright-core').chromium
149
- try {
150
- ;({ chromium } = await import('playwright-core'))
151
- } catch (error) {
152
- if (!isEngineMissing(error)) throw error
153
- return refused('engine-missing', 'playwright-core is not installed')
154
- }
155
-
156
- let browser: Browser
157
- try {
158
- browser = await chromium.launch()
159
- } catch (error) {
160
- const message = error instanceof Error ? error.message : String(error)
161
- return refused(
162
- isBrowserMissing(error) ? 'browser-missing' : 'export-failed',
163
- message,
164
- )
165
- }
166
-
167
- const tokens = resolveFrameTokens(options.root).css
168
- const pptx = new PptxGenJS()
169
- pptx.layout = 'LAYOUT_WIDE'
170
- pptx.title = deck.title
171
- const fallbacks: SlideFallback[] = []
172
- const refusedLinks: SlideRefusedLink[] = []
173
- const refusedCharts: RefusedChart[] = []
174
- const patches: Patch[] = []
175
- const motions: SlideMotionEdit[] = []
176
- const notices: string[] = []
177
-
178
- try {
179
- const page = await browser.newPage({
180
- viewport: { width: SLIDE_WIDTH, height: SLIDE_HEIGHT },
181
- deviceScaleFactor: SCREENSHOT_SCALE,
182
- })
183
- await prepare(page, files[0] ?? '', tokens)
184
- const reading = await page.evaluate(readTheme)
185
- const read = deckTheme(reading)
186
- const theme = read.theme
187
- notices.push(...read.notices)
188
- const drifted: number[] = []
189
- const chartTheme: ChartTheme = {
190
- colors: seriesColors(theme.accent, reading.roles),
191
- ink: theme.ink,
192
- muted: theme.muted,
193
- face: theme.face,
194
- }
195
- pptx.theme = { headFontFace: theme.face, bodyFontFace: theme.face }
196
- const markSize = deck.mark ? await measure(page, deck.mark) : undefined
197
- const masters = new Set<string>()
198
- const sections = new Sections(deck.title, declaresSections(files))
199
-
200
- for (const [index, file] of files.entries()) {
201
- const slideNumber = index + 1
202
- const walked = await layOut(page, file, tokens)
203
- const plan = planSlide(walked.records, { slideCount: files.length })
204
- fallbacks.push(
205
- ...plan.fallbacks.map((each) => ({ ...each, slide: slideNumber })),
206
- )
207
- refusedLinks.push(
208
- ...plan.refusedLinks.map((each) => ({ ...each, slide: slideNumber })),
209
- )
210
- const { textToken } = walked.meta
211
- if (textToken !== undefined && textToken !== theme.ink) {
212
- drifted.push(slideNumber)
213
- }
214
- // pptxgenjs binds a slide to its master at creation, so the bands this
215
- // slide keeps decide the master before any shape is added.
216
- const bands = masterBands(deck, walked.meta)
217
- const master = masterName(bands)
218
- if (!masters.has(master)) {
219
- pptx.defineSlideMaster(buildMaster(deck, theme, bands, markSize))
220
- masters.add(master)
221
- }
222
- const sectionTitle = sections.titleFor(walked.meta, pptx)
223
- const slide = pptx.addSlide({
224
- masterName: master,
225
- ...(sectionTitle ? { sectionTitle } : {}),
226
- })
227
- if (walked.background.hex !== theme.background) {
228
- slide.background = { color: walked.background.hex }
229
- }
230
- if (walked.meta.isHidden) slide.hidden = true
231
- for (const op of plan.ops) {
232
- await draw(page, slide, slideNumber, op, patches)
233
- }
234
- for (const record of walked.charts) {
235
- const chart = planChart(record, chartTheme)
236
- if (chart.status === 'refused') {
237
- refusedCharts.push({ ...chart, slide: slideNumber })
238
- continue
239
- }
240
- slide.addChart(chart.type, [...chart.data], chart.options)
241
- }
242
- drawOverrides(slide, deck, theme, walked.meta)
243
- if (walked.meta.notes) slide.addNotes(walked.meta.notes)
244
- const { transition } = walked.meta
245
- const roots = new Set(
246
- walked.entrances.flatMap(({ record }) =>
247
- record === null ? [] : [record],
248
- ),
249
- )
250
- const entrances = walked.entrances.map(
251
- ({ record, ...spec }): EntranceSpec => ({
252
- ...spec,
253
- shapes: entranceShapes(record, walked.records, plan.names, roots),
254
- }),
255
- )
256
- if (transition || entrances.length > 0) {
257
- motions.push({
258
- slide: slideNumber,
259
- motion: { ...(transition ? { transition } : {}), entrances },
260
- })
261
- }
262
- }
263
- if (drifted.length > 0) {
264
- const noun = drifted.length === 1 ? 'slide' : 'slides'
265
- const verb = drifted.length === 1 ? 'sets' : 'set'
266
- notices.push(
267
- `${noun} ${drifted.join(', ')} ${verb} a --color-text apart from the master, so ${drifted.length === 1 ? 'its' : 'their'} bands keep the master colors`,
268
- )
269
- }
270
- } catch (error) {
271
- return refused(
272
- 'export-failed',
273
- error instanceof Error ? error.message : String(error),
274
- )
275
- } finally {
276
- await browser.close().catch(() => undefined)
277
- }
278
-
279
- const written = await pptx.write({ outputType: 'nodebuffer' })
280
- if (!(written instanceof Uint8Array)) {
281
- return refused('export-failed', 'pptxgenjs returned no buffer')
282
- }
283
- // A deck needing no edit keeps the bytes pptxgenjs wrote.
284
- let packaged: Uint8Array = written
285
- const refusedMotion: MotionNotice[] = []
286
- const refusedFonts: FontNotice[] = []
287
- if (patches.length > 0 || motions.length > 0 || deck.fonts.length > 0) {
288
- const zip = await JSZip.loadAsync(written)
289
- await applyPatches(zip, patches)
290
- refusedMotion.push(...(await writeMotion(zip, motions)))
291
- const faces = deck.fonts.map((font) => ({
292
- family: font.family,
293
- weight: font.weight,
294
- style: font.style,
295
- path: font.source,
296
- bytes: readFileSync(font.path),
297
- }))
298
- refusedFonts.push(...(await embedFonts(zip, faces)))
299
- packaged = await zip.generateAsync({
300
- type: 'nodebuffer',
301
- compression: 'DEFLATE',
302
- })
303
- }
304
-
305
- mkdirSync(outDir, { recursive: true })
306
- const fileName = `${basename(sourceDir)}.pptx`
307
- const pptxPath = join(outDir, fileName)
308
- writeFileSync(pptxPath, packaged)
309
-
310
- let mirrorPath: string | undefined
311
- if (options.mirror) {
312
- mkdirSync(options.mirror, { recursive: true })
313
- mirrorPath = join(options.mirror, fileName)
314
- copyFileSync(pptxPath, mirrorPath)
315
- }
316
-
317
- return {
318
- status: 'written',
319
- pptxPath,
320
- mirrorPath,
321
- slideCount: files.length,
322
- fallbacks,
323
- refusedLinks,
324
- refusedCharts,
325
- refusedMotion,
326
- refusedFonts,
327
- notices,
328
- }
329
- }
330
-
331
- /**
332
- * A band stays on the master unless the deck has it off anyway, so a slide
333
- * hiding only the header keeps the master's footer and numbers.
334
- */
335
- function masterBands(deck: DeckConfig, meta: SlideMeta): MasterBands {
336
- return {
337
- header: deck.header.show && meta.header.kind === 'master',
338
- footer: deck.footer.show && meta.footer.kind === 'master',
339
- }
340
- }
341
-
342
- type Override = Extract<BandOverride, { readonly kind: 'override' }>
343
-
344
- /** The slide's slots over the deck's, so overriding the center keeps the title. */
345
- function mergeBand(override: Override, base: DeckConfig['header']) {
346
- return {
347
- left: override.left ?? base.left,
348
- center: override.center ?? base.center,
349
- right: override.right ?? base.right,
350
- }
351
- }
352
-
353
- function drawOverrides(
354
- slide: PptxGenJS.Slide,
355
- deck: DeckConfig,
356
- theme: DeckTheme,
357
- meta: SlideMeta,
358
- ): void {
359
- if (meta.header.kind === 'override') {
360
- const band = mergeBand(meta.header, deck.header)
361
- const hasMark = deck.mark !== undefined
362
- for (const text of bandTexts(band, 'header', theme, { hasMark })) {
363
- slide.addText(text.text, text.options)
364
- }
365
- }
366
- if (meta.footer.kind === 'override') {
367
- const band = mergeBand(meta.footer, deck.footer)
368
- for (const text of bandTexts(band, 'footer', theme)) {
369
- slide.addText(text.text, text.options)
370
- }
371
- if (deck.slideNumbers && band.right === undefined) {
372
- slide.slideNumber = slideNumberProps(theme)
373
- }
374
- }
375
- }
376
-
377
- /**
378
- * A slide's `data-section` opens a section the slides after it stay in.
379
- * PowerPoint wants every slide in a section once any exists, so slides ahead of
380
- * the first declared one open a section named after the deck.
381
- *
382
- * pptxgenjs files a slide under the first section carrying its title, so a
383
- * title the deck returns to takes a numbered suffix rather than sending the
384
- * slide back into the earlier section, which PowerPoint reads as a broken list.
385
- */
386
- export class Sections {
387
- /** The title as declared, and as written once a repeat is numbered. */
388
- private declared: string | undefined
389
- private current: string | undefined
390
- private readonly opened = new Map<string, number>()
391
-
392
- constructor(
393
- private readonly deckTitle: string,
394
- private readonly isUsed: boolean,
395
- ) {}
396
-
397
- titleFor(meta: SlideMeta, pptx: PptxGenJS): string | undefined {
398
- if (!this.isUsed) return undefined
399
- const isContinuing =
400
- meta.section === undefined || meta.section === this.declared
401
- if (isContinuing && this.current !== undefined) return this.current
402
- const title = meta.section ?? this.deckTitle
403
- const count = (this.opened.get(title) ?? 0) + 1
404
- this.opened.set(title, count)
405
- this.declared = title
406
- this.current = count === 1 ? title : `${title} (${count})`
407
- pptx.addSection({ title: this.current })
408
- return this.current
409
- }
410
- }
411
-
412
- /**
413
- * Whether any slide declares a section, read from the source ahead of layout,
414
- * since the first slide is placed before the last is laid out.
415
- */
416
- function declaresSections(files: readonly string[]): boolean {
417
- return files.some((file) =>
418
- /<body\b[^>]*\sdata-section\s*=/i.test(readFileSync(file, 'utf8')),
419
- )
420
- }
421
-
422
- /** The mark's intrinsic size as Chromium decodes it, square where unknown. */
423
- async function measure(page: Page, path: string): Promise<MarkSize> {
424
- const size = await page.evaluate(async (src) => {
425
- const image = new Image()
426
- image.src = src
427
- await image.decode().catch(() => undefined)
428
- return { width: image.naturalWidth, height: image.naturalHeight }
429
- }, pathToFileURL(path).href)
430
- return size.width > 0 && size.height > 0 ? size : { width: 1, height: 1 }
431
- }
432
-
433
- /**
434
- * The tokens go in ahead of the slide's own styles, so a slide reads
435
- * `var(--color-*)` from the project and can still override any of it.
436
- */
437
- async function layOut(
438
- page: Page,
439
- file: string,
440
- tokens: string,
441
- ): Promise<WalkedSlide> {
442
- await prepare(page, file, tokens)
443
- return page.evaluate(walkSlide, ID_ATTRIBUTE)
444
- }
445
-
446
- async function prepare(
447
- page: Page,
448
- file: string,
449
- tokens: string,
450
- ): Promise<void> {
451
- await page.goto(pathToFileURL(file).href, { waitUntil: 'load' })
452
- if (tokens) {
453
- await page.evaluate((css) => {
454
- const style = document.createElement('style')
455
- style.textContent = css
456
- document.head.prepend(style)
457
- }, tokens)
458
- }
459
- await page.evaluate(() => document.fonts.ready.then(() => undefined))
460
- }
461
-
462
- async function screenshot(page: Page, id: number): Promise<Buffer> {
463
- return page
464
- .locator(`[${ID_ATTRIBUTE}="${id}"]`)
465
- .screenshot({ omitBackground: true, animations: 'disabled' })
466
- }
467
-
468
- function imageSource(src: string): Pick<PptxGenJS.ImageProps, 'path' | 'data'> {
469
- if (src.startsWith('data:')) return { data: src.slice('data:'.length) }
470
- if (src.startsWith('file:')) return { path: fileURLToPath(src) }
471
- return { path: src }
472
- }
473
-
474
- const base64 = (bytes: Buffer | string): string =>
475
- Buffer.from(bytes).toString('base64')
476
-
477
- async function draw(
478
- page: Page,
479
- slide: PptxGenJS.Slide,
480
- slideNumber: number,
481
- op: DrawOp,
482
- patches: Patch[],
483
- ): Promise<void> {
484
- const target = { slide: slideNumber, name: op.options.objectName ?? '' }
485
- if (op.kind === 'shape') {
486
- slide.addShape(op.shape, op.options)
487
- } else if (op.kind === 'text') {
488
- slide.addText(op.runs, op.options)
489
- } else if (op.kind === 'table') {
490
- slide.addTable(op.rows, op.options)
491
- } else if (op.kind === 'image') {
492
- slide.addImage({ ...op.options, ...imageSource(op.src) })
493
- if (op.radius > 0) patches.push({ ...target, radius: op.radius })
494
- if (op.frame) slide.addShape(op.radius > 0 ? 'roundRect' : 'rect', op.frame)
495
- } else if (op.kind === 'svg') {
496
- slide.addImage({
497
- ...op.options,
498
- data: `image/svg+xml;base64,${base64(op.markup)}`,
499
- })
500
- patches.push({ ...target, png: await screenshot(page, op.id) })
501
- } else {
502
- const png = await screenshot(page, op.id)
503
- slide.addImage({ ...op.options, data: `image/png;base64,${base64(png)}` })
504
- }
505
- }
506
-
507
- /**
508
- * Two things pptxgenjs cannot write, fixed in the package it wrote. An SVG's
509
- * PNG fallback comes out as the SVG's own bytes under a `.png` name in Node, so
510
- * a viewer that cannot draw SVG gets a broken image, and a picture takes no
511
- * corner radius. Each patched picture is found by the object name `planSlide`
512
- * gave it, which is the only handle the written XML keeps.
513
- */
514
- async function applyPatches(
515
- zip: JSZip,
516
- patches: readonly Patch[],
517
- ): Promise<void> {
518
- const bySlide = Map.groupBy(patches, (patch) => patch.slide)
519
- for (const [slideNumber, slidePatches] of bySlide) {
520
- const slidePath = `ppt/slides/slide${slideNumber}.xml`
521
- const relsPath = `ppt/slides/_rels/slide${slideNumber}.xml.rels`
522
- let xml = (await zip.file(slidePath)?.async('string')) ?? ''
523
- const rels = (await zip.file(relsPath)?.async('string')) ?? ''
524
- for (const patch of slidePatches) {
525
- const pic = pictureXml(xml, patch.name)
526
- if (!pic) continue
527
- if (patch.png) {
528
- const rId = /<a:blip r:embed="(rId\d+)"/.exec(pic)?.[1]
529
- const target = rId
530
- ? new RegExp(`Id="${rId}"[^>]*Target="\\.\\./([^"]+)"`).exec(
531
- rels,
532
- )?.[1]
533
- : undefined
534
- if (target) zip.file(`ppt/${target}`, patch.png)
535
- }
536
- if (patch.radius) {
537
- xml = xml.replace(pic, roundCorners(pic, patch.radius))
538
- }
539
- }
540
- zip.file(slidePath, xml)
541
- }
542
- }
543
-
544
- function pictureXml(xml: string, name: string): string | undefined {
545
- const start = xml.indexOf(`name="${name}"`)
546
- if (start < 0) return undefined
547
- const open = xml.lastIndexOf('<p:pic>', start)
548
- const close = xml.indexOf('</p:pic>', start)
549
- return open < 0 || close < 0
550
- ? undefined
551
- : xml.slice(open, close + '</p:pic>'.length)
552
- }
553
-
554
- /** The adjust value is the radius over the shorter side, out of 100000. */
555
- function roundCorners(pic: string, radius: number): string {
556
- const extent = /<a:ext cx="(\d+)" cy="(\d+)"\/>/.exec(pic)
557
- const shorter = extent ? Math.min(Number(extent[1]), Number(extent[2])) : 0
558
- if (shorter === 0) return pic
559
- const adjust = Math.min(
560
- 50000,
561
- Math.round((radius * EMU_PER_INCH * 100000) / shorter),
562
- )
563
- return pic.replace(
564
- /<a:prstGeom prst="rect"><a:avLst\/><\/a:prstGeom>/,
565
- `<a:prstGeom prst="roundRect"><a:avLst><a:gd name="adj" fmla="val ${adjust}"/></a:avLst></a:prstGeom>`,
566
- )
567
- }