@erclx/canon 5.52.0 → 5.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +11 -13
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -8
  5. package/claude/skills/context-fold/REQUIREMENT.md +0 -1
  6. package/claude/skills/design-identity/REQUIREMENT.md +2 -2
  7. package/claude/skills/design-identity/SKILL.md +6 -6
  8. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  9. package/claude/skills/design-record/SKILL.md +4 -4
  10. package/claude/skills/design-record/references/folder.md +1 -1
  11. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  12. package/claude/skills/design-taste/references/kinds.md +0 -1
  13. package/claude/skills/draw-figure/REQUIREMENT.md +3 -4
  14. package/claude/skills/draw-figure/SKILL.md +5 -6
  15. package/claude/skills/draw-figure/references/figures.md +0 -1
  16. package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
  17. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -5
  18. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  19. package/claude/skills/pick-by-eye/references/board.md +42 -0
  20. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  21. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  22. package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
  23. package/claude/skills/teach-craft/SKILL.md +70 -0
  24. package/claude/skills/teach-craft/references/adopted.md +113 -0
  25. package/claude/skills/teach-craft/references/lesson-craft.md +60 -0
  26. package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +41 -6
  27. package/docs/agents/audits.md +0 -1
  28. package/docs/agents/install-and-sync.md +0 -8
  29. package/docs/target/migrations.md +16 -2
  30. package/docs/target/projects.md +1 -1
  31. package/docs/workflow/parallel-features.md +1 -1
  32. package/docs/workflow/skill-map-on-request.md +10 -22
  33. package/docs/workflow/skill-map.md +11 -14
  34. package/docs/workflow/visual-design-workflow.md +1 -4
  35. package/governance/detect.toml +1 -6
  36. package/governance/renames.toml +0 -1
  37. package/package.json +2 -8
  38. package/scripts/core/regen-web-previews.ts +2 -67
  39. package/src/audits/catalog.ts +0 -1
  40. package/src/browser/engine.ts +3 -6
  41. package/src/capture/render.ts +1 -1
  42. package/src/claude/cases/authoring.ts +0 -12
  43. package/src/claude/cases/misc.ts +0 -22
  44. package/src/claude/cases/workflow.ts +2 -38
  45. package/src/claude/skills-audit.ts +1 -1
  46. package/src/cli/help.ts +0 -10
  47. package/src/cli.ts +0 -8
  48. package/src/commands/design.ts +16 -315
  49. package/src/commands/records.ts +0 -1
  50. package/src/commands/serve.ts +2 -2
  51. package/src/commands/sync.ts +4 -5
  52. package/src/design/components.ts +8 -1514
  53. package/src/design/css.ts +5 -6
  54. package/src/design/fonts.ts +3 -5
  55. package/src/design/regen.ts +5 -32
  56. package/src/design/render.ts +1 -1
  57. package/src/design/tokens.ts +3 -2
  58. package/src/driver/drive.ts +3 -4
  59. package/src/driver/steps.ts +2 -5
  60. package/src/gate/stages.ts +4 -10
  61. package/src/gov/audit.ts +1 -6
  62. package/src/inventory/walk.ts +1 -1
  63. package/src/markdown/scope.ts +6 -35
  64. package/src/records/validate.ts +4 -176
  65. package/src/sandbox/equivalence.ts +0 -5
  66. package/src/shipped/references.ts +1 -2
  67. package/src/sync/check.ts +4 -29
  68. package/src/sync/stamp.ts +6 -6
  69. package/src/sync/target.ts +1 -4
  70. package/tooling/claude/reference.md +1 -1
  71. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  72. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  73. package/claude/skills/canvas-board/SKILL.md +0 -143
  74. package/claude/skills/canvas-board/references/canvas.md +0 -170
  75. package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
  76. package/claude/skills/draft-and-pick/SKILL.md +0 -11
  77. package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
  78. package/claude/skills/draft-doc/SKILL.md +0 -11
  79. package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
  80. package/claude/skills/draft-figure/SKILL.md +0 -11
  81. package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
  82. package/claude/skills/draft-identity/SKILL.md +0 -11
  83. package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
  84. package/claude/skills/draft-ready/SKILL.md +0 -11
  85. package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
  86. package/claude/skills/draft-screencast/SKILL.md +0 -11
  87. package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
  88. package/claude/skills/draft-slides/SKILL.md +0 -11
  89. package/claude/skills/make-deck/REQUIREMENT.md +0 -36
  90. package/claude/skills/make-deck/SKILL.md +0 -80
  91. package/claude/skills/make-deck/references/deck.md +0 -183
  92. package/claude/skills/read-frames/REQUIREMENT.md +0 -30
  93. package/claude/skills/read-frames/SKILL.md +0 -55
  94. package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
  95. package/claude/skills/record-screencast/SKILL.md +0 -99
  96. package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
  97. package/claude/skills/script-screencast/SKILL.md +0 -70
  98. package/claude/skills/script-screencast/references/beats-template.md +0 -119
  99. package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
  100. package/claude/skills/teach-workspace/SKILL.md +0 -196
  101. package/claude/skills/teach-workspace/references/glossary.md +0 -90
  102. package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
  103. package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
  104. package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
  105. package/claude/skills/teach-workspace/references/promotion.md +0 -116
  106. package/claude/skills/teach-workspace/references/revisit.md +0 -28
  107. package/claude/skills/teach-workspace/references/teach.md +0 -211
  108. package/claude/skills/video-craft/REQUIREMENT.md +0 -35
  109. package/claude/skills/video-craft/SKILL.md +0 -96
  110. package/claude/skills/video-craft/references/adopted.md +0 -110
  111. package/governance/rules/ui/canvas.md +0 -13
  112. package/src/canvas/address.ts +0 -227
  113. package/src/canvas/arrange.ts +0 -66
  114. package/src/canvas/capture.ts +0 -391
  115. package/src/canvas/client/app.tsx +0 -188
  116. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  117. package/src/canvas/client/inspector/color-field.tsx +0 -398
  118. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  119. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  120. package/src/canvas/client/inspector/color-space.ts +0 -240
  121. package/src/canvas/client/inspector/field.tsx +0 -263
  122. package/src/canvas/client/inspector/fields.css +0 -763
  123. package/src/canvas/client/inspector/section.tsx +0 -27
  124. package/src/canvas/client/inspector/size-field.tsx +0 -69
  125. package/src/canvas/client/inspector/token-select.tsx +0 -109
  126. package/src/canvas/client/inspector/values.ts +0 -297
  127. package/src/canvas/client/inspector.tsx +0 -692
  128. package/src/canvas/client/keys.ts +0 -71
  129. package/src/canvas/client/layers.tsx +0 -118
  130. package/src/canvas/client/main.tsx +0 -71
  131. package/src/canvas/client/pages-panel.tsx +0 -212
  132. package/src/canvas/client/panel-handle.tsx +0 -90
  133. package/src/canvas/client/selection.tsx +0 -199
  134. package/src/canvas/client/shell.css +0 -762
  135. package/src/canvas/client/state.ts +0 -792
  136. package/src/canvas/client/surface.tsx +0 -803
  137. package/src/canvas/client/theme-panel.tsx +0 -55
  138. package/src/canvas/client/tools.tsx +0 -153
  139. package/src/canvas/content.ts +0 -781
  140. package/src/canvas/edit.ts +0 -575
  141. package/src/canvas/editing.ts +0 -203
  142. package/src/canvas/fit.ts +0 -238
  143. package/src/canvas/global.d.ts +0 -15
  144. package/src/canvas/history.ts +0 -116
  145. package/src/canvas/index.html +0 -20
  146. package/src/canvas/server.ts +0 -766
  147. package/src/canvas/tokens.ts +0 -221
  148. package/src/commands/canvas.ts +0 -1097
  149. package/src/commands/demo.ts +0 -542
  150. package/src/commands/slides.ts +0 -184
  151. package/src/commands/teach.ts +0 -1401
  152. package/src/demo/beats.ts +0 -135
  153. package/src/demo/compile.ts +0 -350
  154. package/src/demo/container.ts +0 -219
  155. package/src/demo/cursors.ts +0 -55
  156. package/src/demo/drive.ts +0 -442
  157. package/src/demo/pointer.ts +0 -178
  158. package/src/demo/theme.ts +0 -112
  159. package/src/demo/timeline.ts +0 -64
  160. package/src/design/adapter.ts +0 -59
  161. package/src/design/base.css +0 -203
  162. package/src/design/brand-favicon.ts +0 -18
  163. package/src/design/neutral.ts +0 -127
  164. package/src/design/project-fonts.ts +0 -392
  165. package/src/slides/convert/chart.ts +0 -165
  166. package/src/slides/convert/deck.ts +0 -244
  167. package/src/slides/convert/export.ts +0 -567
  168. package/src/slides/convert/master.ts +0 -198
  169. package/src/slides/convert/shapes.ts +0 -804
  170. package/src/slides/convert/svg.ts +0 -38
  171. package/src/slides/convert/walk.ts +0 -671
  172. package/src/slides/decks.ts +0 -110
  173. package/src/slides/open.ts +0 -18
  174. package/src/slides/package/fonts.ts +0 -301
  175. package/src/slides/package/motion.ts +0 -316
  176. package/src/slides/package/test-face.ts +0 -113
  177. package/src/teach/browser/compile.ts +0 -44
  178. package/src/teach/browser/dismiss.ts +0 -76
  179. package/src/teach/browser/focus-line.ts +0 -19
  180. package/src/teach/browser/glossary-filter.ts +0 -44
  181. package/src/teach/browser/head.ts +0 -30
  182. package/src/teach/browser/quiz.ts +0 -19
  183. package/src/teach/browser/reload.ts +0 -9
  184. package/src/teach/browser/sidebar.ts +0 -233
  185. package/src/teach/browser/theme.ts +0 -18
  186. package/src/teach/components/code.tsx +0 -24
  187. package/src/teach/components/heading.tsx +0 -11
  188. package/src/teach/components/list.tsx +0 -17
  189. package/src/teach/components/paragraph.tsx +0 -29
  190. package/src/teach/components/refs.tsx +0 -28
  191. package/src/teach/cross-references.ts +0 -78
  192. package/src/teach/fonts.ts +0 -16
  193. package/src/teach/highlight.ts +0 -71
  194. package/src/teach/html/jsx-dev-runtime.ts +0 -13
  195. package/src/teach/html/jsx-runtime.ts +0 -113
  196. package/src/teach/html/raw.ts +0 -28
  197. package/src/teach/lesson.ts +0 -178
  198. package/src/teach/nav.ts +0 -1337
  199. package/src/teach/render.ts +0 -240
  200. package/src/teach/workspace.ts +0 -1164
  201. package/src/teach/write-if-changed.ts +0 -17
@@ -1,110 +0,0 @@
1
- import { existsSync, readdirSync, statSync } from 'node:fs'
2
- import { join, relative, resolve } from 'node:path'
3
- import { creationRel, recordDir } from '@/roots/record'
4
- import { readDeck } from '@/slides/convert/deck'
5
-
6
- /**
7
- * A deck is a folder under the record root's `slides/`, holding its `.html`
8
- * slides, an optional `deck.json`, and its assets. `layouts/` beside the decks
9
- * holds the project's shared layouts and is never a deck itself.
10
- */
11
- export const SLIDES_FOLDER = 'slides'
12
-
13
- export const LAYOUTS_FOLDER = 'layouts'
14
-
15
- export interface Deck {
16
- readonly name: string
17
- /** From `deck.json`, falling back to the folder name as the export does. */
18
- readonly title: string
19
- readonly slides: number
20
- /** Relative to the project root. */
21
- readonly path: string
22
- }
23
-
24
- export type DeckRefusal = 'no-decks' | 'ambiguous' | 'no-deck'
25
-
26
- export type DeckResolution =
27
- | { readonly status: 'resolved'; readonly dir: string }
28
- | {
29
- readonly status: 'refused'
30
- readonly reason: DeckRefusal
31
- readonly message: string
32
- }
33
-
34
- /** Every `.html` file directly in the folder, in filename order. */
35
- export function slideFiles(dir: string): string[] {
36
- return readdirSync(dir, { withFileTypes: true })
37
- .filter((entry) => entry.isFile() && entry.name.endsWith('.html'))
38
- .map((entry) => entry.name)
39
- .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
40
- .map((name) => join(dir, name))
41
- }
42
-
43
- export function listDecks(root: string): Deck[] {
44
- const dir = recordDir(root, SLIDES_FOLDER)
45
- if (!isDirectory(dir)) return []
46
-
47
- return readdirSync(dir, { withFileTypes: true })
48
- .filter((entry) => entry.isDirectory() && entry.name !== LAYOUTS_FOLDER)
49
- .map((entry) => entry.name)
50
- .sort((a, b) => a.localeCompare(b))
51
- .flatMap((name) => {
52
- const deckDir = join(dir, name)
53
- const slides = slideFiles(deckDir).length
54
- if (slides === 0) return []
55
- const read = readDeck(deckDir)
56
- const title = read.status === 'read' ? read.deck.title : name
57
- return [{ name, title, slides, path: relative(root, deckDir) }]
58
- })
59
- }
60
-
61
- /**
62
- * The folder `render` reads. A deck name wins over a folder of the same name
63
- * relative to the root, and no name at all takes the only deck there is.
64
- */
65
- export function resolveDeck(
66
- root: string,
67
- target: string | undefined,
68
- ): DeckResolution {
69
- const decks = listDecks(root)
70
- const at = creationRel(root, SLIDES_FOLDER)
71
-
72
- if (target === undefined) {
73
- const [only, ...rest] = decks
74
- if (only === undefined) {
75
- return refuse('no-decks', `No deck under ${at}/. Draw one there first.`)
76
- }
77
- if (rest.length > 0) {
78
- return refuse(
79
- 'ambiguous',
80
- `${decks.length} decks under ${at}/, name one: ${names(decks)}`,
81
- )
82
- }
83
- return { status: 'resolved', dir: join(root, only.path) }
84
- }
85
-
86
- const named = decks.find((deck) => deck.name === target)
87
- if (named) return { status: 'resolved', dir: join(root, named.path) }
88
-
89
- const folder = resolve(root, target)
90
- if (isDirectory(folder)) return { status: 'resolved', dir: folder }
91
-
92
- return refuse(
93
- 'no-deck',
94
- decks.length === 0
95
- ? `${target} is neither a deck under ${at}/ nor a folder`
96
- : `${target} is neither a deck nor a folder. Decks: ${names(decks)}`,
97
- )
98
- }
99
-
100
- function names(decks: readonly Deck[]): string {
101
- return decks.map((deck) => deck.name).join(', ')
102
- }
103
-
104
- function refuse(reason: DeckRefusal, message: string): DeckResolution {
105
- return { status: 'refused', reason, message }
106
- }
107
-
108
- function isDirectory(path: string): boolean {
109
- return existsSync(path) && statSync(path).isDirectory()
110
- }
@@ -1,18 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { execa } from 'execa'
3
-
4
- export async function openDeck(path: string): Promise<boolean> {
5
- const windowsPath = await execa('wslpath', ['-w', path], { reject: false })
6
- if (windowsPath.exitCode === 0) {
7
- const cwd = existsSync('/mnt/c') ? '/mnt/c' : undefined
8
- const result = await execa(
9
- 'cmd.exe',
10
- ['/c', 'start', '', windowsPath.stdout.trim()],
11
- { cwd, reject: false },
12
- )
13
- return result.exitCode === 0
14
- }
15
- const opener = process.platform === 'darwin' ? 'open' : 'xdg-open'
16
- const result = await execa(opener, [path], { reject: false })
17
- return result.exitCode === 0
18
- }
@@ -1,301 +0,0 @@
1
- import type JSZip from 'jszip'
2
-
3
- /**
4
- * Embeds the deck's typeface in the written package, which pptxgenjs cannot do.
5
- * PowerPoint reads an embedded face as an Embedded OpenType 2.2 container
6
- * around the TrueType or OpenType data, stored as `ppt/fonts/fontN.fntdata`
7
- * and listed in `presentation.xml`. The header is built from the face's own
8
- * `OS/2`, `head`, and `name` tables, laid out per the W3C EOT submission:
9
- * https://www.w3.org/submissions/EOT/
10
- */
11
-
12
- export interface EmbedFace {
13
- readonly family: string
14
- readonly weight: number
15
- readonly style: 'normal' | 'italic'
16
- /** As `deck.json` wrote it, which every notice names. */
17
- readonly path: string
18
- readonly bytes: Uint8Array
19
- }
20
-
21
- export interface FontNotice {
22
- readonly path: string
23
- readonly message: string
24
- }
25
-
26
- export type EotResult =
27
- | { readonly status: 'embeddable'; readonly eot: Buffer }
28
- | { readonly status: 'refused'; readonly message: string }
29
-
30
- /** `fsType` bits from the OpenType `OS/2` table. */
31
- const RESTRICTED = 0x0002
32
- const BITMAP_ONLY = 0x0200
33
-
34
- const SFNT_VERSIONS = new Set([0x00010000, 0x74727565, 0x4f54544f])
35
- const WRAPPED: Readonly<Record<number, string>> = {
36
- 0x774f4632: 'woff2',
37
- 0x774f4646: 'woff',
38
- 0x74746366: 'a font collection',
39
- }
40
-
41
- const UNDECODABLE = 'it could not be decoded as a TrueType or OpenType font'
42
-
43
- interface Table {
44
- readonly offset: number
45
- readonly length: number
46
- }
47
-
48
- function tables(font: Buffer): Map<string, Table> {
49
- const found = new Map<string, Table>()
50
- const count = font.readUInt16BE(4)
51
- for (let index = 0; index < count; index += 1) {
52
- const at = 12 + index * 16
53
- const offset = font.readUInt32BE(at + 8)
54
- const length = font.readUInt32BE(at + 12)
55
- if (offset + length > font.length) throw new RangeError('table overruns')
56
- found.set(font.toString('latin1', at, at + 4), { offset, length })
57
- }
58
- return found
59
- }
60
-
61
- /**
62
- * The Windows platform's strings, in UTF-16, preferring US English where a
63
- * face carries several languages.
64
- */
65
- function names(font: Buffer, table: Table): Map<number, Buffer> {
66
- const found = new Map<number, { language: number; bytes: Buffer }>()
67
- const count = font.readUInt16BE(table.offset + 2)
68
- const strings = table.offset + font.readUInt16BE(table.offset + 4)
69
- for (let index = 0; index < count; index += 1) {
70
- const at = table.offset + 6 + index * 12
71
- if (font.readUInt16BE(at) !== 3) continue
72
- const language = font.readUInt16BE(at + 4)
73
- const id = font.readUInt16BE(at + 6)
74
- const length = font.readUInt16BE(at + 8)
75
- const start = strings + font.readUInt16BE(at + 10)
76
- if (start + length > font.length) throw new RangeError('name overruns')
77
- const held = found.get(id)
78
- if (held && (held.language === 0x0409 || language !== 0x0409)) continue
79
- const bytes = Buffer.from(font.subarray(start, start + length)).swap16()
80
- found.set(id, { language, bytes })
81
- }
82
- return new Map([...found].map(([id, { bytes }]) => [id, bytes]))
83
- }
84
-
85
- function sized(bytes: Buffer = Buffer.alloc(0)): Buffer {
86
- const size = Buffer.alloc(2)
87
- size.writeUInt16LE(bytes.length)
88
- return Buffer.concat([size, bytes])
89
- }
90
-
91
- function header(font: Buffer, os2: number, head: number): Buffer {
92
- const fixed = Buffer.alloc(80)
93
- fixed.writeUInt32LE(font.length, 4)
94
- fixed.writeUInt32LE(0x00020002, 8)
95
- font.copy(fixed, 16, os2 + 32, os2 + 42)
96
- fixed.writeUInt8(1, 26)
97
- fixed.writeUInt8(font.readUInt16BE(os2 + 62) & 1, 27)
98
- fixed.writeUInt32LE(font.readUInt16BE(os2 + 4), 28)
99
- fixed.writeUInt16LE(font.readUInt16BE(os2 + 8), 32)
100
- fixed.writeUInt16LE(0x504c, 34)
101
- for (let index = 0; index < 4; index += 1) {
102
- fixed.writeUInt32LE(font.readUInt32BE(os2 + 42 + index * 4), 36 + index * 4)
103
- }
104
- for (let index = 0; index < 2; index += 1) {
105
- fixed.writeUInt32LE(font.readUInt32BE(os2 + 78 + index * 4), 52 + index * 4)
106
- }
107
- fixed.writeUInt32LE(font.readUInt32BE(head + 8), 60)
108
- return fixed
109
- }
110
-
111
- /**
112
- * The EOT container around one face, or the reason it may not travel. A face
113
- * whose license restricts embedding or allows a bitmap only is refused, as is
114
- * a variable face, which PowerPoint does not embed. Editable and print-preview
115
- * embedding both pass, since the deck is meant to be edited.
116
- */
117
- export function eotOf(bytes: Uint8Array): EotResult {
118
- const font = Buffer.from(bytes)
119
- const refused = (message: string): EotResult => ({
120
- status: 'refused',
121
- message,
122
- })
123
- if (font.length < 12) return refused(UNDECODABLE)
124
- const version = font.readUInt32BE(0)
125
- const wrapped = WRAPPED[version]
126
- if (wrapped) {
127
- return refused(
128
- `it is ${wrapped}. Supply the TrueType or OpenType file instead`,
129
- )
130
- }
131
- if (!SFNT_VERSIONS.has(version)) return refused(UNDECODABLE)
132
- try {
133
- const found = tables(font)
134
- const os2 = found.get('OS/2')
135
- const head = found.get('head')
136
- const name = found.get('name')
137
- // Version 0 of OS/2 stops before the code page ranges the header copies.
138
- if (!os2 || os2.length < 86 || !head || head.length < 12 || !name) {
139
- return refused(UNDECODABLE)
140
- }
141
- const fsType = font.readUInt16BE(os2.offset + 8)
142
- if (fsType & RESTRICTED) {
143
- return refused('its license forbids embedding (fsType restricted)')
144
- }
145
- if (fsType & BITMAP_ONLY) {
146
- return refused('its license allows bitmap embedding only (fsType 0x0200)')
147
- }
148
- if (found.has('fvar')) {
149
- return refused(
150
- 'it is a variable font, which PowerPoint does not embed. Supply a static instance',
151
- )
152
- }
153
- const strings = names(font, name)
154
- const padding = Buffer.alloc(2)
155
- // RootStringCheckSum, then EUDCCodePage, Padding6, SignatureSize,
156
- // EUDCFlags, and EUDCFontSize, zero for a face with no EUDC data. The
157
- // checksum is the root string's byte sum XOR 0x50475342, so an empty root
158
- // string still writes the constant, and PowerPoint refuses to install a
159
- // face whose checksum is zero as tampered with.
160
- const tail = Buffer.alloc(20)
161
- tail.writeUInt32LE(0x50475342, 0)
162
- const eot = Buffer.concat([
163
- header(font, os2.offset, head.offset),
164
- padding,
165
- sized(strings.get(1)),
166
- padding,
167
- sized(strings.get(2)),
168
- padding,
169
- sized(strings.get(5)),
170
- padding,
171
- sized(strings.get(4)),
172
- padding,
173
- sized(),
174
- tail,
175
- font,
176
- ])
177
- eot.writeUInt32LE(eot.length, 0)
178
- return { status: 'embeddable', eot }
179
- } catch (error) {
180
- if (error instanceof RangeError) return refused(UNDECODABLE)
181
- throw error
182
- }
183
- }
184
-
185
- type Slot = 'regular' | 'bold' | 'italic' | 'boldItalic'
186
-
187
- /** The child order `<p:embeddedFont>` requires. */
188
- const SLOTS: readonly Slot[] = ['regular', 'bold', 'italic', 'boldItalic']
189
-
190
- /** PowerPoint keeps four faces a family, so a weight from 600 reads as bold. */
191
- function slotOf(face: EmbedFace): Slot {
192
- const isBold = face.weight >= 600
193
- if (face.style === 'italic') return isBold ? 'boldItalic' : 'italic'
194
- return isBold ? 'bold' : 'regular'
195
- }
196
-
197
- const FONT_RELATIONSHIP =
198
- 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/font'
199
- const NOTES_SIZE = /<p:notesSz\b[^>]*\/>/
200
-
201
- const escapeXml = (value: string): string =>
202
- value
203
- .replaceAll('&', '&amp;')
204
- .replaceAll('"', '&quot;')
205
- .replaceAll('<', '&lt;')
206
-
207
- /**
208
- * Embeds each face the deck lists and returns one notice per face left out.
209
- * The font list goes after `<p:notesSz>`, where the schema orders it, and a
210
- * package missing that anchor embeds nothing rather than placing it elsewhere.
211
- */
212
- export async function embedFonts(
213
- zip: JSZip,
214
- faces: readonly EmbedFace[],
215
- ): Promise<FontNotice[]> {
216
- const notices: FontNotice[] = []
217
- const families = new Map<string, Map<Slot, number>>()
218
- const parts: Buffer[] = []
219
-
220
- for (const face of faces) {
221
- const result = eotOf(face.bytes)
222
- if (result.status === 'refused') {
223
- notices.push({ path: face.path, message: result.message })
224
- continue
225
- }
226
- const slots = families.get(face.family) ?? new Map<Slot, number>()
227
- const slot = slotOf(face)
228
- if (slots.has(slot)) {
229
- notices.push({
230
- path: face.path,
231
- message: `${face.family} already embeds a ${slot} face, and PowerPoint holds one per slot`,
232
- })
233
- continue
234
- }
235
- parts.push(result.eot)
236
- slots.set(slot, parts.length)
237
- families.set(face.family, slots)
238
- }
239
- if (parts.length === 0) return notices
240
-
241
- const presentationPath = 'ppt/presentation.xml'
242
- const relsPath = 'ppt/_rels/presentation.xml.rels'
243
- const typesPath = '[Content_Types].xml'
244
- const [presentation, rels, types] = await Promise.all(
245
- [presentationPath, relsPath, typesPath].map(
246
- async (path) => (await zip.file(path)?.async('string')) ?? '',
247
- ),
248
- )
249
- if (!presentation || !NOTES_SIZE.test(presentation)) {
250
- const message = 'the deck has no <p:notesSz> to anchor the font list after'
251
- return [
252
- ...notices,
253
- ...faces
254
- .filter((face) => !notices.some((notice) => notice.path === face.path))
255
- .map((face) => ({ path: face.path, message })),
256
- ]
257
- }
258
-
259
- const id = (part: number): string => `rIdCanonFont${part}`
260
- parts.forEach((eot, index) => {
261
- zip.file(`ppt/fonts/font${index + 1}.fntdata`, eot)
262
- })
263
- const relationships = parts
264
- .map(
265
- (_, index) =>
266
- `<Relationship Id="${id(index + 1)}" Type="${FONT_RELATIONSHIP}" Target="fonts/font${index + 1}.fntdata"/>`,
267
- )
268
- .join('')
269
- zip.file(
270
- relsPath,
271
- rels.replace('</Relationships>', `${relationships}</Relationships>`),
272
- )
273
- if (!types.includes('Extension="fntdata"')) {
274
- zip.file(
275
- typesPath,
276
- types.replace(
277
- /(<Types\b[^>]*>)/,
278
- '$1<Default Extension="fntdata" ContentType="application/x-fontdata"/>',
279
- ),
280
- )
281
- }
282
- const list = [...families]
283
- .map(([family, slots]) => {
284
- const children = SLOTS.flatMap((slot) => {
285
- const part = slots.get(slot)
286
- return part ? [`<p:${slot} r:id="${id(part)}"/>`] : []
287
- })
288
- return `<p:embeddedFont><p:font typeface="${escapeXml(family)}"/>${children.join('')}</p:embeddedFont>`
289
- })
290
- .join('')
291
- zip.file(
292
- presentationPath,
293
- presentation
294
- .replace(
295
- NOTES_SIZE,
296
- (anchor) => `${anchor}<p:embeddedFontLst>${list}</p:embeddedFontLst>`,
297
- )
298
- .replace('<p:presentation ', '<p:presentation embedTrueTypeFonts="1" '),
299
- )
300
- return notices
301
- }