@erclx/canon 5.51.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 +20 -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/git-pr/references/pr.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/review-address/SKILL.md +1 -1
- package/claude/skills/review-address/references/post-review-findings.md +1 -1
- package/claude/skills/review-pr/references/post.md +2 -2
- package/claude/skills/target-check/SKILL.md +2 -0
- package/claude/skills/target-setup/SKILL.md +1 -1
- package/claude/skills/target-update/REQUIREMENT.md +33 -0
- package/claude/skills/target-update/SKILL.md +136 -0
- 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/agents/migrate.md +2 -1
- package/docs/target/migrations.md +12 -1
- package/docs/target/projects.md +1 -1
- package/docs/target/sync.md +1 -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 +13 -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/setup.ts +7 -0
- 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/labels.ts +9 -7
- 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/migrate/status.ts +5 -1
- package/src/records/validate.ts +4 -176
- package/src/sandbox/equivalence.ts +0 -5
- package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +1 -1
- 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,316 +0,0 @@
|
|
|
1
|
-
import type JSZip from 'jszip'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Slide transitions and entrance animations, written into a slide's XML after
|
|
5
|
-
* pptxgenjs has written it, since pptxgenjs declares neither. A slide states
|
|
6
|
-
* them in its HTML, `walk.ts` reads the attributes, and the converter resolves
|
|
7
|
-
* each animated element to the object names `planSlide` gave its shapes. This
|
|
8
|
-
* module turns those names into the shape ids the written XML assigned.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export interface TransitionSpec {
|
|
12
|
-
readonly effect: string
|
|
13
|
-
readonly duration?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface EntranceSpec {
|
|
17
|
-
/** The element that declared it, which every notice names. */
|
|
18
|
-
readonly selector: string
|
|
19
|
-
readonly effect: string
|
|
20
|
-
readonly order?: string
|
|
21
|
-
readonly duration?: string
|
|
22
|
-
/** Every shape drawn from the element and the elements inside it. */
|
|
23
|
-
readonly shapes: readonly string[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SlideMotion {
|
|
27
|
-
readonly transition?: TransitionSpec
|
|
28
|
-
readonly entrances: readonly EntranceSpec[]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface MotionResult {
|
|
32
|
-
readonly xml: string
|
|
33
|
-
readonly notices: readonly string[]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** A slide's motion, numbered as the package numbers its slide parts. */
|
|
37
|
-
export interface SlideMotionEdit {
|
|
38
|
-
readonly slide: number
|
|
39
|
-
readonly motion: SlideMotion
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface MotionNotice {
|
|
43
|
-
readonly slide: number
|
|
44
|
-
readonly message: string
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** The child element `<p:transition>` holds, each in its default direction. */
|
|
48
|
-
const TRANSITIONS: Readonly<Record<string, string>> = {
|
|
49
|
-
fade: '<p:fade/>',
|
|
50
|
-
push: '<p:push/>',
|
|
51
|
-
wipe: '<p:wipe/>',
|
|
52
|
-
cover: '<p:cover/>',
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The strict schema states a transition's length only as one of three speeds,
|
|
57
|
-
* 0.5, 0.75, and 1 second. A length in milliseconds needs the PowerPoint 2010
|
|
58
|
-
* extension namespace, so a declared duration rounds to the nearest speed.
|
|
59
|
-
*/
|
|
60
|
-
function speedOf(duration: number | undefined): 'fast' | 'med' | 'slow' {
|
|
61
|
-
if (duration === undefined) return 'med'
|
|
62
|
-
if (duration < 625) return 'fast'
|
|
63
|
-
return duration < 875 ? 'med' : 'slow'
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
type Behaviors = (spid: string, duration: number, next: () => number) => string
|
|
67
|
-
|
|
68
|
-
const target = (spid: string): string =>
|
|
69
|
-
`<p:tgtEl><p:spTgt spid="${spid}"/></p:tgtEl>`
|
|
70
|
-
|
|
71
|
-
const animEffect =
|
|
72
|
-
(filter: string): Behaviors =>
|
|
73
|
-
(spid, duration, next) =>
|
|
74
|
-
`<p:animEffect transition="in" filter="${filter}"><p:cBhvr><p:cTn id="${next()}" dur="${duration}"/>${target(spid)}</p:cBhvr></p:animEffect>`
|
|
75
|
-
|
|
76
|
-
function anim(
|
|
77
|
-
spid: string,
|
|
78
|
-
duration: number,
|
|
79
|
-
id: number,
|
|
80
|
-
attribute: string,
|
|
81
|
-
from: string,
|
|
82
|
-
to: string,
|
|
83
|
-
): string {
|
|
84
|
-
return `<p:anim calcmode="lin" valueType="num"><p:cBhvr additive="base"><p:cTn id="${id}" dur="${duration}" fill="hold"/>${target(spid)}<p:attrNameLst><p:attrName>${attribute}</p:attrName></p:attrNameLst></p:cBhvr><p:tavLst><p:tav tm="0"><p:val><p:strVal val="${from}"/></p:val></p:tav><p:tav tm="100000"><p:val><p:strVal val="${to}"/></p:val></p:tav></p:tavLst></p:anim>`
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface Preset {
|
|
88
|
-
readonly id: number
|
|
89
|
-
readonly subtype: number
|
|
90
|
-
readonly behaviors: Behaviors
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* PowerPoint's own preset ids, so the effect shows by name in its animation
|
|
95
|
-
* pane. Fly comes in from the bottom and wipe from the left, the directions
|
|
96
|
-
* PowerPoint picks when none is chosen. A wipe filter names the edge the
|
|
97
|
-
* reveal starts from, so from the left pairs with `wipe(left)`.
|
|
98
|
-
*/
|
|
99
|
-
const ENTRANCES: Readonly<Record<string, Preset>> = {
|
|
100
|
-
fade: { id: 10, subtype: 0, behaviors: animEffect('fade') },
|
|
101
|
-
fly: {
|
|
102
|
-
id: 2,
|
|
103
|
-
subtype: 4,
|
|
104
|
-
behaviors: (spid, duration, next) =>
|
|
105
|
-
anim(spid, duration, next(), 'ppt_x', '#ppt_x', '#ppt_x') +
|
|
106
|
-
anim(spid, duration, next(), 'ppt_y', '1+#ppt_h/2', '#ppt_y'),
|
|
107
|
-
},
|
|
108
|
-
wipe: { id: 22, subtype: 8, behaviors: animEffect('wipe(left)') },
|
|
109
|
-
zoom: {
|
|
110
|
-
id: 53,
|
|
111
|
-
subtype: 16,
|
|
112
|
-
behaviors: (spid, duration, next) =>
|
|
113
|
-
anim(spid, duration, next(), 'ppt_w', '0', '#ppt_w') +
|
|
114
|
-
anim(spid, duration, next(), 'ppt_h', '0', '#ppt_h') +
|
|
115
|
-
animEffect('fade')(spid, duration, next),
|
|
116
|
-
},
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const DEFAULT_ENTRANCE_MS = 500
|
|
120
|
-
|
|
121
|
-
const ANCHOR = '</p:clrMapOvr>'
|
|
122
|
-
|
|
123
|
-
const list = (names: readonly string[]): string =>
|
|
124
|
-
`${names.slice(0, -1).join(', ')}, or ${names.at(-1)}`
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* The object names an entrance on `root` brings in: every shape drawn from it
|
|
128
|
-
* and from the elements inside it, so a card's box and its text arrive on one
|
|
129
|
-
* click. A descendant in `roots` declares its own entrance and keeps its
|
|
130
|
-
* subtree for that click. A record follows its parent in document order, which
|
|
131
|
-
* lets one pass collect the subtree.
|
|
132
|
-
*/
|
|
133
|
-
export function entranceShapes(
|
|
134
|
-
root: number | null,
|
|
135
|
-
records: readonly { readonly id: number; readonly parent: number | null }[],
|
|
136
|
-
names: readonly { readonly record: number; readonly name: string }[],
|
|
137
|
-
roots: ReadonlySet<number>,
|
|
138
|
-
): string[] {
|
|
139
|
-
if (root === null) return []
|
|
140
|
-
const subtree = new Set([root])
|
|
141
|
-
for (const record of records) {
|
|
142
|
-
const isOwnEntrance = roots.has(record.id)
|
|
143
|
-
if (
|
|
144
|
-
record.parent !== null &&
|
|
145
|
-
subtree.has(record.parent) &&
|
|
146
|
-
!isOwnEntrance
|
|
147
|
-
) {
|
|
148
|
-
subtree.add(record.id)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return names
|
|
152
|
-
.filter((named) => subtree.has(named.record))
|
|
153
|
-
.map((named) => named.name)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Milliseconds from `500`, `500ms`, or `0.5s`, or undefined when unreadable. */
|
|
157
|
-
export function millisecondsOf(value: string | undefined): number | undefined {
|
|
158
|
-
const match = /^\s*(\d+(?:\.\d+)?)\s*(ms|s)?\s*$/.exec(value ?? '')
|
|
159
|
-
if (!match?.[1]) return undefined
|
|
160
|
-
const amount = Number(match[1])
|
|
161
|
-
return Math.round(match[2] === 's' ? amount * 1000 : amount)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* pptxgenjs numbers a table `n * slideNumber + 1` and every other shape by its
|
|
166
|
-
* index plus two, so a table can share an id with a shape beside it. A timing
|
|
167
|
-
* target has to name one shape, so each repeat after the first takes a fresh id.
|
|
168
|
-
*/
|
|
169
|
-
function uniqueIds(xml: string): string {
|
|
170
|
-
const pattern = /<p:cNvPr id="(\d+)"/g
|
|
171
|
-
let highest = Math.max(
|
|
172
|
-
0,
|
|
173
|
-
...[...xml.matchAll(pattern)].map((match) => Number(match[1])),
|
|
174
|
-
)
|
|
175
|
-
const seen = new Set<string>()
|
|
176
|
-
return xml.replace(pattern, (whole, id: string) => {
|
|
177
|
-
if (!seen.has(id)) {
|
|
178
|
-
seen.add(id)
|
|
179
|
-
return whole
|
|
180
|
-
}
|
|
181
|
-
highest += 1
|
|
182
|
-
return `<p:cNvPr id="${highest}"`
|
|
183
|
-
})
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function shapeIds(xml: string): Map<string, string> {
|
|
187
|
-
const ids = new Map<string, string>()
|
|
188
|
-
for (const [, id, name] of xml.matchAll(
|
|
189
|
-
/<p:cNvPr id="(\d+)" name="([^"]*)"/g,
|
|
190
|
-
)) {
|
|
191
|
-
if (id && name && !ids.has(name)) ids.set(name, id)
|
|
192
|
-
}
|
|
193
|
-
return ids
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
interface Click {
|
|
197
|
-
readonly preset: Preset
|
|
198
|
-
readonly duration: number
|
|
199
|
-
readonly spids: readonly string[]
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function timingXml(clicks: readonly Click[]): string {
|
|
203
|
-
let counter = 2
|
|
204
|
-
const next = (): number => {
|
|
205
|
-
counter += 1
|
|
206
|
-
return counter
|
|
207
|
-
}
|
|
208
|
-
const pars = clicks.map((click) => {
|
|
209
|
-
const outer = next()
|
|
210
|
-
const inner = next()
|
|
211
|
-
const effects = click.spids.map((spid, index) => {
|
|
212
|
-
const nodeType = index === 0 ? 'clickEffect' : 'withEffect'
|
|
213
|
-
const effect = next()
|
|
214
|
-
const visible = `<p:set><p:cBhvr><p:cTn id="${next()}" dur="1" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn>${target(spid)}<p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="visible"/></p:to></p:set>`
|
|
215
|
-
const behaviors = click.preset.behaviors(spid, click.duration, next)
|
|
216
|
-
return `<p:par><p:cTn id="${effect}" presetID="${click.preset.id}" presetClass="entr" presetSubtype="${click.preset.subtype}" fill="hold" nodeType="${nodeType}"><p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>${visible}${behaviors}</p:childTnLst></p:cTn></p:par>`
|
|
217
|
-
})
|
|
218
|
-
return `<p:par><p:cTn id="${outer}" fill="hold"><p:stCondLst><p:cond delay="indefinite"/></p:stCondLst><p:childTnLst><p:par><p:cTn id="${inner}" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>${effects.join('')}</p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par>`
|
|
219
|
-
})
|
|
220
|
-
return `<p:timing><p:tnLst><p:par><p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst><p:seq concurrent="1" nextAc="seek"><p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst>${pars.join('')}</p:childTnLst></p:cTn><p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst><p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst></p:seq></p:childTnLst></p:cTn></p:par></p:tnLst></p:timing>`
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Declared order first, then document order for ties and for no order. */
|
|
224
|
-
function ordered(entrances: readonly EntranceSpec[]): EntranceSpec[] {
|
|
225
|
-
const rank = (entrance: EntranceSpec): number => {
|
|
226
|
-
const order = Number.parseFloat(entrance.order ?? '')
|
|
227
|
-
return Number.isFinite(order) ? order : Number.POSITIVE_INFINITY
|
|
228
|
-
}
|
|
229
|
-
return entrances.toSorted((a, b) => rank(a) - rank(b))
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Inserts the transition and an on-click main sequence after the slide's color
|
|
234
|
-
* map override, where the schema orders them. Each entrance is one click, and
|
|
235
|
-
* every shape drawn from its element comes in on that click together. A slide
|
|
236
|
-
* missing the anchor is returned unchanged rather than patched elsewhere.
|
|
237
|
-
*/
|
|
238
|
-
export function addMotion(xml: string, motion: SlideMotion): MotionResult {
|
|
239
|
-
const notices: string[] = []
|
|
240
|
-
let transition = ''
|
|
241
|
-
if (motion.transition) {
|
|
242
|
-
const element = TRANSITIONS[motion.transition.effect]
|
|
243
|
-
if (element) {
|
|
244
|
-
const speed = speedOf(millisecondsOf(motion.transition.duration))
|
|
245
|
-
transition = `<p:transition spd="${speed}">${element}</p:transition>`
|
|
246
|
-
} else {
|
|
247
|
-
notices.push(
|
|
248
|
-
`unknown transition ${motion.transition.effect}. Use ${list(Object.keys(TRANSITIONS))}`,
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const numbered = uniqueIds(xml)
|
|
254
|
-
const ids = shapeIds(numbered)
|
|
255
|
-
const clicks: Click[] = []
|
|
256
|
-
for (const entrance of ordered(motion.entrances)) {
|
|
257
|
-
const preset = ENTRANCES[entrance.effect]
|
|
258
|
-
if (!preset) {
|
|
259
|
-
notices.push(
|
|
260
|
-
`${entrance.selector}: unknown entrance ${entrance.effect}. Use ${list(Object.keys(ENTRANCES))}`,
|
|
261
|
-
)
|
|
262
|
-
continue
|
|
263
|
-
}
|
|
264
|
-
const spids = entrance.shapes.flatMap((name) => {
|
|
265
|
-
const id = ids.get(name)
|
|
266
|
-
return id ? [id] : []
|
|
267
|
-
})
|
|
268
|
-
if (spids.length === 0) {
|
|
269
|
-
notices.push(
|
|
270
|
-
`${entrance.selector}: draws no shape, so its entrance has nothing to animate`,
|
|
271
|
-
)
|
|
272
|
-
continue
|
|
273
|
-
}
|
|
274
|
-
const duration = millisecondsOf(entrance.duration) ?? DEFAULT_ENTRANCE_MS
|
|
275
|
-
clicks.push({ preset, duration, spids })
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const timing = clicks.length > 0 ? timingXml(clicks) : ''
|
|
279
|
-
if (!transition && !timing) return { xml, notices }
|
|
280
|
-
if (!xml.includes(ANCHOR)) {
|
|
281
|
-
return {
|
|
282
|
-
xml,
|
|
283
|
-
notices: [
|
|
284
|
-
...notices,
|
|
285
|
-
`no ${ANCHOR} to anchor motion after, so none was written`,
|
|
286
|
-
],
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return {
|
|
290
|
-
xml: (timing ? numbered : xml).replace(
|
|
291
|
-
ANCHOR,
|
|
292
|
-
`${ANCHOR}${transition}${timing}`,
|
|
293
|
-
),
|
|
294
|
-
notices,
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Writes each slide's motion into the opened package and returns its notices. */
|
|
299
|
-
export async function writeMotion(
|
|
300
|
-
zip: JSZip,
|
|
301
|
-
edits: readonly SlideMotionEdit[],
|
|
302
|
-
): Promise<MotionNotice[]> {
|
|
303
|
-
const notices: MotionNotice[] = []
|
|
304
|
-
for (const { slide, motion } of edits) {
|
|
305
|
-
const path = `ppt/slides/slide${slide}.xml`
|
|
306
|
-
const xml = await zip.file(path)?.async('string')
|
|
307
|
-
if (xml === undefined) {
|
|
308
|
-
notices.push({ slide, message: `no ${path} in the written deck` })
|
|
309
|
-
continue
|
|
310
|
-
}
|
|
311
|
-
const result = addMotion(xml, motion)
|
|
312
|
-
if (result.xml !== xml) zip.file(path, result.xml)
|
|
313
|
-
notices.push(...result.notices.map((message) => ({ slide, message })))
|
|
314
|
-
}
|
|
315
|
-
return notices
|
|
316
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds the smallest sfnt the font step reads, holding only the `OS/2`,
|
|
3
|
-
* `head`, and `name` tables an EOT header is built from, plus `fvar` when the
|
|
4
|
-
* face should read as variable. It carries no glyphs, so it embeds and never
|
|
5
|
-
* renders. Tests use it in place of a real face, whose license the repository
|
|
6
|
-
* would then have to carry.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface TestFaceOptions {
|
|
10
|
-
readonly family?: string
|
|
11
|
-
readonly subfamily?: string
|
|
12
|
-
readonly weight?: number
|
|
13
|
-
readonly isItalic?: boolean
|
|
14
|
-
readonly fsType?: number
|
|
15
|
-
readonly isVariable?: boolean
|
|
16
|
-
/** The first four bytes, `0x00010000` for TrueType. */
|
|
17
|
-
readonly version?: number
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Values distinct enough that a header copying the wrong field shows it. */
|
|
21
|
-
export const TEST_FACE = {
|
|
22
|
-
panose: [2, 11, 5, 3, 3, 4, 3, 2, 2, 4],
|
|
23
|
-
unicodeRanges: [0xe0000aff, 0x5000e5ff, 0x00000009, 0x00000001],
|
|
24
|
-
codePages: [0x0000019f, 0x01020000],
|
|
25
|
-
checkSumAdjustment: 0x1234abcd,
|
|
26
|
-
} as const
|
|
27
|
-
|
|
28
|
-
function os2(options: TestFaceOptions): Buffer {
|
|
29
|
-
const table = Buffer.alloc(96)
|
|
30
|
-
table.writeUInt16BE(4, 0)
|
|
31
|
-
table.writeUInt16BE(options.weight ?? 400, 4)
|
|
32
|
-
table.writeUInt16BE(options.fsType ?? 0, 8)
|
|
33
|
-
Buffer.from(TEST_FACE.panose).copy(table, 32)
|
|
34
|
-
TEST_FACE.unicodeRanges.forEach((range, index) => {
|
|
35
|
-
table.writeUInt32BE(range, 42 + index * 4)
|
|
36
|
-
})
|
|
37
|
-
table.write('TEST', 58, 'latin1')
|
|
38
|
-
table.writeUInt16BE(options.isItalic ? 0x0001 : 0x0040, 62)
|
|
39
|
-
TEST_FACE.codePages.forEach((page, index) => {
|
|
40
|
-
table.writeUInt32BE(page, 78 + index * 4)
|
|
41
|
-
})
|
|
42
|
-
return table
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function head(): Buffer {
|
|
46
|
-
const table = Buffer.alloc(54)
|
|
47
|
-
table.writeUInt32BE(0x00010000, 0)
|
|
48
|
-
table.writeUInt32BE(TEST_FACE.checkSumAdjustment, 8)
|
|
49
|
-
table.writeUInt32BE(0x5f0f3cf5, 12)
|
|
50
|
-
return table
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function name(strings: ReadonlyMap<number, string>): Buffer {
|
|
54
|
-
const encoded = [...strings].map(([id, text]) => ({
|
|
55
|
-
id,
|
|
56
|
-
bytes: Buffer.from(text, 'utf16le').swap16(),
|
|
57
|
-
}))
|
|
58
|
-
const header = Buffer.alloc(6 + encoded.length * 12)
|
|
59
|
-
header.writeUInt16BE(0, 0)
|
|
60
|
-
header.writeUInt16BE(encoded.length, 2)
|
|
61
|
-
header.writeUInt16BE(header.length, 4)
|
|
62
|
-
let offset = 0
|
|
63
|
-
encoded.forEach(({ id, bytes }, index) => {
|
|
64
|
-
const at = 6 + index * 12
|
|
65
|
-
header.writeUInt16BE(3, at)
|
|
66
|
-
header.writeUInt16BE(1, at + 2)
|
|
67
|
-
header.writeUInt16BE(0x0409, at + 4)
|
|
68
|
-
header.writeUInt16BE(id, at + 6)
|
|
69
|
-
header.writeUInt16BE(bytes.length, at + 8)
|
|
70
|
-
header.writeUInt16BE(offset, at + 10)
|
|
71
|
-
offset += bytes.length
|
|
72
|
-
})
|
|
73
|
-
return Buffer.concat([header, ...encoded.map(({ bytes }) => bytes)])
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function testFace(options: TestFaceOptions = {}): Buffer {
|
|
77
|
-
const family = options.family ?? 'Fixture Sans'
|
|
78
|
-
const subfamily = options.subfamily ?? 'Regular'
|
|
79
|
-
const tables = new Map<string, Buffer>([
|
|
80
|
-
['OS/2', os2(options)],
|
|
81
|
-
['head', head()],
|
|
82
|
-
[
|
|
83
|
-
'name',
|
|
84
|
-
name(
|
|
85
|
-
new Map([
|
|
86
|
-
[1, family],
|
|
87
|
-
[2, subfamily],
|
|
88
|
-
[4, `${family} ${subfamily}`],
|
|
89
|
-
[5, 'Version 1.000'],
|
|
90
|
-
]),
|
|
91
|
-
),
|
|
92
|
-
],
|
|
93
|
-
])
|
|
94
|
-
if (options.isVariable) tables.set('fvar', Buffer.alloc(16))
|
|
95
|
-
|
|
96
|
-
const entries = [...tables].sort(([a], [b]) => a.localeCompare(b))
|
|
97
|
-
const directory = Buffer.alloc(12 + entries.length * 16)
|
|
98
|
-
directory.writeUInt32BE(options.version ?? 0x00010000, 0)
|
|
99
|
-
directory.writeUInt16BE(entries.length, 4)
|
|
100
|
-
let offset = directory.length
|
|
101
|
-
const bodies: Buffer[] = []
|
|
102
|
-
entries.forEach(([tag, table], index) => {
|
|
103
|
-
const at = 12 + index * 16
|
|
104
|
-
directory.write(tag, at, 'latin1')
|
|
105
|
-
directory.writeUInt32BE(offset, at + 8)
|
|
106
|
-
directory.writeUInt32BE(table.length, at + 12)
|
|
107
|
-
const padded = Buffer.alloc(Math.ceil(table.length / 4) * 4)
|
|
108
|
-
table.copy(padded)
|
|
109
|
-
bodies.push(padded)
|
|
110
|
-
offset += padded.length
|
|
111
|
-
})
|
|
112
|
-
return Buffer.concat([directory, ...bodies])
|
|
113
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
type BrowserFunction = (...args: never[]) => unknown
|
|
2
|
-
|
|
3
|
-
const transpiler = new Bun.Transpiler({ loader: 'ts' })
|
|
4
|
-
|
|
5
|
-
const MODULE_SYNTAX = /^\s*(import|export)\b/m
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* `</script>` inside a serialized argument would end the block early, and
|
|
9
|
-
* `<` is the same character to the JavaScript parser.
|
|
10
|
-
*/
|
|
11
|
-
function serialize(args: readonly unknown[]): string {
|
|
12
|
-
return JSON.stringify(args).slice(1, -1).replace(/</g, '\\u003c')
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Turns a browser module's function into the `<script>` a page carries. The
|
|
17
|
-
* transpiler does not bundle, so a module is self-contained: whatever it
|
|
18
|
-
* calls but does not define arrives as a named function in `helpers`, and a
|
|
19
|
-
* value it needs arrives in `args`. Output holding module syntax throws, since
|
|
20
|
-
* a script block with an `import` fails in the browser with nothing in the
|
|
21
|
-
* type checker to say so.
|
|
22
|
-
*/
|
|
23
|
-
export function compileScript(
|
|
24
|
-
run: BrowserFunction,
|
|
25
|
-
args: readonly unknown[] = [],
|
|
26
|
-
helpers: readonly BrowserFunction[] = [],
|
|
27
|
-
): string {
|
|
28
|
-
const source = helpers.length
|
|
29
|
-
? `(function () {\n${helpers.map(String).join('\n')}\nreturn ${run};\n})()`
|
|
30
|
-
: String(run)
|
|
31
|
-
// A bare expression statement is dead code to the transpiler and comes back
|
|
32
|
-
// empty, so the source rides an export that it must keep.
|
|
33
|
-
const code = transpiler
|
|
34
|
-
.transformSync(`export default ${source}`)
|
|
35
|
-
.trim()
|
|
36
|
-
.replace(/^export default /, '')
|
|
37
|
-
.replace(/;$/, '')
|
|
38
|
-
|
|
39
|
-
if (MODULE_SYNTAX.test(code)) {
|
|
40
|
-
throw new Error(`Browser module ${run.name} left import or export syntax`)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return `<script>(${code})(${serialize(args)})</script>`
|
|
44
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
export function dismiss(): void {
|
|
2
|
-
var MENUS = 'details.jump[open], details.sb-ws[open]'
|
|
3
|
-
|
|
4
|
-
function close(except: Element | null) {
|
|
5
|
-
document.querySelectorAll<HTMLDetailsElement>(MENUS).forEach(function (d) {
|
|
6
|
-
if (d !== except) d.open = false
|
|
7
|
-
})
|
|
8
|
-
}
|
|
9
|
-
document.addEventListener('click', function (e) {
|
|
10
|
-
var inside = (e.target as Element).closest('details.jump, details.sb-ws')
|
|
11
|
-
close(inside)
|
|
12
|
-
})
|
|
13
|
-
document.addEventListener('keydown', function (e) {
|
|
14
|
-
if (e.key !== 'Escape') return
|
|
15
|
-
var open = document.querySelector<HTMLDetailsElement>(MENUS)
|
|
16
|
-
if (!open) return
|
|
17
|
-
open.open = false
|
|
18
|
-
var s = open.querySelector('summary')
|
|
19
|
-
if (s) s.focus()
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
/* Hover intent on the breadcrumb only. The sidebar's workspace switcher stays
|
|
23
|
-
click-only on purpose: its panel opens directly over the lesson list, which
|
|
24
|
-
is where the pointer is headed, so hovering it would cover the thing
|
|
25
|
-
being reached for. The breadcrumb's panel drops over body text instead. */
|
|
26
|
-
var OPEN = 120,
|
|
27
|
-
SHUT = 260
|
|
28
|
-
|
|
29
|
-
document
|
|
30
|
-
.querySelectorAll<HTMLDetailsElement>('details.jump')
|
|
31
|
-
.forEach(function (d) {
|
|
32
|
-
var host = d.closest('.crumb-item') || d
|
|
33
|
-
var timer: ReturnType<typeof setTimeout> | undefined
|
|
34
|
-
var openedByHover = false
|
|
35
|
-
|
|
36
|
-
function arm(want: boolean) {
|
|
37
|
-
clearTimeout(timer)
|
|
38
|
-
timer = setTimeout(
|
|
39
|
-
function () {
|
|
40
|
-
if (want) close(d)
|
|
41
|
-
d.open = want
|
|
42
|
-
},
|
|
43
|
-
want ? OPEN : SHUT,
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
host.addEventListener('mouseenter', function () {
|
|
48
|
-
if (!d.open) openedByHover = true
|
|
49
|
-
arm(true)
|
|
50
|
-
})
|
|
51
|
-
host.addEventListener('mouseleave', function () {
|
|
52
|
-
openedByHover = false
|
|
53
|
-
arm(false)
|
|
54
|
-
})
|
|
55
|
-
/* Keeps it open while the pointer is inside the panel, without cancelling a
|
|
56
|
-
pending open, which is what silently disabled this on one of two menus. */
|
|
57
|
-
d.addEventListener('mouseenter', function () {
|
|
58
|
-
if (d.open) clearTimeout(timer)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
/* Hover and click were wired to one disclosure and fought: hover opened the
|
|
62
|
-
panel, then the summary's native click toggled what hover had already
|
|
63
|
-
opened, so reaching for an item shut it. A click on a panel hover opened
|
|
64
|
-
keeps it open, and a second click closes. The click path stays live for
|
|
65
|
-
touch, where no hover exists, and for the keyboard, where Enter is the
|
|
66
|
-
only way in. */
|
|
67
|
-
var summary = d.querySelector('summary')
|
|
68
|
-
if (summary)
|
|
69
|
-
summary.addEventListener('click', function (e) {
|
|
70
|
-
if (d.open && openedByHover) {
|
|
71
|
-
e.preventDefault()
|
|
72
|
-
openedByHover = false
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The outline rail's focus-line ramp. It runs from near the top at scroll 0 to
|
|
3
|
-
* the viewport's bottom edge at max scroll, so the last heading is reachable
|
|
4
|
-
* however little content trails it. A ramp ending 120px short of the edge left
|
|
5
|
-
* a heading with under 120px after it permanently unmarked, since its top never
|
|
6
|
-
* fell below the line even at max scroll.
|
|
7
|
-
*
|
|
8
|
-
* The sidebar script embeds this function's own source, so a test exercises
|
|
9
|
-
* what the browser runs.
|
|
10
|
-
*/
|
|
11
|
-
export function focusLine(
|
|
12
|
-
scrollY: number,
|
|
13
|
-
max: number,
|
|
14
|
-
innerHeight: number,
|
|
15
|
-
): number {
|
|
16
|
-
if (max <= 0) return innerHeight
|
|
17
|
-
var progress = Math.min(1, Math.max(0, scrollY / max))
|
|
18
|
-
return 120 + progress * Math.max(0, innerHeight - 120)
|
|
19
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export function glossaryFilter(): void {
|
|
2
|
-
var input = document.getElementById('gfilter') as HTMLInputElement | null
|
|
3
|
-
var list = document.getElementById('gloss')
|
|
4
|
-
var count = document.getElementById('gloss-count')
|
|
5
|
-
if (!input || !list) return
|
|
6
|
-
var field = input
|
|
7
|
-
var terms = list
|
|
8
|
-
function updateGroups() {
|
|
9
|
-
terms.querySelectorAll('.gloss-group').forEach(function (heading) {
|
|
10
|
-
var el = heading.nextElementSibling as HTMLElement | null
|
|
11
|
-
var any = false
|
|
12
|
-
while (el && !el.classList.contains('gloss-group')) {
|
|
13
|
-
if (el.style.display !== 'none') any = true
|
|
14
|
-
el = el.nextElementSibling as HTMLElement | null
|
|
15
|
-
}
|
|
16
|
-
;(heading as HTMLElement).style.display = any ? '' : 'none'
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
field.addEventListener('input', function () {
|
|
20
|
-
var q = field.value.toLowerCase()
|
|
21
|
-
var n = 0
|
|
22
|
-
terms.querySelectorAll<HTMLElement>('.gterm').forEach(function (entry) {
|
|
23
|
-
var match = (entry.textContent as string).toLowerCase().includes(q)
|
|
24
|
-
entry.style.display = match ? '' : 'none'
|
|
25
|
-
if (match) n++
|
|
26
|
-
})
|
|
27
|
-
terms.classList.toggle('none', n === 0)
|
|
28
|
-
updateGroups()
|
|
29
|
-
if (count) {
|
|
30
|
-
var total = terms.querySelectorAll('.gterm').length
|
|
31
|
-
count.textContent =
|
|
32
|
-
(n === total ? total : n + ' of ' + total) +
|
|
33
|
-
(total === 1 ? ' term' : ' terms')
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
var clear = terms.querySelector('.clear')
|
|
37
|
-
if (clear) {
|
|
38
|
-
clear.addEventListener('click', function () {
|
|
39
|
-
field.value = ''
|
|
40
|
-
field.dispatchEvent(new Event('input'))
|
|
41
|
-
field.focus()
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The panel's state, settled before the first paint rather than after it. An
|
|
3
|
-
* arm that set the narrow default afterwards slid the panel in and back out on
|
|
4
|
-
* every load, and a restored custom width animated in from the default.
|
|
5
|
-
*
|
|
6
|
-
* A listing page starts shut because the body already lists what the panel
|
|
7
|
-
* would, and a lesson starts open because there the panel is the only
|
|
8
|
-
* cross-lesson navigation on the page. A stored preference beats both. A count
|
|
9
|
-
* threshold on its own was built and reverted, since it hides the panel on a
|
|
10
|
-
* lesson too.
|
|
11
|
-
*/
|
|
12
|
-
export function head(page: 'index' | 'lesson', breakpoint: number): void {
|
|
13
|
-
var r = document.documentElement
|
|
14
|
-
var s: string | null = null
|
|
15
|
-
r.dataset.page = page
|
|
16
|
-
if (matchMedia('(max-width: ' + breakpoint + 'px)').matches) {
|
|
17
|
-
r.classList.add('sb-shut')
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
try {
|
|
21
|
-
s = localStorage.getItem('teach-sb')
|
|
22
|
-
} catch {}
|
|
23
|
-
var idx = r.dataset.page === 'index'
|
|
24
|
-
if (s === 'shut' || (s === null && idx)) r.classList.add('sb-shut')
|
|
25
|
-
var w: string | null = null
|
|
26
|
-
try {
|
|
27
|
-
w = localStorage.getItem('teach-sb-w')
|
|
28
|
-
} catch {}
|
|
29
|
-
if (w) r.style.setProperty('--sb-w', w + 'px')
|
|
30
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What reveals feedback in a lesson written with button options. The radio
|
|
3
|
-
* shape needs none of it.
|
|
4
|
-
*/
|
|
5
|
-
export function quiz(): void {
|
|
6
|
-
document.querySelectorAll<HTMLElement>('.q').forEach(function (q) {
|
|
7
|
-
var f = q.querySelector('.fb') as HTMLElement
|
|
8
|
-
q.querySelectorAll<HTMLElement>('.opt').forEach(function (b) {
|
|
9
|
-
b.addEventListener('click', function () {
|
|
10
|
-
if (f.classList.contains('show')) return
|
|
11
|
-
q.querySelectorAll<HTMLElement>('.opt').forEach(function (o) {
|
|
12
|
-
o.dataset.state =
|
|
13
|
-
o.dataset.a === '1' ? 'right' : o === b ? 'chosen' : 'wrong'
|
|
14
|
-
})
|
|
15
|
-
f.classList.add('show')
|
|
16
|
-
})
|
|
17
|
-
})
|
|
18
|
-
})
|
|
19
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/* Spliced in by `canon teach up` at serve time and never written to a page, so
|
|
2
|
-
the server sends a message only once a refresh has landed on disk. */
|
|
3
|
-
export function reload(path: string): void {
|
|
4
|
-
var source = new EventSource(path)
|
|
5
|
-
source.onmessage = function () {
|
|
6
|
-
source.close()
|
|
7
|
-
location.reload()
|
|
8
|
-
}
|
|
9
|
-
}
|