@tycoworks/tycoslide 0.7.0 → 0.9.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 (62) hide show
  1. package/README.md +6 -6
  2. package/SKILL.md +2 -1
  3. package/dist/cli.js +8 -107
  4. package/dist/engine/dom.d.ts +5 -0
  5. package/dist/engine/dom.js +19 -3
  6. package/dist/engine/fillers/filler.d.ts +21 -13
  7. package/dist/engine/fillers/filler.js +21 -27
  8. package/dist/engine/fillers/image.d.ts +46 -7
  9. package/dist/engine/fillers/image.js +78 -36
  10. package/dist/engine/fillers/table.d.ts +4 -3
  11. package/dist/engine/fillers/table.js +58 -9
  12. package/dist/engine/generate.d.ts +34 -18
  13. package/dist/engine/generate.js +234 -65
  14. package/dist/engine/index.d.ts +3 -2
  15. package/dist/engine/index.js +1 -1
  16. package/dist/engine/notes.d.ts +76 -0
  17. package/dist/engine/notes.js +313 -0
  18. package/dist/engine/types.d.ts +57 -24
  19. package/dist/engine/types.js +11 -7
  20. package/dist/index.d.ts +19 -25
  21. package/dist/index.js +65 -92
  22. package/dist/manifest.js +19 -29
  23. package/dist/markdown/blocks/code.d.ts +15 -0
  24. package/dist/markdown/blocks/code.js +50 -0
  25. package/dist/markdown/blocks/image.d.ts +2 -0
  26. package/dist/markdown/blocks/image.js +9 -0
  27. package/dist/markdown/blocks/mermaid.d.ts +15 -0
  28. package/dist/markdown/blocks/mermaid.js +227 -0
  29. package/dist/markdown/{resolvers → blocks}/mermaidTheme.d.ts +1 -1
  30. package/dist/markdown/{resolvers → blocks}/mermaidTheme.js +1 -1
  31. package/dist/markdown/blocks/registry.d.ts +16 -0
  32. package/dist/markdown/blocks/registry.js +44 -0
  33. package/dist/markdown/blocks/table.d.ts +2 -0
  34. package/dist/markdown/blocks/table.js +23 -0
  35. package/dist/markdown/blocks/text.d.ts +12 -0
  36. package/dist/markdown/blocks/text.js +90 -0
  37. package/dist/markdown/deckCompiler.d.ts +18 -30
  38. package/dist/markdown/deckCompiler.js +167 -122
  39. package/dist/markdown/index.d.ts +11 -11
  40. package/dist/markdown/index.js +9 -8
  41. package/dist/markdown/inline.d.ts +26 -0
  42. package/dist/markdown/inline.js +136 -0
  43. package/dist/markdown/mdast.d.ts +25 -0
  44. package/dist/markdown/mdast.js +49 -0
  45. package/dist/markdown/schema/deckSchema.d.ts +30 -0
  46. package/dist/markdown/schema/deckSchema.js +51 -0
  47. package/dist/markdown/schema/strict.d.ts +9 -0
  48. package/dist/markdown/schema/strict.js +18 -0
  49. package/dist/markdown/schema/themeConfigSchema.d.ts +99 -0
  50. package/dist/markdown/schema/themeConfigSchema.js +145 -0
  51. package/dist/markdown/types.d.ts +184 -137
  52. package/dist/markdown/types.js +30 -19
  53. package/package.json +7 -3
  54. package/syntax.md +25 -5
  55. package/dist/markdown/parsers.d.ts +0 -32
  56. package/dist/markdown/parsers.js +0 -233
  57. package/dist/markdown/resolvers/code.d.ts +0 -17
  58. package/dist/markdown/resolvers/code.js +0 -44
  59. package/dist/markdown/resolvers/mermaid.d.ts +0 -14
  60. package/dist/markdown/resolvers/mermaid.js +0 -89
  61. package/dist/markdown/resolvers/resolver.d.ts +0 -42
  62. package/dist/markdown/resolvers/resolver.js +0 -52
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Speaker-notes injection — an in-band pass run per slide during `write()`.
3
+ *
4
+ * The render library (pptx-automizer) has no public notes writer, but it does
5
+ * expose a general seam: `slide.modify((document, parent) => …)` runs while the
6
+ * deck is being written and hands us `parent.targetArchive` (the OUTPUT archive)
7
+ * and `parent.targetNumber` (the real output slide number). automizer's clone
8
+ * auto-copies the source slide's notes as `notesSlide${targetNumber}.xml` and
9
+ * wires its rels + content-type BEFORE our callback runs, so notes map 1:1 to the
10
+ * output slide number — no `sldIdLst`→rels→slideK mapping is needed.
11
+ *
12
+ * For each slide we either overwrite/synthesize its `notesSlide${N}.xml` (when the
13
+ * step authored notes) or strip any auto-copied part (when it did not, or notes
14
+ * are excluded) — so the designer's template notes never leak into the output.
15
+ */
16
+ import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
17
+ import { Attr, buildParagraph, buildRun, nextFreeRId as nextFreeRIdForDoc, Tag } from "./dom.js";
18
+ // Package-level OOXML names not covered by dom.ts's `Tag`/`Attr` (which name
19
+ // slide-XML tokens): `[Content_Types].xml` Override entries and the tail
20
+ // segments of relationship-type URLs.
21
+ const CT_OVERRIDE = "Override";
22
+ const CT_PART_NAME = "PartName";
23
+ const CT_CONTENT_TYPE = "ContentType";
24
+ const REL_TYPE_SUFFIX = {
25
+ NotesSlide: "/notesSlide",
26
+ NotesMaster: "/notesMaster",
27
+ };
28
+ const NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main";
29
+ const NS_P = "http://schemas.openxmlformats.org/presentationml/2006/main";
30
+ const NS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
31
+ const NS_REL = "http://schemas.openxmlformats.org/package/2006/relationships";
32
+ const NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
33
+ const REL_TYPE = {
34
+ NotesSlide: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide",
35
+ Slide: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide",
36
+ NotesMaster: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster",
37
+ };
38
+ const NOTES_SLIDE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml";
39
+ const XML_DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n';
40
+ const CONTENT_TYPES_PART = "[Content_Types].xml";
41
+ const PRESENTATION_RELS_PART = "ppt/_rels/presentation.xml.rels";
42
+ const NOTES_SKELETON = `<p:notes xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}"><p:cSld><p:spTree>` +
43
+ `<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>` +
44
+ `<p:sp><p:nvSpPr><p:cNvPr id="2" name="Notes Placeholder 1"/>` +
45
+ `<p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr>` +
46
+ `<p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/></p:txBody></p:sp>` +
47
+ `</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:notes>`;
48
+ /**
49
+ * Build a minimal `<p:notes>` part whose body placeholder carries one `<a:p>`
50
+ * per line of `notes`. Text goes through DOM text nodes, so `& < > "` escaping
51
+ * is handled by the serializer.
52
+ */
53
+ export function buildNotesSlideXml(notes) {
54
+ const doc = new DOMParser().parseFromString(NOTES_SKELETON, "text/xml");
55
+ const txBody = doc.getElementsByTagName("p:txBody")[0];
56
+ for (const line of notes.replace(/\r?\n+$/, "").split(/\r?\n/)) {
57
+ // Reuse the engine's run/paragraph builders — they set xml:space="preserve"
58
+ // for leading/trailing whitespace. The skeleton declares xmlns:a, so the
59
+ // builders' "a:"-prefixed element creation serializes correctly on this doc.
60
+ txBody.appendChild(buildParagraph(doc, null, buildRun(doc, null, line)));
61
+ }
62
+ return serialize(doc);
63
+ }
64
+ /** Next unused `rIdN` in a `.rels` XML string (max existing id + 1). */
65
+ export function nextFreeRId(relsXml) {
66
+ return nextFreeRIdForDoc(new DOMParser().parseFromString(relsXml, "text/xml"));
67
+ }
68
+ function serialize(doc) {
69
+ // xmldom re-emits any `<?xml?>` declaration present in the parsed source, so a
70
+ // doc parsed from an already-declared part would serialize with two of them.
71
+ // Strip any leading declaration, then prepend exactly one canonical XML_DECL.
72
+ const body = new XMLSerializer().serializeToString(doc).replace(/^<\?xml[^>]*\?>\s*/, "");
73
+ return XML_DECL + body;
74
+ }
75
+ function parse(xml) {
76
+ return new DOMParser().parseFromString(xml, "text/xml");
77
+ }
78
+ async function readString(archive, file) {
79
+ const raw = await archive.read(file, "string");
80
+ return typeof raw === "string" ? raw : raw.toString("utf-8");
81
+ }
82
+ function appendRel(relsDoc, id, type, target) {
83
+ const rel = relsDoc.createElementNS(NS_REL, Tag.RELATIONSHIP);
84
+ rel.setAttribute(Attr.ID, id);
85
+ rel.setAttribute(Attr.TYPE, type);
86
+ rel.setAttribute(Attr.TARGET, target);
87
+ relsDoc.documentElement.appendChild(rel);
88
+ }
89
+ /** Remove the (single) notesSlide relationship from a slide's rels doc, if present. */
90
+ function removeNotesRel(relsDoc) {
91
+ const rels = relsDoc.getElementsByTagName(Tag.RELATIONSHIP);
92
+ for (let i = 0; i < rels.length; i++) {
93
+ if (rels[i].getAttribute(Attr.TYPE) === REL_TYPE.NotesSlide) {
94
+ rels[i].parentNode?.removeChild(rels[i]);
95
+ return true;
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+ function notesRelsXml(slideNumber, masterTarget) {
101
+ return (XML_DECL +
102
+ `<Relationships xmlns="${NS_REL}">` +
103
+ `<Relationship Id="rId1" Type="${REL_TYPE.Slide}" Target="../slides/slide${slideNumber}.xml"/>` +
104
+ `<Relationship Id="rId2" Type="${REL_TYPE.NotesMaster}" Target="${masterTarget}"/>` +
105
+ `</Relationships>`);
106
+ }
107
+ /**
108
+ * The notes master's target relative to `ppt/notesSlides/` (what a notesSlide's
109
+ * rels needs), read from the package's `presentation.xml.rels`. That file's
110
+ * targets are relative to `ppt/` (e.g. `notesMasters/notesMaster1.xml`), so a
111
+ * notesSlide part one directory deeper prefixes `../`. Returns undefined when
112
+ * the presentation declares no notes master relationship.
113
+ */
114
+ async function findNotesMaster(archive) {
115
+ if (!archive.fileExists(PRESENTATION_RELS_PART))
116
+ return undefined;
117
+ const relsDoc = parse(await readString(archive, PRESENTATION_RELS_PART));
118
+ const rels = relsDoc.getElementsByTagName(Tag.RELATIONSHIP);
119
+ for (let i = 0; i < rels.length; i++) {
120
+ if (rels[i].getAttribute(Attr.TYPE)?.endsWith(REL_TYPE_SUFFIX.NotesMaster)) {
121
+ return `../${rels[i].getAttribute(Attr.TARGET)}`;
122
+ }
123
+ }
124
+ return undefined;
125
+ }
126
+ function findOverride(ctDoc, partName) {
127
+ const overrides = ctDoc.getElementsByTagName(CT_OVERRIDE);
128
+ for (let i = 0; i < overrides.length; i++) {
129
+ if (overrides[i].getAttribute(CT_PART_NAME) === partName)
130
+ return overrides[i];
131
+ }
132
+ return undefined;
133
+ }
134
+ async function addContentTypeOverride(archive, partName) {
135
+ const ctDoc = parse(await readString(archive, CONTENT_TYPES_PART));
136
+ if (findOverride(ctDoc, partName))
137
+ return;
138
+ const override = ctDoc.createElementNS(NS_CT, CT_OVERRIDE);
139
+ override.setAttribute(CT_PART_NAME, partName);
140
+ override.setAttribute(CT_CONTENT_TYPE, NOTES_SLIDE_CONTENT_TYPE);
141
+ ctDoc.documentElement.appendChild(override);
142
+ await archive.write(CONTENT_TYPES_PART, serialize(ctDoc));
143
+ }
144
+ /** Remove a part's `<Override>` from an already-parsed `[Content_Types].xml` doc. */
145
+ function removeOverrideFromDoc(ctDoc, partName) {
146
+ const override = findOverride(ctDoc, partName);
147
+ if (!override)
148
+ return false;
149
+ override.parentNode?.removeChild(override);
150
+ return true;
151
+ }
152
+ async function removeContentTypeOverride(archive, partName) {
153
+ const ctDoc = parse(await readString(archive, CONTENT_TYPES_PART));
154
+ if (!removeOverrideFromDoc(ctDoc, partName))
155
+ return;
156
+ await archive.write(CONTENT_TYPES_PART, serialize(ctDoc));
157
+ }
158
+ /** Resolve `target` (from a rels file) against `baseDir`, collapsing `.`/`..`. */
159
+ function resolveRelative(baseDir, target) {
160
+ const parts = baseDir.split("/").filter(Boolean);
161
+ for (const seg of target.split("/")) {
162
+ if (seg === "" || seg === ".")
163
+ continue;
164
+ if (seg === "..")
165
+ parts.pop();
166
+ else
167
+ parts.push(seg);
168
+ }
169
+ return parts.join("/");
170
+ }
171
+ const NOTES_SLIDE_PART = /^ppt\/notesSlides\/notesSlide\d+\.xml$/;
172
+ const PRESENTATION_PART = "ppt/presentation.xml";
173
+ /**
174
+ * The slide parts a deck actually presents, in `<p:sldIdLst>` order, resolved via
175
+ * `presentation.xml.rels`. Returns full archive paths (e.g. `ppt/slides/slide1.xml`).
176
+ *
177
+ * This deliberately does NOT enumerate `ppt/slides/_rels`: when automizer builds a
178
+ * deck it removes the template's original slides from `<p:sldIdLst>` first, but
179
+ * leaves their physical `slideN.xml`/`slideN.xml.rels` parts in the archive until
180
+ * a later cleanup pass. Those orphaned slide rels still carry `/notesSlide`
181
+ * relationships, so folder-enumeration would wrongly count template notes as
182
+ * referenced. Only slides reachable from `presentation.xml` are live.
183
+ */
184
+ async function liveSlideParts(archive) {
185
+ const pres = parse(await readString(archive, PRESENTATION_PART));
186
+ const sldIds = pres.getElementsByTagName("p:sldId");
187
+ const rIds = [];
188
+ for (let i = 0; i < sldIds.length; i++) {
189
+ const rid = sldIds[i].getAttributeNS(NS_R, "id") || sldIds[i].getAttribute(Attr.HLINK_ID);
190
+ if (rid)
191
+ rIds.push(rid);
192
+ }
193
+ const relsDoc = parse(await readString(archive, PRESENTATION_RELS_PART));
194
+ const rels = relsDoc.getElementsByTagName(Tag.RELATIONSHIP);
195
+ const targetById = new Map();
196
+ for (let i = 0; i < rels.length; i++) {
197
+ const id = rels[i].getAttribute(Attr.ID);
198
+ const target = rels[i].getAttribute(Attr.TARGET);
199
+ if (id && target)
200
+ targetById.set(id, target);
201
+ }
202
+ const parts = [];
203
+ for (const rid of rIds) {
204
+ const target = targetById.get(rid);
205
+ if (target)
206
+ parts.push(resolveRelative("ppt", target));
207
+ }
208
+ return parts;
209
+ }
210
+ /** The `_rels` path for an archive part (`ppt/slides/slide1.xml` → `ppt/slides/_rels/slide1.xml.rels`). */
211
+ function relsPathFor(part) {
212
+ return part.replace(/\/([^/]+)$/, "/_rels/$1.rels");
213
+ }
214
+ /**
215
+ * Remove every notesSlide part that no live slide references. automizer drops the
216
+ * template's orphaned *slides* from the deck when building it but leaves all their
217
+ * `notesSlides` behind as unreferenced parts (plus their `[Content_Types].xml`
218
+ * Overrides), so the designer's private notes would otherwise leak into every
219
+ * output. Run once as a presentation-level pass, after the deck is assembled.
220
+ *
221
+ * A notesSlide is "referenced" iff a LIVE slide (reachable from `presentation.xml`,
222
+ * see {@link liveSlideParts}) carries a `/notesSlide` relationship pointing at it.
223
+ * Everything else is swept: the part, its rels, and its content-type Override (all
224
+ * Override removals batched into a single `[Content_Types].xml` read+write).
225
+ */
226
+ export async function sweepOrphanNotes(archive) {
227
+ const notesParts = (await archive.folder("ppt/notesSlides"))
228
+ .map((entry) => entry.name)
229
+ .filter((name) => NOTES_SLIDE_PART.test(name));
230
+ if (notesParts.length === 0)
231
+ return;
232
+ const referenced = new Set();
233
+ for (const slidePart of await liveSlideParts(archive)) {
234
+ const relsPath = relsPathFor(slidePart);
235
+ if (!archive.fileExists(relsPath))
236
+ continue;
237
+ const rels = parse(await readString(archive, relsPath)).getElementsByTagName(Tag.RELATIONSHIP);
238
+ for (let i = 0; i < rels.length; i++) {
239
+ const target = rels[i].getAttribute(Attr.TARGET);
240
+ if (target && rels[i].getAttribute(Attr.TYPE)?.endsWith(REL_TYPE_SUFFIX.NotesSlide)) {
241
+ referenced.add(resolveRelative("ppt/slides", target));
242
+ }
243
+ }
244
+ }
245
+ const orphans = notesParts.filter((part) => !referenced.has(part));
246
+ if (orphans.length === 0)
247
+ return;
248
+ const ctDoc = parse(await readString(archive, CONTENT_TYPES_PART));
249
+ let ctChanged = false;
250
+ for (const part of orphans) {
251
+ await archive.remove(part);
252
+ const relsPart = `ppt/notesSlides/_rels/${part.slice(part.lastIndexOf("/") + 1)}.rels`;
253
+ if (archive.fileExists(relsPart))
254
+ await archive.remove(relsPart);
255
+ if (removeOverrideFromDoc(ctDoc, `/${part}`))
256
+ ctChanged = true;
257
+ }
258
+ if (ctChanged)
259
+ await archive.write(CONTENT_TYPES_PART, serialize(ctDoc));
260
+ }
261
+ /**
262
+ * Set (or strip) the speaker notes on one output slide, keyed on its real output
263
+ * `slideNumber` (`parent.targetNumber`). Runs inside `slide.modify` during
264
+ * `write()`, operating on the OUTPUT archive.
265
+ *
266
+ * - Authored notes (non-empty and `!excludeNotes`): if automizer auto-copied a
267
+ * notes part (`notesSlide${N}.xml` already exists), overwrite its text only —
268
+ * its rels and content-type are already correct. Otherwise synthesize the part,
269
+ * its rels (→slide + →master), a `notesSlide` rel on the slide (next free rId),
270
+ * and the `[Content_Types].xml` override.
271
+ * - No notes (or excluded): if a part was auto-copied, remove it, its rels, the
272
+ * slide's `notesSlide` rel, and the content-type override — so the designer's
273
+ * template notes never leak.
274
+ *
275
+ * Throws (no silent default) if a slide needs a NEW notes part but the template
276
+ * ships no notes master — synthesizing one is out of scope.
277
+ */
278
+ export async function applyNotesToSlide(archive, slideNumber, notes, excludeNotes) {
279
+ const notesPath = `ppt/notesSlides/notesSlide${slideNumber}.xml`;
280
+ const notesRelsPath = `ppt/notesSlides/_rels/notesSlide${slideNumber}.xml.rels`;
281
+ const slideRelsPath = `ppt/slides/_rels/slide${slideNumber}.xml.rels`;
282
+ const notesExists = archive.fileExists(notesPath);
283
+ const shouldWrite = notes !== undefined && notes !== "" && !excludeNotes;
284
+ if (shouldWrite) {
285
+ if (notesExists) {
286
+ // Auto-copied part: overwrite its body; leave rels + content-type as wired.
287
+ await archive.write(notesPath, buildNotesSlideXml(notes));
288
+ return;
289
+ }
290
+ const masterTarget = await findNotesMaster(archive);
291
+ if (masterTarget === undefined) {
292
+ throw new Error(`Speaker notes: slide ${slideNumber} has notes, but the template ships no notes master ` +
293
+ "(ppt/notesMasters/). Add a notes master to the template (PowerPoint: View → Notes Master), " +
294
+ "or build with notes disabled. Synthesizing a notes master is not supported.");
295
+ }
296
+ await archive.write(notesPath, buildNotesSlideXml(notes));
297
+ await archive.write(notesRelsPath, notesRelsXml(slideNumber, masterTarget));
298
+ const relsDoc = parse(await readString(archive, slideRelsPath));
299
+ appendRel(relsDoc, nextFreeRIdForDoc(relsDoc), REL_TYPE.NotesSlide, `../notesSlides/notesSlide${slideNumber}.xml`);
300
+ await archive.write(slideRelsPath, serialize(relsDoc));
301
+ await addContentTypeOverride(archive, `/${notesPath}`);
302
+ return;
303
+ }
304
+ if (!notesExists)
305
+ return;
306
+ await archive.remove(notesPath);
307
+ if (archive.fileExists(notesRelsPath))
308
+ await archive.remove(notesRelsPath);
309
+ const relsDoc = parse(await readString(archive, slideRelsPath));
310
+ if (removeNotesRel(relsDoc))
311
+ await archive.write(slideRelsPath, serialize(relsDoc));
312
+ await removeContentTypeOverride(archive, `/${notesPath}`);
313
+ }
@@ -1,11 +1,3 @@
1
- /** How the engine should scale an image inside its picture frame. */
2
- export declare const FitMode: {
3
- /** Shrink the picture frame to the image's aspect ratio, centered. */
4
- readonly Contain: "contain";
5
- /** Fill the frame; center-crop overflow via srcRect. */
6
- readonly Cover: "cover";
7
- };
8
- export type FitMode = (typeof FitMode)[keyof typeof FitMode];
9
1
  /**
10
2
  * Fill-strategy discriminator carried on every Slot. Required — there is no
11
3
  * silent default. The engine dispatches to fillTemplate / fillText / fillTable /
@@ -80,37 +72,78 @@ export type TableFill = {
80
72
  rows: StyledParagraph[][];
81
73
  };
82
74
  /**
83
- * Input to fillImage a resolved image path plus its fit mode. `path` must
84
- * be an absolute filesystem path; the compiler is responsible for resolution
85
- * before an ImageFill reaches the engine. Relative paths fail loudly at
86
- * `readFileSync` / `imageSize` in the engine.
75
+ * How a picture is scaled into its frame the engine's image-sizing directive,
76
+ * mirroring CSS `object-fit`. `contain`: fit the whole image, scale both ways,
77
+ * letterbox. `cover`: fill the frame, centre-crop the overflow. `scale-down`:
78
+ * like contain but never enlarge past native (a small image sits at native size).
79
+ */
80
+ export declare const ImageFit: {
81
+ readonly Contain: "contain";
82
+ readonly Cover: "cover";
83
+ readonly ScaleDown: "scale-down";
84
+ };
85
+ export type ImageFit = (typeof ImageFit)[keyof typeof ImageFit];
86
+ /**
87
+ * Input to fillImage — a resolved image path plus its object-fit directive.
88
+ * `path` must be absolute; the compiler resolves it (and maps the asset's
89
+ * semantic type to a `fit`) before the ImageFill reaches the engine.
87
90
  */
88
91
  export type ImageFill = {
89
92
  type: typeof SlotType.Image;
90
93
  path: string;
91
- fit: FitMode;
94
+ fit: ImageFit;
92
95
  };
93
- export type Slot = {
94
- key: string;
95
- shapeName: string;
96
- /** Fill strategy discriminator — required, no silent default. */
96
+ /** A shape's absolute position and size, in EMU — the slot's frame. */
97
+ export type Frame = {
98
+ x: number;
99
+ y: number;
100
+ cx: number;
101
+ cy: number;
102
+ };
103
+ /**
104
+ * A kind of content a slot accepts, and the real template shape that realizes
105
+ * it. `type` is the fill-strategy discriminator; `shapeName` names the shape on
106
+ * `sourceSlide` that carries the specimen styling. When `sourceSlide` equals the
107
+ * layout's `baseSlide` the shape is already on the cloned slide (fill in place);
108
+ * otherwise the shape is transplanted from `sourceSlide` into the slot's frame.
109
+ * `startAt` is a text-specimen concern (leave the first N specimen paragraphs
110
+ * untouched) and only meaningful on a text block.
111
+ *
112
+ * Named `Block` — a kind of content (image / table / text) the way an author
113
+ * thinks of it. Distinct from the compiler's `MarkdownBlock` (a parsed markdown
114
+ * block); different layer, kept separate on purpose.
115
+ */
116
+ export type Block = {
97
117
  type: SlotType;
98
- /** Leave the first N specimen paragraphs untouched (fillText only). */
118
+ sourceSlide: number;
119
+ shapeName: string;
99
120
  startAt?: number;
100
- /** Enforced column count for table slots. */
101
- columns?: number;
121
+ };
122
+ /**
123
+ * An author-facing fill region. Not welded to one shape+type: a slot owns its
124
+ * `frame` and `accepts` a set of `Block`s; the supplied value's shape selects
125
+ * which block fills. A block whose `sourceSlide === baseSlide` fills in place;
126
+ * any other block is transplanted into the slot's `frame`.
127
+ */
128
+ export type Slot = {
129
+ key: string;
130
+ frame: Frame;
131
+ accepts: Block[];
102
132
  };
103
133
  export type Layout = {
104
134
  name: string;
105
- slideNumber: number;
106
- description: string;
107
- whenToUse: string;
108
- whenNotToUse: string;
135
+ /**
136
+ * The template slide cloned for chrome/background. A block whose `sourceSlide`
137
+ * equals this is filled in place; any other block is transplanted onto the clone.
138
+ */
139
+ baseSlide: number;
109
140
  slots: Slot[];
110
141
  };
111
142
  export type DeckStep = {
112
143
  layout: string;
113
144
  content?: Record<string, TextFill | TableFill | ImageFill | TemplateFill>;
145
+ /** Slide-level speaker notes — plain text, one paragraph per line. Not a slot. */
146
+ notes?: string;
114
147
  };
115
148
  export type Deck = {
116
149
  theme: string;
@@ -1,10 +1,3 @@
1
- /** How the engine should scale an image inside its picture frame. */
2
- export const FitMode = {
3
- /** Shrink the picture frame to the image's aspect ratio, centered. */
4
- Contain: "contain",
5
- /** Fill the frame; center-crop overflow via srcRect. */
6
- Cover: "cover",
7
- };
8
1
  /**
9
2
  * Fill-strategy discriminator carried on every Slot. Required — there is no
10
3
  * silent default. The engine dispatches to fillTemplate / fillText / fillTable /
@@ -20,3 +13,14 @@ export const SlotType = {
20
13
  /** fillImage: swap the picture's blip and adjust frame geometry. */
21
14
  Image: "image",
22
15
  };
16
+ /**
17
+ * How a picture is scaled into its frame — the engine's image-sizing directive,
18
+ * mirroring CSS `object-fit`. `contain`: fit the whole image, scale both ways,
19
+ * letterbox. `cover`: fill the frame, centre-crop the overflow. `scale-down`:
20
+ * like contain but never enlarge past native (a small image sits at native size).
21
+ */
22
+ export const ImageFit = {
23
+ Contain: "contain",
24
+ Cover: "cover",
25
+ ScaleDown: "scale-down",
26
+ };
package/dist/index.d.ts CHANGED
@@ -1,18 +1,5 @@
1
- import { type Config, type ThemeConfig } from "./engine/index.js";
2
- import { type CompilerConfig, type CompilerDeck, type CompilerThemeConfig, type ResolvedCompilerDeck } from "./markdown/types.js";
3
- /**
4
- * Run every compiler-owned resolver over `deck` (highlight code fences,
5
- * render mermaid PNGs) and return a `ResolvedCompilerDeck` whose content
6
- * values are narrowed to the engine's `TextFill | TableFill | ImageFill |
7
- * TemplateFill` union. Structurally equivalent to the engine's `Deck` — a
8
- * caller passes the returned value straight to `generate()` with no cast.
9
- *
10
- * Fails fast if `deck.output` is missing: downstream `generate()` requires it,
11
- * and the CLI populates it before calling `buildDeck`; a programmatic caller
12
- * that forgot to set it hits the error here instead of a confusing engine-side
13
- * failure.
14
- */
15
- export declare function resolveDeck(deck: CompilerDeck, config: CompilerConfig): Promise<ResolvedCompilerDeck>;
1
+ import { type Config, type GenerateOptions, type ThemeConfig } from "./engine/index.js";
2
+ import { type CompilerConfig, type CompilerDeck, type CompilerThemeConfig } from "./markdown/types.js";
16
3
  /**
17
4
  * Project a CompilerThemeConfig down to the engine's ThemeConfig shape.
18
5
  * Fields are copied cell-by-cell so the boundary is explicit — no casts.
@@ -28,19 +15,26 @@ export declare function toEngineThemeConfig(config: CompilerThemeConfig): ThemeC
28
15
  */
29
16
  export declare function toEngineConfig(config: CompilerConfig): Config;
30
17
  /**
31
- * End-to-end build: run compiler-owned resolvers (syntax highlighting, mermaid
32
- * PNG rendering) over the deck via `resolveDeck`, which returns a narrowed
33
- * `ResolvedCompilerDeck` structurally equivalent to the engine's `Deck`
34
- * then hand it to the engine's primitives-only `generate()`. No cast required:
35
- * the narrowing happens at the type level via `resolveDeck`.
18
+ * End-to-end build: `compileDeck` already produced engine-shaped content (code
19
+ * highlighted, mermaid rendered), so `buildDeck` only asserts an `output` is set
20
+ * and hands the deck to the engine's primitives-only `generate()`. The deck is
21
+ * structurally equivalent to the engine's `Deck` once `output` is present, so no
22
+ * cast is required. `buildDeck` does not itself validate `config` — a
23
+ * programmatic caller assembling a `CompilerConfig` by hand should load it
24
+ * through `loadThemeConfig` (or `parseThemeConfig`) first to get the same
25
+ * fail-fast structural checks the CLI gets.
26
+ *
27
+ * Fails fast if `deck.output` is missing: `generate()` requires it, and the CLI
28
+ * populates it before calling `buildDeck`; a programmatic caller that forgot to
29
+ * set it hits this error instead of a confusing engine-side failure.
36
30
  *
37
31
  * Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
38
32
  * post-write cleanup is needed.
39
33
  */
40
- export declare function buildDeck(deck: CompilerDeck, config: CompilerConfig): Promise<void>;
41
- export type { Config, Deck, DeckStep, ImageFill, Layout, Slot, StyledParagraph, TableFill, TextFill, TextRun, ThemeConfig, } from "./engine/index.js";
42
- export { FitMode, fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
34
+ export declare function buildDeck(deck: CompilerDeck, config: CompilerConfig, options?: GenerateOptions): Promise<void>;
35
+ export type { Config, Deck, DeckStep, GenerateOptions, ImageFill, Layout, Slot, StyledParagraph, TableFill, TextFill, TextRun, ThemeConfig, } from "./engine/index.js";
36
+ export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
43
37
  export type { ManifestOptions } from "./manifest.js";
44
38
  export { generateManifest } from "./manifest.js";
45
- export type { AssetCatalog, AssetEntry, CodeFence, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, MarkdownBlock, MermaidConfig, MermaidFence, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
46
- export { CompilerSlotType, compileMarkdownDeck, FenceType, ParameterType, resolveFences } from "./markdown/index.js";
39
+ export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, Limit, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
40
+ export { AcceptType, compileMarkdownDeck, loadThemeConfig, ParameterType, parseThemeConfig } from "./markdown/index.js";