@tycoworks/tycoslide 0.8.0 → 0.10.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 (54) hide show
  1. package/README.md +12 -9
  2. package/SKILL.md +6 -7
  3. package/dist/cli.js +39 -59
  4. package/dist/engine/fillers/filler.d.ts +21 -13
  5. package/dist/engine/fillers/filler.js +21 -24
  6. package/dist/engine/generate.d.ts +23 -17
  7. package/dist/engine/generate.js +157 -70
  8. package/dist/engine/index.d.ts +1 -1
  9. package/dist/engine/types.d.ts +39 -9
  10. package/dist/index.d.ts +15 -21
  11. package/dist/index.js +63 -88
  12. package/dist/manifest.js +17 -25
  13. package/dist/markdown/blocks/code.d.ts +17 -0
  14. package/dist/markdown/blocks/code.js +65 -0
  15. package/dist/markdown/blocks/image.d.ts +2 -0
  16. package/dist/markdown/blocks/image.js +9 -0
  17. package/dist/markdown/blocks/mermaid.d.ts +15 -0
  18. package/dist/markdown/blocks/mermaid.js +227 -0
  19. package/dist/markdown/{resolvers → blocks}/mermaidTheme.d.ts +1 -1
  20. package/dist/markdown/{resolvers → blocks}/mermaidTheme.js +1 -1
  21. package/dist/markdown/blocks/registry.d.ts +16 -0
  22. package/dist/markdown/blocks/registry.js +44 -0
  23. package/dist/markdown/blocks/table.d.ts +2 -0
  24. package/dist/markdown/blocks/table.js +23 -0
  25. package/dist/markdown/blocks/text.d.ts +12 -0
  26. package/dist/markdown/blocks/text.js +90 -0
  27. package/dist/markdown/deckCompiler.d.ts +17 -20
  28. package/dist/markdown/deckCompiler.js +143 -113
  29. package/dist/markdown/index.d.ts +11 -11
  30. package/dist/markdown/index.js +9 -8
  31. package/dist/markdown/inline.d.ts +26 -0
  32. package/dist/markdown/inline.js +136 -0
  33. package/dist/markdown/mdast.d.ts +25 -0
  34. package/dist/markdown/mdast.js +49 -0
  35. package/dist/markdown/schema/deckSchema.d.ts +30 -0
  36. package/dist/markdown/schema/deckSchema.js +51 -0
  37. package/dist/markdown/schema/strict.d.ts +9 -0
  38. package/dist/markdown/schema/strict.js +18 -0
  39. package/dist/markdown/schema/themeConfigSchema.d.ts +106 -0
  40. package/dist/markdown/schema/themeConfigSchema.js +147 -0
  41. package/dist/markdown/types.d.ts +194 -134
  42. package/dist/markdown/types.js +34 -23
  43. package/dist/skillZip.d.ts +17 -0
  44. package/dist/skillZip.js +35 -0
  45. package/package.json +7 -3
  46. package/syntax.md +1 -1
  47. package/dist/markdown/parsers.d.ts +0 -32
  48. package/dist/markdown/parsers.js +0 -233
  49. package/dist/markdown/resolvers/code.d.ts +0 -17
  50. package/dist/markdown/resolvers/code.js +0 -44
  51. package/dist/markdown/resolvers/mermaid.d.ts +0 -14
  52. package/dist/markdown/resolvers/mermaid.js +0 -81
  53. package/dist/markdown/resolvers/resolver.d.ts +0 -42
  54. package/dist/markdown/resolvers/resolver.js +0 -52
@@ -18,9 +18,13 @@
18
18
  * file (registered by generate(), swapped by the ImageFiller) and adjusts
19
19
  * geometry for the chosen fit. See fillers/image.ts.
20
20
  *
21
- * generate() loads the template, registers media, and for each DeckStep walks
22
- * the unified `layout.slots`. Each value in `step.content` is dispatched by the
23
- * slot's type via `FILLERS[slot.type]` (required, no default).
21
+ * generate() loads the template, registers media, and for each DeckStep clones
22
+ * the layout's base slide and calls `fillSlide`. Each value in `step.content`
23
+ * selects, by its own shape, the `Block` in the slot's `accepts` whose type it
24
+ * matches: a base-slide block fills in place; any other block is transplanted
25
+ * from its source slide onto the clone, then filled with the same callbacks.
26
+ *
27
+ * `generate()` is first below; its helpers follow (function declarations hoist).
24
28
  */
25
29
  import { existsSync, rmSync } from "node:fs";
26
30
  import { basename, dirname, resolve } from "node:path";
@@ -36,9 +40,9 @@ import { applyNotesToSlide, sweepOrphanNotes } from "./notes.js";
36
40
  * 1. Load the template; register it twice (as root and under an alias).
37
41
  * 2. Pre-register every image's media buffer with pptx-automizer, walking
38
42
  * the unified `step.content` for ImageFill values.
39
- * 3. For each deck step, clone the layout's source slide, then within the
40
- * addSlide callback walk `layout.slots` once and dispatch by slot.type
41
- * via `FILLERS[slot.type]`.
43
+ * 3. For each deck step, clone the layout's base slide, then within the
44
+ * addSlide callback call `fillSlide` to dispatch each content value to the
45
+ * matching `Block` (fill in place, or transplant + fill).
42
46
  * 4. Write the output PPTX.
43
47
  */
44
48
  export async function generate(deck, config, options = {}) {
@@ -62,33 +66,17 @@ export async function generate(deck, config, options = {}) {
62
66
  throw new Error(`Unknown layout: ${name}`);
63
67
  return match;
64
68
  };
65
- // Register the media for every image slot. Each file is validated (absolute
66
- // paths are the caller's responsibility; a stray relative path trips the
67
- // existsSync guard here or readFileSync in fillImage) and handed to
68
- // pptx-automizer once per distinct filename.
69
- const registeredMedia = new Set();
70
- for (const step of deck.steps) {
71
- for (const value of Object.values(step.content ?? {})) {
72
- if (!isImageFill(value))
73
- continue;
74
- if (!existsSync(value.path)) {
75
- throw new Error(`Layout "${step.layout}" image "${value.path}": file not found`);
76
- }
77
- const file = basename(value.path);
78
- if (registeredMedia.has(file))
79
- continue;
80
- registeredMedia.add(file);
81
- pres.loadMedia(file, dirname(value.path));
82
- }
83
- }
84
- // pptx-automizer runs modifyElement callbacks during write() and SWALLOWS any
85
- // error they throw (it logs a stack trace but keeps going, producing a broken
86
- // slide). A fill primitive's fail-fast throw would therefore never fail the
87
- // build. Collect those errors and re-surface them after write().
69
+ registerMedia(pres, deck);
70
+ assertLayoutsWellFormed(layouts);
71
+ // pptx-automizer runs fill callbacks during write() and SWALLOWS any error they
72
+ // throw (it logs a stack trace but keeps going, producing a broken slide). A
73
+ // fill primitive's fail-fast throw would therefore never fail the build. This
74
+ // wraps BOTH fill entry points — `modifyElement` (in-place) and `addElement`
75
+ // (transplant) so a throw on a transplanted shape fails the build too.
88
76
  const fillErrors = [];
89
- const captureFillErrors = (slide) => {
90
- const modifyElement = slide.modifyElement.bind(slide);
91
- slide.modifyElement = (shapeName, callbacks) => modifyElement(shapeName, callbacks.map((cb) => typeof cb === "function"
77
+ const wrapCallbacks = (callbacks) => {
78
+ const arr = Array.isArray(callbacks) ? callbacks : callbacks === undefined ? [] : [callbacks];
79
+ return arr.map((cb) => typeof cb === "function"
92
80
  ? (...args) => {
93
81
  try {
94
82
  return cb(...args);
@@ -98,29 +86,13 @@ export async function generate(deck, config, options = {}) {
98
86
  throw err;
99
87
  }
100
88
  }
101
- : cb));
89
+ : cb);
102
90
  };
103
- // Populate one cloned slide: for each declared slot that the step supplies a
104
- // value for, hand the value to the filler registered for the slot's type.
105
- const fillSlide = (slide, layout, step) => {
106
- captureFillErrors(slide);
107
- for (const slot of layout.slots) {
108
- const value = step.content?.[slot.key];
109
- if (value === undefined)
110
- continue;
111
- if (typeof value === "string") {
112
- // Fallback: bare string. Compiler normally normalizes to
113
- // StyledParagraph[] / ImageFill; this branch only fires when
114
- // callers construct decks by hand.
115
- slide.modifyElement(slot.shapeName, [modify.setText(value)]);
116
- continue;
117
- }
118
- const filler = FILLERS[slot.type];
119
- if (!filler.matches(value)) {
120
- throw new Error(`Layout "${step.layout}" slot "${slot.key}" (type "${slot.type}"): expected ${filler.label}, got ${describeValue(value)}`);
121
- }
122
- filler.fill(slide, slot, value, { layoutName: step.layout });
123
- }
91
+ const captureFillErrors = (slide) => {
92
+ const modifyElement = slide.modifyElement.bind(slide);
93
+ slide.modifyElement = (shapeName, callbacks) => modifyElement(shapeName, wrapCallbacks(callbacks));
94
+ const addElement = slide.addElement.bind(slide);
95
+ slide.addElement = (presName, slideNumber, selector, callbacks) => addElement(presName, slideNumber, selector, wrapCallbacks(callbacks));
124
96
  };
125
97
  // Default: write authored notes and strip any template notes automizer clones
126
98
  // onto slides. When true, no notes are written but inherited notes are still
@@ -128,8 +100,9 @@ export async function generate(deck, config, options = {}) {
128
100
  const excludeNotes = options.excludeNotes ?? false;
129
101
  for (const step of deck.steps) {
130
102
  const layout = resolveLayout(step.layout);
131
- pres.addSlide(sourceAlias, layout.slideNumber, (slide) => {
132
- fillSlide(slide, layout, step);
103
+ pres.addSlide(sourceAlias, layout.baseSlide, (slide) => {
104
+ captureFillErrors(slide);
105
+ fillSlide(slide, layout, step, sourceAlias);
133
106
  // In-band notes pass: automizer runs this during write() and hands us the
134
107
  // OUTPUT archive (parent.targetArchive) and the real output slide number
135
108
  // (parent.targetNumber), so notes map 1:1 with no slide-number mapping.
@@ -162,6 +135,35 @@ export async function generate(deck, config, options = {}) {
162
135
  }
163
136
  console.log(`tycoslide: built ${deck.steps.length} slide(s) → ${resolve(outDir, outFile)}`);
164
137
  }
138
+ // ── generate() helpers ───────────────────────────────────────────────────────
139
+ /**
140
+ * Register the media for every image slot. Each file is validated (absolute
141
+ * paths are the caller's responsibility; a stray relative path trips the
142
+ * existsSync guard here or readFileSync in fillImage) and handed to
143
+ * pptx-automizer once per distinct filename.
144
+ */
145
+ function registerMedia(pres, deck) {
146
+ const registeredMedia = new Set();
147
+ for (const step of deck.steps) {
148
+ for (const value of Object.values(step.content ?? {})) {
149
+ if (!isImageFill(value))
150
+ continue;
151
+ if (!existsSync(value.path)) {
152
+ throw new Error(`Layout "${step.layout}" image "${value.path}": file not found`);
153
+ }
154
+ const file = basename(value.path);
155
+ if (registeredMedia.has(file))
156
+ continue;
157
+ registeredMedia.add(file);
158
+ pres.loadMedia(file, dirname(value.path));
159
+ }
160
+ }
161
+ }
162
+ // Validate every layout once, up front (see assertSlotsWellFormed).
163
+ function assertLayoutsWellFormed(layouts) {
164
+ for (const layout of layouts)
165
+ assertSlotsWellFormed(layout);
166
+ }
165
167
  function describeValue(v) {
166
168
  if (v === null)
167
169
  return "null";
@@ -173,6 +175,105 @@ function describeValue(v) {
173
175
  }
174
176
  return typeof v;
175
177
  }
178
+ // ── Slot fill dispatch (composition-aware) ───────────────────────────────────
179
+ /**
180
+ * Fill one cloned slide. Per slot the step supplies a value for: resolve WHICH
181
+ * shape realizes it (`resolveBlock`), build WHAT to write (`FILLERS[…].callbacks`),
182
+ * and place it WHERE/HOW (`applyBlock`). Every ambiguity fails fast, naming layout
183
+ * + slot.
184
+ *
185
+ * Exported for tests; `generate()` calls it inside the `addSlide` callback.
186
+ */
187
+ export function fillSlide(slide, layout, step, sourceAlias) {
188
+ assertNoUnknownSlots(step, layout);
189
+ for (const slot of layout.slots) {
190
+ const value = step.content?.[slot.key];
191
+ if (value === undefined)
192
+ continue; // Empty slot: leave the base slide's shape untouched.
193
+ const block = resolveBlock(step, slot, value);
194
+ const callbacks = FILLERS[block.type].callbacks(value, targetOf(block));
195
+ applyBlock(slide, sourceAlias, layout.baseSlide, slot, block, callbacks);
196
+ }
197
+ }
198
+ /** The SlotType a resolved `*Fill` value maps to, via the filler discriminators. */
199
+ function fillTypeOf(value) {
200
+ for (const type of Object.keys(FILLERS)) {
201
+ if (FILLERS[type].matches(value))
202
+ return type;
203
+ }
204
+ return undefined;
205
+ }
206
+ /**
207
+ * Reject a slot whose `accepts` lists two blocks of the same type — the
208
+ * value→block lookup would silently pick the first. Called once per layout at
209
+ * build start.
210
+ */
211
+ export function assertSlotsWellFormed(layout) {
212
+ for (const slot of layout.slots) {
213
+ const seen = new Set();
214
+ for (const block of slot.accepts) {
215
+ if (seen.has(block.type)) {
216
+ throw new Error(`Layout "${layout.name}" slot "${slot.key}": accepts two ${block.type} blocks; each content type may appear once.`);
217
+ }
218
+ seen.add(block.type);
219
+ }
220
+ }
221
+ }
222
+ /**
223
+ * A deck supplying content for a slot the layout doesn't declare is an authoring
224
+ * mistake, not a silent no-op. Runs per step, over `step.content`'s keys.
225
+ */
226
+ function assertNoUnknownSlots(step, layout) {
227
+ const keys = new Set(layout.slots.map((s) => s.key));
228
+ for (const key of Object.keys(step.content ?? {})) {
229
+ if (!keys.has(key)) {
230
+ const declared = layout.slots.map((s) => s.key).join(", ") || "none";
231
+ throw new Error(`Layout "${step.layout}" has no slot "${key}" (declared slots: ${declared}).`);
232
+ }
233
+ }
234
+ }
235
+ /**
236
+ * WHICH shape: pick the `Block` in `slot.accepts` whose type matches the value's
237
+ * shape. Fails fast — first on an unrecognized value, then on a value no block
238
+ * accepts (order matters; both messages are asserted).
239
+ */
240
+ function resolveBlock(step, slot, value) {
241
+ const requestedType = fillTypeOf(value);
242
+ if (requestedType === undefined) {
243
+ throw new Error(`Layout "${step.layout}" slot "${slot.key}": unrecognized content value ${describeValue(value)}.`);
244
+ }
245
+ const block = slot.accepts.find((b) => b.type === requestedType);
246
+ if (!block) {
247
+ const available = slot.accepts.map((b) => b.type).join(", ") || "none";
248
+ throw new Error(`Layout "${step.layout}" slot "${slot.key}": no block accepts ${requestedType} content (this slot accepts: ${available}).`);
249
+ }
250
+ return block;
251
+ }
252
+ /** The shape a filler targets, plus `startAt` when the (text) block declares it. */
253
+ function targetOf(block) {
254
+ const target = { shapeName: block.shapeName };
255
+ if (block.startAt !== undefined)
256
+ target.startAt = block.startAt;
257
+ return target;
258
+ }
259
+ /**
260
+ * WHERE/HOW: place the (already-built) fill callbacks. A base-slide block is
261
+ * already on the cloned slide → fill in place. Any other block is transplanted:
262
+ * pptx-automizer runs an appended shape's callbacks against the imported element
263
+ * itself, so the same callbacks refill it — position it to the slot's frame,
264
+ * then remove the base shape it supersedes (a base block, if this slot has one —
265
+ * a slot need not).
266
+ */
267
+ function applyBlock(slide, sourceAlias, baseSlide, slot, block, callbacks) {
268
+ if (block.sourceSlide === baseSlide) {
269
+ slide.modifyElement(block.shapeName, callbacks);
270
+ return;
271
+ }
272
+ slide.addElement(sourceAlias, block.sourceSlide, block.shapeName, [modify.setPosition(slot.frame), ...callbacks]);
273
+ const baseBlock = slot.accepts.find((b) => b.sourceSlide === baseSlide);
274
+ if (baseBlock && baseBlock.shapeName !== block.shapeName)
275
+ slide.removeElement(baseBlock.shapeName);
276
+ }
176
277
  // ── Notes archive adapter (automizer-buffer glue) ────────────────────────────
177
278
  /** Installed pptx-automizer version this notes-buffer adapter is pinned to. */
178
279
  const PPTX_AUTOMIZER_VERSION = "0.8.2";
@@ -227,17 +328,3 @@ function toNotesArchive(target) {
227
328
  },
228
329
  };
229
330
  }
230
- // ── Content-slot validation (test-visible helper) ────────────────────────────
231
- /**
232
- * Validate that every required slot on a layout is supplied. Type/shape
233
- * validation happens in the compiler now — this is a thin required-only
234
- * checker kept for tests and defensive callers.
235
- *
236
- * Exported for tests; not part of the public engine surface (index.ts).
237
- */
238
- export function validateContentSlots(step, tpl) {
239
- const missing = tpl.slots.filter((s) => step.content?.[s.key] === undefined).map((s) => s.key);
240
- if (missing.length > 0) {
241
- throw new Error(`Layout "${step.layout}": missing content for slot(s): ${missing.join(", ")}`);
242
- }
243
- }
@@ -5,5 +5,5 @@ export { fillTemplate } from "./fillers/template.js";
5
5
  export { fillText, isTextFill } from "./fillers/text.js";
6
6
  export type { GenerateOptions } from "./generate.js";
7
7
  export { generate } from "./generate.js";
8
- export type { Config, Deck, DeckStep, ImageFill, Layout, Slot, StyledParagraph, TableFill, TemplateFill, TemplateSegment, TextFill, TextRun, ThemeConfig, } from "./types.js";
8
+ export type { Block, Config, Deck, DeckStep, Frame, ImageFill, Layout, Slot, StyledParagraph, TableFill, TemplateFill, TemplateSegment, TextFill, TextRun, ThemeConfig, } from "./types.js";
9
9
  export { ImageFit, SlotType } from "./types.js";
@@ -93,20 +93,50 @@ export type ImageFill = {
93
93
  path: string;
94
94
  fit: ImageFit;
95
95
  };
96
- export type Slot = {
97
- key: string;
98
- shapeName: string;
99
- /** 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 = {
100
117
  type: SlotType;
101
- /** Leave the first N specimen paragraphs untouched (fillText only). */
118
+ sourceSlide: number;
119
+ shapeName: string;
102
120
  startAt?: number;
103
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[];
132
+ };
104
133
  export type Layout = {
105
134
  name: string;
106
- slideNumber: number;
107
- description: string;
108
- whenToUse: string;
109
- 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;
110
140
  slots: Slot[];
111
141
  };
112
142
  export type DeckStep = {
package/dist/index.d.ts CHANGED
@@ -1,18 +1,5 @@
1
1
  import { type Config, type GenerateOptions, 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>;
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,11 +15,18 @@ 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.
@@ -42,5 +36,5 @@ export type { Config, Deck, DeckStep, GenerateOptions, ImageFill, Layout, Slot,
42
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";
package/dist/index.js CHANGED
@@ -1,99 +1,64 @@
1
1
  import { generate, SlotType, } from "./engine/index.js";
2
- import { isFence, resolveFences } from "./markdown/resolvers/resolver.js";
3
- import { CompilerSlotType, ParameterType, } from "./markdown/types.js";
2
+ import { ParameterType, } from "./markdown/types.js";
4
3
  /**
5
- * Narrow a resolved content map to the engine-shaped value union. Runs after
6
- * `resolveFences`, so no CodeFence or MermaidFence should remain a leftover is
7
- * a resolver bug and throws. Every surviving value is already an engine fill
8
- * (TextFill / TableFill / ImageFill / TemplateFill), so no unwrapping is needed.
4
+ * A frontmatter parameter always fills one physical shape on the layout's own
5
+ * slide, so it projects to a single base `Block` (`sourceSlide === baseSlide`)
6
+ * and never transplants its `frame` is never read. `NO_FRAME` is that unread
7
+ * placeholder; only body slots with a transplant block carry a real frame.
9
8
  */
10
- function narrowContent(content) {
11
- const out = {};
12
- for (const [key, value] of Object.entries(content)) {
13
- if (isFence(value)) {
14
- throw new Error(`resolveDeck: slot "${key}" still holds an unresolved ${value.type} block ` +
15
- "after resolvers ran. This is a resolver bug.");
16
- }
17
- out[key] = value;
9
+ const NO_FRAME = { x: 0, y: 0, cx: 0, cy: 0 };
10
+ function paramToEngineSlot(param, baseSlide) {
11
+ const block = (type) => ({ type, sourceSlide: baseSlide, shapeName: param.shapeName });
12
+ switch (param.type) {
13
+ case ParameterType.Template:
14
+ // A text shape carries no top-level key — its template placeholders are the keys. The
15
+ // compiler emits its expanded content under shapeName, so the engine slot
16
+ // is keyed by shapeName too.
17
+ return { key: param.shapeName, frame: NO_FRAME, accepts: [block(SlotType.Template)] };
18
+ case ParameterType.Image:
19
+ return { key: param.key, frame: NO_FRAME, accepts: [block(SlotType.Image)] };
18
20
  }
19
- return out;
20
21
  }
21
22
  /**
22
- * Run every compiler-owned resolver over `deck` (highlight code fences,
23
- * render mermaid PNGs) and return a `ResolvedCompilerDeck` whose content
24
- * values are narrowed to the engine's `TextFill | TableFill | ImageFill |
25
- * TemplateFill` union. Structurally equivalent to the engine's `Deck` a
26
- * caller passes the returned value straight to `generate()` with no cast.
27
- *
28
- * Fails fast if `deck.output` is missing: downstream `generate()` requires it,
29
- * and the CLI populates it before calling `buildDeck`; a programmatic caller
30
- * that forgot to set it hits the error here instead of a confusing engine-side
31
- * failure.
23
+ * Project a body slot's real `accepts` to engine `Block[]` and pass its `frame`
24
+ * through. The compiler `accepts` already carry engine content types
25
+ * (text/table/image) code folded to text and mermaid to image at authoring
26
+ * so projection is 1:1. A slot with no declared `frame` (a base-only slot that
27
+ * never transplants) gets `NO_FRAME`, which the engine never reads.
32
28
  */
33
- export async function resolveDeck(deck, config) {
34
- await resolveFences(deck, config);
35
- if (deck.output === undefined) {
36
- throw new Error('resolveDeck: deck.output is not set. Set it (e.g. "deck.pptx") before calling buildDeck.');
37
- }
38
- return {
39
- theme: deck.theme,
40
- output: deck.output,
41
- steps: deck.steps.map((step) => {
42
- const resolvedStep = { layout: step.layout };
43
- if (step.content)
44
- resolvedStep.content = narrowContent(step.content);
45
- if (step.notes !== undefined)
46
- resolvedStep.notes = step.notes;
47
- return resolvedStep;
48
- }),
49
- };
29
+ function slotToEngineSlot(slot) {
30
+ const accepts = slot.accepts.map((b) => {
31
+ const eb = { type: b.type, sourceSlide: b.sourceSlide, shapeName: b.shapeName };
32
+ if (b.startAt !== undefined)
33
+ eb.startAt = b.startAt;
34
+ return eb;
35
+ });
36
+ return { key: slot.key, frame: slot.frame ?? NO_FRAME, accepts };
50
37
  }
51
38
  /**
52
- * Project a CompilerParameter or CompilerSlot down to the engine's flat Slot.
53
- * Parameters (Template, Image) map straight to their engine equivalent; compiler-
54
- * only slot types (Code, Mermaid) map to Text / Image since their resolved
55
- * StyledParagraph[] / ImageFill content is filled by the corresponding engine
56
- * primitive once the compiler is done.
57
- *
58
- * The discriminated unions narrow per-variant fields, so the projection is a
59
- * straight switch over all six type values — no runtime "wrong field on wrong
60
- * type" checks; TypeScript enforces the invariants at authoring time.
39
+ * Compiler→engine boundary check: a slot with a transplant block (any block
40
+ * whose `sourceSlide` differs from the layout's base slide) must declare a
41
+ * `frame` the real region the transplant is positioned into. A base-only slot
42
+ * (all blocks in place) needs none. Missing fail fast, naming layout + slot.
43
+ * (The engine's `assertSlotsWellFormed` separately rejects duplicate accept
44
+ * types.)
61
45
  */
62
- function toEngineSlot(slot) {
63
- switch (slot.type) {
64
- case ParameterType.Template:
65
- // A text shape carries no top-level key — its template placeholders are the keys. The
66
- // compiler emits its expanded content under shapeName, so the engine slot
67
- // is keyed by shapeName too.
68
- return { key: slot.shapeName, shapeName: slot.shapeName, type: SlotType.Template };
69
- case ParameterType.Image:
70
- return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Image };
71
- case CompilerSlotType.Text: {
72
- const result = { key: slot.key, shapeName: slot.shapeName, type: SlotType.Text };
73
- if (slot.startAt !== undefined)
74
- result.startAt = slot.startAt;
75
- return result;
46
+ function assertSlotFrames(layout) {
47
+ for (const slot of layout.slots) {
48
+ const transplants = slot.accepts.some((b) => b.sourceSlide !== layout.slideNumber);
49
+ if (transplants && slot.frame === undefined) {
50
+ throw new Error(`Layout "${layout.name}" slot "${slot.key}": a transplant block (sourceSlide ${layout.slideNumber}) ` +
51
+ 'requires a "frame" (the region to position it into), but none is declared.');
76
52
  }
77
- case CompilerSlotType.Table:
78
- return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Table };
79
- case CompilerSlotType.Code:
80
- // Highlighter resolves the code fence into StyledParagraph[]; engine
81
- // fills it via fillText.
82
- return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Text };
83
- case CompilerSlotType.Mermaid:
84
- // Mermaid renderer produces a PNG (ImageFill); engine fills it via
85
- // fillImage. The fit lives on the ImageFill, not the engine Slot.
86
- return { key: slot.key, shapeName: slot.shapeName, type: SlotType.Image };
87
53
  }
88
54
  }
89
55
  function toEngineLayout(layout) {
56
+ assertSlotFrames(layout);
57
+ const base = layout.slideNumber;
90
58
  return {
91
59
  name: layout.name,
92
- slideNumber: layout.slideNumber,
93
- description: layout.description,
94
- whenToUse: layout.whenToUse,
95
- whenNotToUse: layout.whenNotToUse,
96
- slots: [...layout.parameters.map(toEngineSlot), ...layout.slots.map(toEngineSlot)],
60
+ baseSlide: base,
61
+ slots: [...layout.parameters.map((p) => paramToEngineSlot(p, base)), ...layout.slots.map(slotToEngineSlot)],
97
62
  };
98
63
  }
99
64
  /**
@@ -124,21 +89,31 @@ export function toEngineConfig(config) {
124
89
  };
125
90
  }
126
91
  /**
127
- * End-to-end build: run compiler-owned resolvers (syntax highlighting, mermaid
128
- * PNG rendering) over the deck via `resolveDeck`, which returns a narrowed
129
- * `ResolvedCompilerDeck` structurally equivalent to the engine's `Deck`
130
- * then hand it to the engine's primitives-only `generate()`. No cast required:
131
- * the narrowing happens at the type level via `resolveDeck`.
92
+ * End-to-end build: `compileDeck` already produced engine-shaped content (code
93
+ * highlighted, mermaid rendered), so `buildDeck` only asserts an `output` is set
94
+ * and hands the deck to the engine's primitives-only `generate()`. The deck is
95
+ * structurally equivalent to the engine's `Deck` once `output` is present, so no
96
+ * cast is required. `buildDeck` does not itself validate `config` — a
97
+ * programmatic caller assembling a `CompilerConfig` by hand should load it
98
+ * through `loadThemeConfig` (or `parseThemeConfig`) first to get the same
99
+ * fail-fast structural checks the CLI gets.
100
+ *
101
+ * Fails fast if `deck.output` is missing: `generate()` requires it, and the CLI
102
+ * populates it before calling `buildDeck`; a programmatic caller that forgot to
103
+ * set it hits this error instead of a confusing engine-side failure.
132
104
  *
133
105
  * Mermaid PNGs are cached under `<outputDir>/.tycoslide-cache/mermaid/` so no
134
106
  * post-write cleanup is needed.
135
107
  */
136
108
  export async function buildDeck(deck, config, options = {}) {
137
- const resolved = await resolveDeck(deck, config);
138
- await generate(resolved, toEngineConfig(config), options);
109
+ if (deck.output === undefined) {
110
+ throw new Error('buildDeck: deck.output is not set. Set it (e.g. "deck.pptx") before calling buildDeck.');
111
+ }
112
+ const engineDeck = { theme: deck.theme, output: deck.output, steps: deck.steps };
113
+ await generate(engineDeck, toEngineConfig(config), options);
139
114
  }
140
115
  // Engine — primitives-only public surface.
141
116
  export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
142
117
  export { generateManifest } from "./manifest.js";
143
118
  // Markdown / Compiler
144
- export { CompilerSlotType, compileMarkdownDeck, FenceType, ParameterType, resolveFences } from "./markdown/index.js";
119
+ export { AcceptType, compileMarkdownDeck, loadThemeConfig, ParameterType, parseThemeConfig } from "./markdown/index.js";