@erclx/canon 5.52.0 → 5.53.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.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -4
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +0 -3
- package/claude/skills/context-fold/REQUIREMENT.md +0 -1
- package/claude/skills/design-taste/references/kinds.md +0 -1
- package/claude/skills/draw-figure/REQUIREMENT.md +2 -3
- package/claude/skills/draw-figure/SKILL.md +4 -5
- package/claude/skills/draw-figure/references/figures.md +0 -1
- package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +0 -1
- package/claude/skills/pick-by-eye/SKILL.md +1 -1
- package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
- package/claude/skills/teach-craft/SKILL.md +39 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +52 -0
- package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +12 -4
- package/docs/agents/audits.md +0 -1
- package/docs/target/migrations.md +10 -1
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +10 -22
- package/docs/workflow/skill-map.md +12 -14
- package/package.json +1 -4
- package/scripts/core/regen-web-previews.ts +2 -67
- package/src/audits/catalog.ts +0 -1
- package/src/browser/engine.ts +3 -6
- package/src/capture/render.ts +1 -1
- package/src/claude/cases/authoring.ts +0 -12
- package/src/claude/cases/misc.ts +0 -17
- package/src/claude/cases/workflow.ts +2 -38
- package/src/claude/skills-audit.ts +0 -1
- package/src/cli/help.ts +0 -7
- package/src/cli.ts +0 -6
- package/src/commands/records.ts +0 -1
- package/src/commands/serve.ts +2 -2
- package/src/design/components.ts +8 -1514
- package/src/design/css.ts +5 -6
- package/src/design/fonts.ts +3 -5
- package/src/driver/drive.ts +3 -4
- package/src/driver/steps.ts +2 -5
- package/src/inventory/walk.ts +1 -1
- package/src/markdown/scope.ts +4 -31
- package/src/records/validate.ts +4 -176
- package/src/sandbox/equivalence.ts +0 -5
- package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
- package/claude/skills/draft-and-pick/SKILL.md +0 -11
- package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
- package/claude/skills/draft-doc/SKILL.md +0 -11
- package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
- package/claude/skills/draft-figure/SKILL.md +0 -11
- package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
- package/claude/skills/draft-identity/SKILL.md +0 -11
- package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
- package/claude/skills/draft-ready/SKILL.md +0 -11
- package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
- package/claude/skills/draft-screencast/SKILL.md +0 -11
- package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
- package/claude/skills/draft-slides/SKILL.md +0 -11
- package/claude/skills/make-deck/REQUIREMENT.md +0 -36
- package/claude/skills/make-deck/SKILL.md +0 -80
- package/claude/skills/make-deck/references/deck.md +0 -183
- package/claude/skills/read-frames/REQUIREMENT.md +0 -30
- package/claude/skills/read-frames/SKILL.md +0 -55
- package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
- package/claude/skills/record-screencast/SKILL.md +0 -99
- package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
- package/claude/skills/script-screencast/SKILL.md +0 -70
- package/claude/skills/script-screencast/references/beats-template.md +0 -119
- package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
- package/claude/skills/teach-workspace/SKILL.md +0 -196
- package/claude/skills/teach-workspace/references/glossary.md +0 -90
- package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
- package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
- package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
- package/claude/skills/teach-workspace/references/promotion.md +0 -116
- package/claude/skills/teach-workspace/references/revisit.md +0 -28
- package/claude/skills/teach-workspace/references/teach.md +0 -211
- package/claude/skills/video-craft/REQUIREMENT.md +0 -35
- package/claude/skills/video-craft/SKILL.md +0 -96
- package/claude/skills/video-craft/references/adopted.md +0 -110
- package/src/commands/demo.ts +0 -542
- package/src/commands/slides.ts +0 -184
- package/src/commands/teach.ts +0 -1401
- package/src/demo/beats.ts +0 -135
- package/src/demo/compile.ts +0 -350
- package/src/demo/container.ts +0 -219
- package/src/demo/cursors.ts +0 -55
- package/src/demo/drive.ts +0 -442
- package/src/demo/pointer.ts +0 -178
- package/src/demo/theme.ts +0 -112
- package/src/demo/timeline.ts +0 -64
- package/src/slides/convert/chart.ts +0 -165
- package/src/slides/convert/deck.ts +0 -244
- package/src/slides/convert/export.ts +0 -567
- package/src/slides/convert/master.ts +0 -198
- package/src/slides/convert/shapes.ts +0 -804
- package/src/slides/convert/svg.ts +0 -38
- package/src/slides/convert/walk.ts +0 -671
- package/src/slides/decks.ts +0 -110
- package/src/slides/open.ts +0 -18
- package/src/slides/package/fonts.ts +0 -301
- package/src/slides/package/motion.ts +0 -316
- package/src/slides/package/test-face.ts +0 -113
- package/src/teach/browser/compile.ts +0 -44
- package/src/teach/browser/dismiss.ts +0 -76
- package/src/teach/browser/focus-line.ts +0 -19
- package/src/teach/browser/glossary-filter.ts +0 -44
- package/src/teach/browser/head.ts +0 -30
- package/src/teach/browser/quiz.ts +0 -19
- package/src/teach/browser/reload.ts +0 -9
- package/src/teach/browser/sidebar.ts +0 -233
- package/src/teach/browser/theme.ts +0 -18
- package/src/teach/components/code.tsx +0 -24
- package/src/teach/components/heading.tsx +0 -11
- package/src/teach/components/list.tsx +0 -17
- package/src/teach/components/paragraph.tsx +0 -29
- package/src/teach/components/refs.tsx +0 -28
- package/src/teach/cross-references.ts +0 -78
- package/src/teach/fonts.ts +0 -16
- package/src/teach/highlight.ts +0 -71
- package/src/teach/html/jsx-dev-runtime.ts +0 -13
- package/src/teach/html/jsx-runtime.ts +0 -113
- package/src/teach/html/raw.ts +0 -28
- package/src/teach/lesson.ts +0 -178
- package/src/teach/nav.ts +0 -1337
- package/src/teach/render.ts +0 -240
- package/src/teach/workspace.ts +0 -1164
- 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
|
-
}
|