@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.
- package/README.md +2 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canvas/SKILL.md +1 -1
- package/claude/skills/context-fold/REQUIREMENT.md +6 -2
- package/claude/skills/context-fold/SKILL.md +8 -6
- package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/SKILL.md +4 -2
- package/claude/skills/design-taste/references/adopted.md +40 -0
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/systems.md +1 -7
- package/claude/skills/design-taste/references/tells.md +2 -24
- package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
- package/claude/skills/document-health/REQUIREMENT.md +2 -0
- package/claude/skills/document-health/SKILL.md +21 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/draft-and-pick/SKILL.md +1 -1
- package/claude/skills/draft-identity/SKILL.md +1 -1
- package/claude/skills/draft-screencast/SKILL.md +9 -7
- package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
- package/claude/skills/draft-slides/SKILL.md +31 -20
- package/claude/skills/git-issue/references/issue.md +10 -0
- package/claude/skills/git-pr/references/evidence.md +8 -0
- package/claude/skills/git-pr/references/labels.md +12 -0
- package/claude/skills/git-split/SKILL.md +19 -8
- package/claude/skills/markdown-propose/references/format.md +9 -0
- package/claude/skills/memory-review/references/later-phases.md +7 -0
- package/claude/skills/review-ui/references/post.md +4 -2
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +3 -1
- package/claude/skills/search-craft/references/adopted.md +30 -0
- package/claude/skills/target-setup/references/detect.md +8 -0
- package/claude/skills/target-setup/references/indexes.md +13 -0
- package/claude/skills/target-setup/references/verify.md +8 -0
- package/claude/skills/test-first/SKILL.md +2 -0
- package/claude/skills/test-first/references/adopted.md +36 -0
- package/claude/skills/write-human/SKILL.md +1 -1
- package/claude/skills/write-human/references/machine-tells.md +13 -0
- package/docs/agents/canvas.md +3 -1
- package/docs/agents/capture.md +1 -1
- package/docs/agents/commands.md +3 -3
- package/docs/agents/index.md +2 -2
- package/docs/agents/records-stale.md +22 -9
- package/docs/agents/records.md +1 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/skills-audit.md +10 -3
- package/docs/agents/slides.md +15 -3
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/index.md +1 -0
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +42 -0
- package/docs/workflow/skill-map.md +1 -34
- package/package.json +1 -1
- package/scripts/lib/sandbox-dispatch.sh +1 -1
- package/scripts/lib/sandbox-git.sh +1 -9
- package/scripts/lib/sandbox-path.sh +1 -1
- package/scripts/sandbox-hook.sh +99 -0
- package/src/canvas/capture.ts +1 -9
- package/src/canvas/client/inspector/color-field.tsx +368 -0
- package/src/canvas/client/inspector/fields.css +163 -22
- package/src/canvas/client/inspector/values.ts +123 -0
- package/src/canvas/client/inspector.tsx +46 -69
- package/src/capture/render.ts +45 -7
- package/src/capture/sources.ts +43 -0
- package/src/census/count.ts +1 -1
- package/src/claude/cases/workflow.ts +5 -0
- package/src/claude/seeds.ts +1 -1
- package/src/claude/skills-audit.ts +68 -14
- package/src/client-commands.ts +1 -1
- package/src/commands/claude.ts +27 -2
- package/src/commands/feedback.ts +3 -3
- package/src/commands/records.ts +88 -6
- package/src/commands/sandbox.ts +50 -10
- package/src/commands/slides.ts +31 -77
- package/src/context/audit.ts +1 -1
- package/src/design/tokens.ts +2 -18
- package/src/docs/read.ts +1 -1
- package/src/{github.ts → feedback/github.ts} +1 -1
- package/src/gate/measures.ts +6 -4
- package/src/gov/citations.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/superseded.ts +2 -2
- package/src/markdown/ceiling.ts +1 -1
- package/src/record-root.ts +1 -0
- package/src/records/backup.ts +1 -0
- package/src/records/push-guard.ts +1 -1
- package/src/records/stale-canonical.ts +146 -0
- package/src/records/stale.ts +1 -1
- package/src/sandbox/hooks.ts +128 -0
- package/src/sandbox/provision.ts +596 -0
- package/src/sandbox/tree.ts +103 -4
- package/src/secrets/marker.ts +2 -2
- package/src/secrets/scan.ts +1 -1
- package/src/shipped/references.ts +2 -2
- package/src/slides/convert/export.ts +2 -16
- package/src/slides/decks.ts +110 -0
- package/src/standards/read.ts +1 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/teach/nav.ts +1 -1
- package/src/tooling/inject.ts +1 -1
- package/src/web/readme-citations.ts +1 -1
- package/standards/architecture.md +1 -0
- package/standards/canvas.md +1 -1
- package/standards/requirements.md +2 -0
- package/standards/skill-practice.md +2 -1
- package/standards/skill.md +1 -0
- package/scripts/manage-sandbox.sh +0 -472
- package/src/slides/layouts.ts +0 -478
- package/src/slides/parse.ts +0 -84
- package/src/slides/render.ts +0 -110
- package/src/slides/styles.ts +0 -96
- /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
- /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
- /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
- /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
- /package/src/{binary.ts → sweep/binary.ts} +0 -0
- /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
- /package/src/{paths.ts → tasks/paths.ts} +0 -0
- /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
package/src/slides/layouts.ts
DELETED
|
@@ -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
|
-
}
|
package/src/slides/parse.ts
DELETED
|
@@ -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
|
-
}
|
package/src/slides/render.ts
DELETED
|
@@ -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
|
-
}
|